From 502e6af008757204e53f04eb5cbfe7f586558989 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 00:21:54 +0900 Subject: [PATCH 01/21] Remove PlayingUsers list from SpectatorClient --- .../Gameplay/TestSceneSpectatorPlayback.cs | 38 ------------------- ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 10 ----- .../Dashboard/CurrentlyPlayingDisplay.cs | 30 +++++++-------- .../Visual/Spectator/TestSpectatorClient.cs | 7 ++-- 5 files changed, 19 insertions(+), 68 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 90abdf2ba3..75369661a0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -3,12 +3,9 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -20,7 +17,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; @@ -47,8 +43,6 @@ namespace osu.Game.Tests.Visual.Gameplay private Replay replay; - private readonly IBindableList users = new BindableList(); - private TestReplayRecorder recorder; private ManualClock manualClock; @@ -57,9 +51,6 @@ namespace osu.Game.Tests.Visual.Gameplay private TestFramedReplayInputHandler replayHandler; - [Resolved] - private IAPIProvider api { get; set; } - [Resolved] private SpectatorClient spectatorClient { get; set; } @@ -78,35 +69,6 @@ namespace osu.Game.Tests.Visual.Gameplay spectatorClient.OnNewFrames += onNewFrames; - users.BindTo(spectatorClient.PlayingUsers); - users.BindCollectionChanged((obj, args) => - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - Debug.Assert(args.NewItems != null); - - foreach (int user in args.NewItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.WatchUser(user); - } - - break; - - case NotifyCollectionChangedAction.Remove: - Debug.Assert(args.OldItems != null); - - foreach (int user in args.OldItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.StopWatchingUser(user); - } - - break; - } - }, true); - Children = new Drawable[] { new GridContainer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 9b8e67b07a..8b71dfac21 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RandomlyUpdateState() { - foreach (int userId in PlayingUsers) + foreach ((int userId, _) in PlayingUserStates) { if (RNG.NextBool()) continue; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index fddb94fad7..c3e70edcd3 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -37,9 +37,6 @@ namespace osu.Game.Online.Spectator private readonly List watchingUsers = new List(); - public IBindableList PlayingUsers => playingUsers; - private readonly BindableList playingUsers = new BindableList(); - public IBindableDictionary PlayingUserStates => playingUserStates; private readonly BindableDictionary playingUserStates = new BindableDictionary(); @@ -88,10 +85,7 @@ namespace osu.Game.Online.Spectator BeginPlayingInternal(currentState); } else - { - playingUsers.Clear(); playingUserStates.Clear(); - } }), true); } @@ -99,9 +93,6 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - if (!playingUsers.Contains(userId)) - playingUsers.Add(userId); - // UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched. // This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29). // We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations. @@ -118,7 +109,6 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - playingUsers.Remove(userId); playingUserStates.Remove(userId); OnUserFinishedPlaying?.Invoke(userId, state); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index fde20575fc..afbfb9f7ef 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private readonly IBindableList playingUsers = new BindableList(); + private readonly IBindableDictionary playingUserStates = new BindableDictionary(); private FillFlowContainer userFlow; @@ -51,18 +51,20 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - playingUsers.BindTo(spectatorClient.PlayingUsers); - playingUsers.BindCollectionChanged(onUsersChanged, true); + playingUserStates.BindTo(spectatorClient.PlayingUserStates); + playingUserStates.BindCollectionChanged(onUsersChanged, true); } - private void onUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => + private void onUsersChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { - case NotifyCollectionChangedAction.Add: - foreach (int id in e.NewItems.OfType().ToArray()) + case NotifyDictionaryChangedAction.Add: + Debug.Assert(e.NewItems != null); + + foreach ((int userId, _) in e.NewItems) { - users.GetUserAsync(id).ContinueWith(task => + users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); @@ -71,7 +73,7 @@ namespace osu.Game.Overlays.Dashboard Schedule(() => { // user may no longer be playing. - if (!playingUsers.Contains(user.Id)) + if (!playingUserStates.ContainsKey(user.Id)) return; userFlow.Add(createUserPanel(user)); @@ -81,13 +83,11 @@ namespace osu.Game.Overlays.Dashboard break; - case NotifyCollectionChangedAction.Remove: - foreach (int u in e.OldItems.OfType()) - userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire(); - break; + case NotifyDictionaryChangedAction.Remove: + Debug.Assert(e.OldItems != null); - case NotifyCollectionChangedAction.Reset: - userFlow.Clear(); + foreach ((int userId, _) in e.OldItems) + userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); break; } }); diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 1a1d493249..7848a825f4 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -42,7 +41,7 @@ namespace osu.Game.Tests.Visual.Spectator { OnNewFrames += (i, bundle) => { - if (PlayingUsers.Contains(i)) + if (PlayingUserStates.ContainsKey(i)) lastReceivedUserFrames[i] = bundle.Frames[^1]; }; } @@ -65,7 +64,7 @@ namespace osu.Game.Tests.Visual.Spectator /// The user to end play for. public void EndPlay(int userId) { - if (!PlayingUsers.Contains(userId)) + if (!PlayingUserStates.ContainsKey(userId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState @@ -131,7 +130,7 @@ namespace osu.Game.Tests.Visual.Spectator protected override Task WatchUserInternal(int userId) { // When newly watching a user, the server sends the playing state immediately. - if (PlayingUsers.Contains(userId)) + if (PlayingUserStates.ContainsKey(userId)) sendPlayingState(userId); return Task.CompletedTask; From 781cb9f18d55e21b153967ee389c1a45ca934e0a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 01:45:11 +0900 Subject: [PATCH 02/21] Move HasPassed/HasFailed into GameplayState --- .../TestSceneTaikoSuddenDeath.cs | 2 +- .../Visual/Gameplay/TestSceneFailAnimation.cs | 2 +- .../Visual/Gameplay/TestSceneFailJudgement.cs | 2 +- .../Visual/Gameplay/TestScenePause.cs | 12 ++++++------ .../TestScenePlayerScoreSubmission.cs | 4 ++-- .../Gameplay/TestSceneStoryboardWithOutro.cs | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 2 +- .../Multiplayer/MultiplayerPlayerLoader.cs | 2 +- osu.Game/Screens/Play/GameplayState.cs | 10 ++++++++++ osu.Game/Screens/Play/Player.cs | 19 ++++++------------- 10 files changed, 30 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 0be005e1c4..eec88d7bf8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Player.ScoreProcessor.NewJudgement += b => judged = true; }); AddUntilStep("swell judged", () => judged); - AddAssert("failed", () => Player.HasFailed); + AddAssert("failed", () => Player.GameplayState.HasFailed); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index 7167d3120a..744227c55e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible); // The pause screen and fail animation both ramp frequency. diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index fa27e1abdd..6430c29dfa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddAssert("total number of results == 1", () => { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 04676f656f..ea0255ab76 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPauseAfterFail() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayAfterFailAnimation() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayDuringFailAnimation() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); // will finish the fail animation and show the fail/pause screen. AddStep("attempt exit via pause key", () => Player.ExitViaPause()); @@ -227,7 +227,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickRetryFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); @@ -236,7 +236,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickExitFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); @@ -341,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay { confirmClockRunning(false); confirmNotExited(); - AddAssert("player not failed", () => !Player.HasFailed); + AddAssert("player not failed", () => !Player.GameplayState.HasFailed); AddAssert("pause overlay shown", () => Player.PauseOverlayVisible); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index a9675a2ee2..58b5df2612 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for token request", () => Player.TokenCreationRequested); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("exit", () => Player.Exit()); AddAssert("ensure no submission", () => Player.SubmittedScore == null); @@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay addFakeHit(); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("exit", () => Player.Exit()); AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 69798dcb82..b87183cbc7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600); }); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index ed484e03f6..63984a1bed 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -209,7 +209,7 @@ namespace osu.Game.Tests.Visual.Navigation return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); - AddUntilStep("wait for fail", () => player.HasFailed); + AddUntilStep("wait for fail", () => player.GameplayState.HasFailed); AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying); AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs index 470ba59a76..772651727e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerPlayerLoader : PlayerLoader { - public bool GameplayPassed => player?.GameplayPassed == true; + public bool GameplayPassed => player?.GameplayState.HasPassed == true; private Player player; diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 83881f739d..60fbfa6644 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -39,6 +39,16 @@ namespace osu.Game.Screens.Play /// public readonly Score Score; + /// + /// Whether gameplay completed without the user failing. + /// + public bool HasPassed { get; set; } + + /// + /// Whether the user failed during gameplay. + /// + public bool HasFailed { get; set; } + /// /// A bindable tracking the last judgement result applied to any hit object. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 0312789b12..814aacd2fa 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -72,15 +72,8 @@ namespace osu.Game.Screens.Play /// protected virtual bool PauseOnFocusLost => true; - /// - /// Whether gameplay has completed without the user having failed. - /// - public bool GameplayPassed { get; private set; } - public Action RestartRequested; - public bool HasFailed { get; private set; } - private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -559,7 +552,7 @@ namespace osu.Game.Screens.Play if (showDialogFirst && !pauseOrFailDialogVisible) { // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). - if (ValidForResume && HasFailed) + if (ValidForResume && GameplayState.HasFailed) { failAnimationLayer.FinishTransforms(true); return; @@ -678,7 +671,7 @@ namespace osu.Game.Screens.Play resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate = null; - GameplayPassed = false; + GameplayState.HasPassed = false; ValidForResume = true; skipOutroOverlay.Hide(); return; @@ -688,7 +681,7 @@ namespace osu.Game.Screens.Play if (HealthProcessor.HasFailed) return; - GameplayPassed = true; + GameplayState.HasPassed = true; // Setting this early in the process means that even if something were to go wrong in the order of events following, there // is no chance that a user could return to the (already completed) Player instance from a child screen. @@ -804,7 +797,7 @@ namespace osu.Game.Screens.Play if (!CheckModsAllowFailure()) return false; - HasFailed = true; + GameplayState.HasFailed = true; Score.ScoreInfo.Passed = false; // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) @@ -859,13 +852,13 @@ namespace osu.Game.Screens.Play // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value // cannot pause if we are already in a fail state - && !HasFailed; + && !GameplayState.HasFailed; private bool canResume => // cannot resume from a non-paused state GameplayClockContainer.IsPaused.Value // cannot resume if we are already in a fail state - && !HasFailed + && !GameplayState.HasFailed // already resuming && !IsResuming; From 38e075c522f72371742e4d02a948f7a53a0aac5d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 02:02:31 +0900 Subject: [PATCH 03/21] Add HasQuit gameplay state --- osu.Game/Screens/Play/GameplayState.cs | 5 +++++ osu.Game/Screens/Play/Player.cs | 3 +++ 2 files changed, 8 insertions(+) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 60fbfa6644..64e873b3bb 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -49,6 +49,11 @@ namespace osu.Game.Screens.Play /// public bool HasFailed { get; set; } + /// + /// Whether the user quit gameplay without either having either passed or failed. + /// + public bool HasQuit { get; set; } + /// /// A bindable tracking the last judgement result applied to any hit object. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 814aacd2fa..f6a2310826 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -983,6 +983,9 @@ namespace osu.Game.Screens.Play public override bool OnExiting(IScreen next) { + if (!GameplayState.HasPassed && !GameplayState.HasFailed) + GameplayState.HasQuit = true; + screenSuspension?.RemoveAndDisposeImmediately(); failAnimationLayer?.RemoveFilters(); From 41007169f722febe2217ff805cf95cdb6d81c3dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Feb 2022 15:51:41 +0900 Subject: [PATCH 04/21] Give SpectatorState a user state --- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../Online/Spectator/SpectatingUserState.cs | 33 +++++++++++++++++++ osu.Game/Online/Spectator/SpectatorClient.cs | 10 +++++- osu.Game/Online/Spectator/SpectatorState.cs | 7 ++-- osu.Game/Rulesets/UI/ReplayRecorder.cs | 4 ++- osu.Game/Screens/Play/Player.cs | 2 +- 6 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Online/Spectator/SpectatingUserState.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 917b3c89a8..86d6de6975 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send frames and finish play", () => { spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero)); - spectatorClient.EndPlaying(); + spectatorClient.EndPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()) { HasPassed = true }); }); // We can't access API because we're an "online" test. diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatingUserState.cs new file mode 100644 index 0000000000..c7ba4ba248 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatingUserState.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.Spectator +{ + public enum SpectatingUserState + { + /// + /// The spectated user has not yet played. + /// + Idle, + + /// + /// The spectated user is currently playing. + /// + Playing, + + /// + /// The spectated user has successfully completed gameplay. + /// + Completed, + + /// + /// The spectator user has failed during gameplay. + /// + Failed, + + /// + /// The spectated user has quit during gameplay. + /// + Quit + } +} diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index c3e70edcd3..f4161e1db9 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -138,6 +138,7 @@ namespace osu.Game.Online.Spectator currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); + currentState.State = SpectatingUserState.Playing; currentBeatmap = state.Beatmap; currentScore = score; @@ -148,7 +149,7 @@ namespace osu.Game.Online.Spectator public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data); - public void EndPlaying() + public void EndPlaying(GameplayState state) { // This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue). // We probably need to find a better way to handle this... @@ -163,6 +164,13 @@ namespace osu.Game.Online.Spectator IsPlaying = false; currentBeatmap = null; + if (state.HasPassed) + currentState.State = SpectatingUserState.Completed; + else if (state.HasFailed) + currentState.State = SpectatingUserState.Failed; + else + currentState.State = SpectatingUserState.Quit; + EndPlayingInternal(currentState); }); } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index ebb91e4dd2..fc62f16bba 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -24,14 +24,17 @@ namespace osu.Game.Online.Spectator [Key(2)] public IEnumerable Mods { get; set; } = Enumerable.Empty(); + [Key(3)] + public SpectatingUserState State { get; set; } + public bool Equals(SpectatorState other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID; + return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID && State == other.State; } - public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID} State:{State}"; } } diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 976f95cef8..277040b2a6 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -55,7 +55,9 @@ namespace osu.Game.Rulesets.UI protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - spectatorClient?.EndPlaying(); + + if (spectatorClient != null && gameplayState != null) + spectatorClient.EndPlaying(gameplayState); } protected override void Update() diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f6a2310826..1aa6cded48 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1000,7 +1000,7 @@ namespace osu.Game.Screens.Play // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // To resolve test failures, forcefully end playing synchronously when this screen exits. // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. - spectatorClient.EndPlaying(); + spectatorClient.EndPlaying(GameplayState); // GameplayClockContainer performs seeks / start / stop operations on the beatmap's track. // as we are no longer the current screen, we cannot guarantee the track is still usable. From f4210f7a302a3bad2130181800106cdcefeb2020 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Jan 2022 22:21:07 +0900 Subject: [PATCH 05/21] Rework spectator components to use new user state --- osu.Game/Online/Spectator/SpectatorClient.cs | 8 +--- .../Dashboard/CurrentlyPlayingDisplay.cs | 42 ++++++++++++------- osu.Game/Screens/Spectate/SpectatorScreen.cs | 33 ++++++++------- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index f4161e1db9..fb7526347b 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -93,12 +93,8 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - // UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched. - // This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29). - // We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations. if (watchingUsers.Contains(userId)) playingUserStates[userId] = state; - OnUserBeganPlaying?.Invoke(userId, state); }); @@ -109,8 +105,8 @@ namespace osu.Game.Online.Spectator { Schedule(() => { - playingUserStates.Remove(userId); - + if (watchingUsers.Contains(userId)) + playingUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); }); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index afbfb9f7ef..383f17d8d2 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private readonly IBindableDictionary playingUserStates = new BindableDictionary(); + private readonly IBindableDictionary userStates = new BindableDictionary(); private FillFlowContainer userFlow; @@ -51,33 +51,32 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - playingUserStates.BindTo(spectatorClient.PlayingUserStates); - playingUserStates.BindCollectionChanged(onUsersChanged, true); + userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindCollectionChanged(onUserStatesChanged, true); } - private void onUsersChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Replace: Debug.Assert(e.NewItems != null); - foreach ((int userId, _) in e.NewItems) + foreach ((int userId, SpectatorState state) in e.NewItems) { + if (state.State != SpectatingUserState.Playing) + { + removePlayingUser(userId); + continue; + } + users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); - if (user == null) return; - - Schedule(() => - { - // user may no longer be playing. - if (!playingUserStates.ContainsKey(user.Id)) - return; - - userFlow.Add(createUserPanel(user)); - }); + if (user != null) + Schedule(() => addPlayingUser(user)); }); } @@ -87,9 +86,20 @@ namespace osu.Game.Overlays.Dashboard Debug.Assert(e.OldItems != null); foreach ((int userId, _) in e.OldItems) - userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); + removePlayingUser(userId); break; } + + void addPlayingUser(APIUser user) + { + // user may no longer be playing. + if (!userStates.TryGetValue(user.Id, out var state2) || state2.State != SpectatingUserState.Playing) + return; + + userFlow.Add(createUserPanel(user)); + } + + void removePlayingUser(int userId) => userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); }); private PlayingUserPanel createUserPanel(APIUser user) => diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 3cf9f79611..783ba494eb 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Spectate [Resolved] private UserLookupCache userLookupCache { get; set; } - private readonly IBindableDictionary playingUserStates = new BindableDictionary(); + private readonly IBindableDictionary userStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); private readonly Dictionary gameplayStates = new Dictionary(); @@ -77,8 +77,8 @@ namespace osu.Game.Screens.Spectate userMap[u.Id] = u; } - playingUserStates.BindTo(spectatorClient.PlayingUserStates); - playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); + userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindCollectionChanged(onUserStatesChanged, true); realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); @@ -99,7 +99,7 @@ namespace osu.Game.Screens.Spectate { foreach ((int userId, _) in userMap) { - if (!playingUserStates.TryGetValue(userId, out var userState)) + if (!userStates.TryGetValue(userId, out var userState)) continue; if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID)) @@ -107,40 +107,41 @@ namespace osu.Game.Screens.Spectate } } - private void onPlayingUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) + private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { case NotifyDictionaryChangedAction.Add: foreach ((int userId, var state) in e.NewItems.AsNonNull()) - onUserStateAdded(userId, state); + onUserStateChanged(userId, state); break; case NotifyDictionaryChangedAction.Remove: - foreach ((int userId, var _) in e.OldItems.AsNonNull()) + foreach ((int userId, _) 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); + onUserStateChanged(userId, state); break; } } - private void onUserStateAdded(int userId, SpectatorState state) + private void onUserStateChanged(int userId, SpectatorState newState) { - if (state.RulesetID == null || state.BeatmapID == null) + if (newState.RulesetID == null || newState.BeatmapID == null) return; if (!userMap.ContainsKey(userId)) return; - Schedule(() => OnUserStateChanged(userId, state)); - updateGameplayState(userId); + // Do nothing for failed/completed states. + if (newState.State == SpectatingUserState.Playing) + { + Schedule(() => OnUserStateChanged(userId, newState)); + updateGameplayState(userId); + } } private void onUserStateRemoved(int userId) @@ -162,7 +163,7 @@ namespace osu.Game.Screens.Spectate Debug.Assert(userMap.ContainsKey(userId)); var user = userMap[userId]; - var spectatorState = playingUserStates[userId]; + var spectatorState = userStates[userId]; var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.OnlineID == spectatorState.RulesetID)?.CreateInstance(); if (resolvedRuleset == null) From 9d1d13c7152592d31a212fc04b04aac1fac171b1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:00:43 +0900 Subject: [PATCH 06/21] Fix up TestSpectatorClient implementation Rather than using a list which is supposed to be updated "client"-side, now uses the "server"-side list. --- .../Visual/Spectator/TestSpectatorClient.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 7848a825f4..6862cda88c 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -39,11 +39,7 @@ namespace osu.Game.Tests.Visual.Spectator public TestSpectatorClient() { - OnNewFrames += (i, bundle) => - { - if (PlayingUserStates.ContainsKey(i)) - lastReceivedUserFrames[i] = bundle.Frames[^1]; - }; + OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1]; } /// @@ -62,16 +58,20 @@ namespace osu.Game.Tests.Visual.Spectator /// Ends play for an arbitrary user. /// /// The user to end play for. - public void EndPlay(int userId) + /// The spectator state to end play with. + public void EndPlay(int userId, SpectatingUserState state = SpectatingUserState.Quit) { - if (!PlayingUserStates.ContainsKey(userId)) + if (!userBeatmapDictionary.ContainsKey(userId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, + State = state }); + + userBeatmapDictionary.Remove(userId); } public new void Schedule(Action action) => base.Schedule(action); @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Spectator protected override Task WatchUserInternal(int userId) { // When newly watching a user, the server sends the playing state immediately. - if (PlayingUserStates.ContainsKey(userId)) + if (userBeatmapDictionary.ContainsKey(userId)) sendPlayingState(userId); return Task.CompletedTask; @@ -144,6 +144,7 @@ namespace osu.Game.Tests.Visual.Spectator { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, + State = SpectatingUserState.Playing }); } } From 589f5e7a31f53275f7cd001adbe4deedb1e18473 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:09:38 +0900 Subject: [PATCH 07/21] Update test which has now been resolved --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index d28e5203e3..9f8470446c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -155,11 +155,12 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); checkPaused(true); + sendFrames(); - finish(); + finish(SpectatingUserState.Failed); - checkPaused(false); - // TODO: should replay until running out of frames then fail + checkPaused(false); // Should continue playing until out of frames + checkPaused(true); } [Test] @@ -246,7 +247,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); - private void finish() => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id)); + private void finish(SpectatingUserState state = SpectatingUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); From fcbba3d9481ca396acd9885593065ea782362475 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:11:29 +0900 Subject: [PATCH 08/21] Rename PlayingUserStates -> WatchingUserStates --- .../Visual/Gameplay/TestSceneSpectatorHost.cs | 4 ++-- .../TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 12 ++++++------ .../Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs index 409cec4cf6..6d6b0bf89e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClientSendsCorrectRuleset() { - AddUntilStep("spectator client sending frames", () => spectatorClient.PlayingUserStates.ContainsKey(dummy_user_id)); - AddAssert("spectator client sent correct ruleset", () => spectatorClient.PlayingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); + AddUntilStep("spectator client sending frames", () => spectatorClient.WatchingUserStates.ContainsKey(dummy_user_id)); + AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); } public override void TearDownSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 8b71dfac21..55450b36e2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RandomlyUpdateState() { - foreach ((int userId, _) in PlayingUserStates) + foreach ((int userId, _) in WatchingUserStates) { if (RNG.NextBool()) continue; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 156d544960..3646c51d94 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -37,8 +37,8 @@ namespace osu.Game.Online.Spectator private readonly List watchingUsers = new List(); - public IBindableDictionary PlayingUserStates => playingUserStates; - private readonly BindableDictionary playingUserStates = new BindableDictionary(); + public IBindableDictionary WatchingUserStates => watchingUserStates; + private readonly BindableDictionary watchingUserStates = new BindableDictionary(); private IBeatmap? currentBeatmap; private Score? currentScore; @@ -85,7 +85,7 @@ namespace osu.Game.Online.Spectator BeginPlayingInternal(currentState); } else - playingUserStates.Clear(); + watchingUserStates.Clear(); }), true); } @@ -94,7 +94,7 @@ namespace osu.Game.Online.Spectator Schedule(() => { if (watchingUsers.Contains(userId)) - playingUserStates[userId] = state; + watchingUserStates[userId] = state; OnUserBeganPlaying?.Invoke(userId, state); }); @@ -106,7 +106,7 @@ namespace osu.Game.Online.Spectator Schedule(() => { if (watchingUsers.Contains(userId)) - playingUserStates[userId] = state; + watchingUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); }); @@ -193,7 +193,7 @@ namespace osu.Game.Online.Spectator Schedule(() => { watchingUsers.Remove(userId); - playingUserStates.Remove(userId); + watchingUserStates.Remove(userId); StopWatchingUserInternal(userId); }); } diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 383f17d8d2..975402a443 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindTo(spectatorClient.WatchingUserStates); userStates.BindCollectionChanged(onUserStatesChanged, true); } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 783ba494eb..0b2a7cecf6 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Spectate userMap[u.Id] = u; } - userStates.BindTo(spectatorClient.PlayingUserStates); + userStates.BindTo(spectatorClient.WatchingUserStates); userStates.BindCollectionChanged(onUserStatesChanged, true); realmSubscription = realm.RegisterForNotifications( From 81a22dbd29543b3ff5387669bd6ce1fc048a4811 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Feb 2022 23:19:43 +0900 Subject: [PATCH 09/21] Add back playing users list --- osu.Game/Online/Spectator/SpectatorClient.cs | 28 +++++++++-- .../Dashboard/CurrentlyPlayingDisplay.cs | 48 ++++++++----------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 3646c51d94..9e168411b0 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -35,16 +35,28 @@ namespace osu.Game.Online.Spectator /// public abstract IBindable IsConnected { get; } + /// + /// The states of all users currently being watched. + /// + public IBindableDictionary WatchingUserStates => watchingUserStates; + + /// + /// A global list of all players currently playing. + /// + public IBindableList PlayingUsers => playingUsers; + + /// + /// All users currently being watched. + /// private readonly List watchingUsers = new List(); - public IBindableDictionary WatchingUserStates => watchingUserStates; private readonly BindableDictionary watchingUserStates = new BindableDictionary(); + private readonly BindableList playingUsers = new BindableList(); + private readonly SpectatorState currentState = new SpectatorState(); private IBeatmap? currentBeatmap; private Score? currentScore; - private readonly SpectatorState currentState = new SpectatorState(); - /// /// Whether the local user is playing. /// @@ -85,7 +97,10 @@ namespace osu.Game.Online.Spectator BeginPlayingInternal(currentState); } else + { watchingUserStates.Clear(); + playingUsers.Clear(); + } }), true); } @@ -93,8 +108,12 @@ namespace osu.Game.Online.Spectator { Schedule(() => { + if (!playingUsers.Contains(userId)) + playingUsers.Add(userId); + if (watchingUsers.Contains(userId)) watchingUserStates[userId] = state; + OnUserBeganPlaying?.Invoke(userId, state); }); @@ -105,8 +124,11 @@ namespace osu.Game.Online.Spectator { Schedule(() => { + playingUsers.Remove(userId); + if (watchingUsers.Contains(userId)) watchingUserStates[userId] = state; + OnUserFinishedPlaying?.Invoke(userId, state); }); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 975402a443..02ef28f825 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private readonly IBindableDictionary userStates = new BindableDictionary(); + private readonly IBindableList playingUsers = new BindableList(); private FillFlowContainer userFlow; @@ -51,55 +52,46 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - userStates.BindTo(spectatorClient.WatchingUserStates); - userStates.BindCollectionChanged(onUserStatesChanged, true); + playingUsers.BindTo(spectatorClient.PlayingUsers); + playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } - private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { switch (e.Action) { - case NotifyDictionaryChangedAction.Add: - case NotifyDictionaryChangedAction.Replace: + case NotifyCollectionChangedAction.Add: Debug.Assert(e.NewItems != null); - foreach ((int userId, SpectatorState state) in e.NewItems) + foreach (int userId in e.NewItems) { - if (state.State != SpectatingUserState.Playing) - { - removePlayingUser(userId); - continue; - } - users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); if (user != null) - Schedule(() => addPlayingUser(user)); + { + Schedule(() => + { + // user may no longer be playing. + if (!playingUsers.Contains(user.Id)) + return; + + userFlow.Add(createUserPanel(user)); + }); + } }); } break; - case NotifyDictionaryChangedAction.Remove: + case NotifyCollectionChangedAction.Remove: Debug.Assert(e.OldItems != null); - foreach ((int userId, _) in e.OldItems) - removePlayingUser(userId); + foreach (int userId in e.OldItems) + userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); break; } - - void addPlayingUser(APIUser user) - { - // user may no longer be playing. - if (!userStates.TryGetValue(user.Id, out var state2) || state2.State != SpectatingUserState.Playing) - return; - - userFlow.Add(createUserPanel(user)); - } - - void removePlayingUser(int userId) => userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); }); private PlayingUserPanel createUserPanel(APIUser user) => From 62537eb4aa5f458fd959846a8bf318b16e2acef0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 12:44:33 +0900 Subject: [PATCH 10/21] Fix spectator not completing --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 21 +++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 0b2a7cecf6..460352ee07 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -112,6 +112,7 @@ namespace osu.Game.Screens.Spectate switch (e.Action) { case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Replace: foreach ((int userId, var state) in e.NewItems.AsNonNull()) onUserStateChanged(userId, state); break; @@ -120,11 +121,6 @@ namespace osu.Game.Screens.Spectate foreach ((int userId, _) in e.OldItems.AsNonNull()) onUserStateRemoved(userId); break; - - case NotifyDictionaryChangedAction.Replace: - foreach ((int userId, var state) in e.NewItems.AsNonNull()) - onUserStateChanged(userId, state); - break; } } @@ -136,11 +132,18 @@ namespace osu.Game.Screens.Spectate if (!userMap.ContainsKey(userId)) return; - // Do nothing for failed/completed states. - if (newState.State == SpectatingUserState.Playing) + switch (newState.State) { - Schedule(() => OnUserStateChanged(userId, newState)); - updateGameplayState(userId); + case SpectatingUserState.Completed: + // Make sure that gameplay completes to the end. + if (gameplayStates.TryGetValue(userId, out var gameplayState)) + gameplayState.Score.Replay.HasReceivedAllFrames = true; + break; + + case SpectatingUserState.Playing: + Schedule(() => OnUserStateChanged(userId, newState)); + updateGameplayState(userId); + break; } } From f2850601486ec6a8b4b4a88d76b13c75fddd0320 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 3 Feb 2022 21:50:15 +0900 Subject: [PATCH 11/21] Fix MultiSpectatorScreen not continuing to results --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 5 ++++- osu.Game/Screens/Play/SoloSpectator.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 16 ++++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 4646f42d63..bbdd7a3d56 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -215,8 +215,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score); - protected override void EndGameplay(int userId) + protected override void EndGameplay(int userId, SpectatorState state) { + if (state.State == SpectatingUserState.Completed || state.State == SpectatingUserState.Failed) + return; + RemoveUser(userId); var instance = instances.Single(i => i.UserId == userId); diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index b530965269..a710db6d24 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -180,7 +180,7 @@ namespace osu.Game.Screens.Play scheduleStart(spectatorGameplayState); } - protected override void EndGameplay(int userId) + protected override void EndGameplay(int userId, SpectatorState state) { scheduledStart?.Cancel(); immediateSpectatorGameplayState = null; diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 460352ee07..7b58f669a0 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -118,8 +118,8 @@ namespace osu.Game.Screens.Spectate break; case NotifyDictionaryChangedAction.Remove: - foreach ((int userId, _) in e.OldItems.AsNonNull()) - onUserStateRemoved(userId); + foreach ((int userId, SpectatorState state) in e.OldItems.AsNonNull()) + onUserStateRemoved(userId, state); break; } } @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Spectate } } - private void onUserStateRemoved(int userId) + private void onUserStateRemoved(int userId, SpectatorState state) { if (!userMap.ContainsKey(userId)) return; @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Spectate gameplayState.Score.Replay.HasReceivedAllFrames = true; gameplayStates.Remove(userId); - Schedule(() => EndGameplay(userId)); + Schedule(() => EndGameplay(userId, state)); } private void updateGameplayState(int userId) @@ -212,7 +212,8 @@ namespace osu.Game.Screens.Spectate /// Ends gameplay for a user. /// /// The user to end gameplay for. - protected abstract void EndGameplay(int userId); + /// The final user state. + protected abstract void EndGameplay(int userId, SpectatorState state); /// /// Stops spectating a user. @@ -220,7 +221,10 @@ namespace osu.Game.Screens.Spectate /// The user to stop spectating. protected void RemoveUser(int userId) { - onUserStateRemoved(userId); + if (!userStates.TryGetValue(userId, out var state)) + return; + + onUserStateRemoved(userId, state); users.Remove(userId); userMap.Remove(userId); From 0d99017178af1b373212f4174242e7bf8ee224a3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 20:27:08 +0900 Subject: [PATCH 12/21] Add state tests --- .../Visual/Gameplay/TestSceneSpectator.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 9f8470446c..65583919d6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -235,6 +235,71 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("last frame has header", () => lastBundle.Frames[^1].Header != null); } + [Test] + public void TestPlayingState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + + [Test] + public void TestCompletionState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send completion", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Completed)); + AddUntilStep("state is completed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Completed); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + + [Test] + public void TestQuitState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Quit)); + AddUntilStep("state is quit", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Quit); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + + [Test] + public void TestFailedState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Failed)); + AddUntilStep("state is failed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Failed); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + } + private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; From 4c76027178933d9bcade478495f0588746220a18 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 20:29:49 +0900 Subject: [PATCH 13/21] Rename completed state to passed --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 6 +++--- osu.Game/Online/Spectator/SpectatingUserState.cs | 10 +++++----- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 65583919d6..a263bb1e6f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -247,7 +247,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestCompletionState() + public void TestPassedState() { loadSpectatingScreen(); @@ -255,8 +255,8 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send completion", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Completed)); - AddUntilStep("state is completed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Completed); + AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Passed)); + AddUntilStep("state is passed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Passed); start(); sendFrames(); diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatingUserState.cs index c7ba4ba248..30be57b739 100644 --- a/osu.Game/Online/Spectator/SpectatingUserState.cs +++ b/osu.Game/Online/Spectator/SpectatingUserState.cs @@ -6,7 +6,7 @@ namespace osu.Game.Online.Spectator public enum SpectatingUserState { /// - /// The spectated user has not yet played. + /// The spectated user is not yet playing. /// Idle, @@ -16,17 +16,17 @@ namespace osu.Game.Online.Spectator Playing, /// - /// The spectated user has successfully completed gameplay. + /// The spectated user has passed gameplay. /// - Completed, + Passed, /// - /// The spectator user has failed during gameplay. + /// The spectator user has failed gameplay. /// Failed, /// - /// The spectated user has quit during gameplay. + /// The spectated user has quit gameplay. /// Quit } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 9e168411b0..ee5fdc917a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -186,7 +186,7 @@ namespace osu.Game.Online.Spectator currentBeatmap = null; if (state.HasPassed) - currentState.State = SpectatingUserState.Completed; + currentState.State = SpectatingUserState.Passed; else if (state.HasFailed) currentState.State = SpectatingUserState.Failed; else diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 125e4b261c..60bf87ff61 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -216,7 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void EndGameplay(int userId, SpectatorState state) { - if (state.State == SpectatingUserState.Completed || state.State == SpectatingUserState.Failed) + if (state.State == SpectatingUserState.Passed || state.State == SpectatingUserState.Failed) return; RemoveUser(userId); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 7b58f669a0..394afb3a4c 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -134,7 +134,7 @@ namespace osu.Game.Screens.Spectate switch (newState.State) { - case SpectatingUserState.Completed: + case SpectatingUserState.Passed: // Make sure that gameplay completes to the end. if (gameplayStates.TryGetValue(userId, out var gameplayState)) gameplayState.Score.Replay.HasReceivedAllFrames = true; From c1766d8a411f1626255c1d74fbf0b634e7bc4610 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 20:29:53 +0900 Subject: [PATCH 14/21] Add paused state --- osu.Game/Online/Spectator/SpectatingUserState.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatingUserState.cs index 30be57b739..e00c2e1947 100644 --- a/osu.Game/Online/Spectator/SpectatingUserState.cs +++ b/osu.Game/Online/Spectator/SpectatingUserState.cs @@ -15,6 +15,11 @@ namespace osu.Game.Online.Spectator /// Playing, + /// + /// The spectated user is currently paused. Unused for the time being. + /// + Paused, + /// /// The spectated user has passed gameplay. /// From 886d1d2df637dcbac1e3b7147b3062ba576ce178 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Feb 2022 21:20:33 +0900 Subject: [PATCH 15/21] Refactorings --- .../Visual/Gameplay/TestSceneSpectator.cs | 3 ++- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- .../Dashboard/CurrentlyPlayingDisplay.cs | 20 +++++++++---------- .../Spectate/MultiSpectatorScreen.cs | 2 ++ osu.Game/Screens/Play/GameplayState.cs | 2 +- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index a263bb1e6f..e0946df2dc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -160,7 +160,8 @@ namespace osu.Game.Tests.Visual.Gameplay finish(SpectatingUserState.Failed); checkPaused(false); // Should continue playing until out of frames - checkPaused(true); + checkPaused(true); // And eventually stop after running out of frames and fail. + // Todo: Should check for + display a failed message. } [Test] diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index ee5fdc917a..ed6e355ddf 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -98,8 +98,8 @@ namespace osu.Game.Online.Spectator } else { - watchingUserStates.Clear(); playingUsers.Clear(); + watchingUserStates.Clear(); } }), true); } diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 02ef28f825..117de88166 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -69,17 +69,17 @@ namespace osu.Game.Overlays.Dashboard { var user = task.GetResultSafely(); - if (user != null) - { - Schedule(() => - { - // user may no longer be playing. - if (!playingUsers.Contains(user.Id)) - return; + if (user == null) + return; - userFlow.Add(createUserPanel(user)); - }); - } + Schedule(() => + { + // user may no longer be playing. + if (!playingUsers.Contains(user.Id)) + return; + + userFlow.Add(createUserPanel(user)); + }); }); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 60bf87ff61..55df8301da 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -216,6 +216,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void EndGameplay(int userId, SpectatorState state) { + // Allowed passed/failed users to complete their remaining replay frames. + // The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used. if (state.State == SpectatingUserState.Passed || state.State == SpectatingUserState.Failed) return; diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 64e873b3bb..c6a072da74 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play public bool HasFailed { get; set; } /// - /// Whether the user quit gameplay without either having either passed or failed. + /// Whether the user quit gameplay without having either passed or failed. /// public bool HasQuit { get; set; } From 4966c4e974c9a3e7b98910adeee5b658d6889e22 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Feb 2022 11:51:47 +0900 Subject: [PATCH 16/21] Remove redundant parameter --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index e0946df2dc..7235bce789 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -274,7 +274,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Quit)); + AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id)); AddUntilStep("state is quit", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Quit); start(); From ffc4c64f7e7fb0951c9c3f3109497b37f50196b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Feb 2022 12:09:04 +0900 Subject: [PATCH 17/21] Unify namings across the board --- .../Visual/Gameplay/TestSceneSpectator.cs | 22 ++++++------ .../Visual/Gameplay/TestSceneSpectatorHost.cs | 4 +-- ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- ...tingUserState.cs => SpectatedUserState.cs} | 4 +-- osu.Game/Online/Spectator/SpectatorClient.cs | 36 +++++++++---------- osu.Game/Online/Spectator/SpectatorState.cs | 2 +- .../Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 6 ++-- .../Visual/Spectator/TestSpectatorClient.cs | 4 +-- 9 files changed, 41 insertions(+), 41 deletions(-) rename osu.Game/Online/Spectator/{SpectatingUserState.cs => SpectatedUserState.cs} (90%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 7235bce789..157c248d69 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Gameplay checkPaused(true); sendFrames(); - finish(SpectatingUserState.Failed); + finish(SpectatedUserState.Failed); checkPaused(false); // Should continue playing until out of frames checkPaused(true); // And eventually stop after running out of frames and fail. @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] @@ -256,13 +256,13 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Passed)); - AddUntilStep("state is passed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Passed); + AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Passed)); + AddUntilStep("state is passed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Passed); start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] @@ -275,12 +275,12 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id)); - AddUntilStep("state is quit", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Quit); + AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit); start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] @@ -292,13 +292,13 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatingUserState.Failed)); - AddUntilStep("state is failed", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Failed); + AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Failed)); + AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); start(); sendFrames(); waitForPlayer(); - AddUntilStep("state is playing", () => spectatorClient.WatchingUserStates[streamingUser.Id].State == SpectatingUserState.Playing); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } private OsuFramedReplayInputHandler replayHandler => @@ -313,7 +313,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); - private void finish(SpectatingUserState state = SpectatingUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); + private void finish(SpectatedUserState state = SpectatedUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs index 6d6b0bf89e..034519fbf8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClientSendsCorrectRuleset() { - AddUntilStep("spectator client sending frames", () => spectatorClient.WatchingUserStates.ContainsKey(dummy_user_id)); - AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); + AddUntilStep("spectator client sending frames", () => spectatorClient.WatchedUserStates.ContainsKey(dummy_user_id)); + AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchedUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); } public override void TearDownSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 55450b36e2..1322fbc96e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RandomlyUpdateState() { - foreach ((int userId, _) in WatchingUserStates) + foreach ((int userId, _) in WatchedUserStates) { if (RNG.NextBool()) continue; diff --git a/osu.Game/Online/Spectator/SpectatingUserState.cs b/osu.Game/Online/Spectator/SpectatedUserState.cs similarity index 90% rename from osu.Game/Online/Spectator/SpectatingUserState.cs rename to osu.Game/Online/Spectator/SpectatedUserState.cs index e00c2e1947..0f0a3068b8 100644 --- a/osu.Game/Online/Spectator/SpectatingUserState.cs +++ b/osu.Game/Online/Spectator/SpectatedUserState.cs @@ -3,7 +3,7 @@ namespace osu.Game.Online.Spectator { - public enum SpectatingUserState + public enum SpectatedUserState { /// /// The spectated user is not yet playing. @@ -26,7 +26,7 @@ namespace osu.Game.Online.Spectator Passed, /// - /// The spectator user has failed gameplay. + /// The spectated user has failed gameplay. /// Failed, diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index ed6e355ddf..a54ea0d9ee 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -38,7 +38,7 @@ namespace osu.Game.Online.Spectator /// /// The states of all users currently being watched. /// - public IBindableDictionary WatchingUserStates => watchingUserStates; + public IBindableDictionary WatchedUserStates => watchedUserStates; /// /// A global list of all players currently playing. @@ -48,9 +48,9 @@ namespace osu.Game.Online.Spectator /// /// All users currently being watched. /// - private readonly List watchingUsers = new List(); + private readonly List watchedUsers = new List(); - private readonly BindableDictionary watchingUserStates = new BindableDictionary(); + private readonly BindableDictionary watchedUserStates = new BindableDictionary(); private readonly BindableList playingUsers = new BindableList(); private readonly SpectatorState currentState = new SpectatorState(); @@ -85,8 +85,8 @@ namespace osu.Game.Online.Spectator if (connected.NewValue) { // get all the users that were previously being watched - int[] users = watchingUsers.ToArray(); - watchingUsers.Clear(); + int[] users = watchedUsers.ToArray(); + watchedUsers.Clear(); // resubscribe to watched users. foreach (int userId in users) @@ -99,7 +99,7 @@ namespace osu.Game.Online.Spectator else { playingUsers.Clear(); - watchingUserStates.Clear(); + watchedUserStates.Clear(); } }), true); } @@ -111,8 +111,8 @@ namespace osu.Game.Online.Spectator if (!playingUsers.Contains(userId)) playingUsers.Add(userId); - if (watchingUsers.Contains(userId)) - watchingUserStates[userId] = state; + if (watchedUsers.Contains(userId)) + watchedUserStates[userId] = state; OnUserBeganPlaying?.Invoke(userId, state); }); @@ -126,8 +126,8 @@ namespace osu.Game.Online.Spectator { playingUsers.Remove(userId); - if (watchingUsers.Contains(userId)) - watchingUserStates[userId] = state; + if (watchedUsers.Contains(userId)) + watchedUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); }); @@ -159,7 +159,7 @@ namespace osu.Game.Online.Spectator currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); - currentState.State = SpectatingUserState.Playing; + currentState.State = SpectatedUserState.Playing; currentBeatmap = state.Beatmap; currentScore = score; @@ -186,11 +186,11 @@ namespace osu.Game.Online.Spectator currentBeatmap = null; if (state.HasPassed) - currentState.State = SpectatingUserState.Passed; + currentState.State = SpectatedUserState.Passed; else if (state.HasFailed) - currentState.State = SpectatingUserState.Failed; + currentState.State = SpectatedUserState.Failed; else - currentState.State = SpectatingUserState.Quit; + currentState.State = SpectatedUserState.Quit; EndPlayingInternal(currentState); }); @@ -200,10 +200,10 @@ namespace osu.Game.Online.Spectator { Debug.Assert(ThreadSafety.IsUpdateThread); - if (watchingUsers.Contains(userId)) + if (watchedUsers.Contains(userId)) return; - watchingUsers.Add(userId); + watchedUsers.Add(userId); WatchUserInternal(userId); } @@ -214,8 +214,8 @@ namespace osu.Game.Online.Spectator // Todo: This should not be a thing, but requires framework changes. Schedule(() => { - watchingUsers.Remove(userId); - watchingUserStates.Remove(userId); + watchedUsers.Remove(userId); + watchedUserStates.Remove(userId); StopWatchingUserInternal(userId); }); } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index fc62f16bba..77686d12da 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -25,7 +25,7 @@ namespace osu.Game.Online.Spectator public IEnumerable Mods { get; set; } = Enumerable.Empty(); [Key(3)] - public SpectatingUserState State { get; set; } + public SpectatedUserState State { get; set; } public bool Equals(SpectatorState other) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 55df8301da..571b6b4324 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -218,7 +218,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { // Allowed passed/failed users to complete their remaining replay frames. // The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used. - if (state.State == SpectatingUserState.Passed || state.State == SpectatingUserState.Failed) + if (state.State == SpectatedUserState.Passed || state.State == SpectatedUserState.Failed) return; RemoveUser(userId); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 394afb3a4c..9d0f8abddc 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Spectate userMap[u.Id] = u; } - userStates.BindTo(spectatorClient.WatchingUserStates); + userStates.BindTo(spectatorClient.WatchedUserStates); userStates.BindCollectionChanged(onUserStatesChanged, true); realmSubscription = realm.RegisterForNotifications( @@ -134,13 +134,13 @@ namespace osu.Game.Screens.Spectate switch (newState.State) { - case SpectatingUserState.Passed: + case SpectatedUserState.Passed: // Make sure that gameplay completes to the end. if (gameplayStates.TryGetValue(userId, out var gameplayState)) gameplayState.Score.Replay.HasReceivedAllFrames = true; break; - case SpectatingUserState.Playing: + case SpectatedUserState.Playing: Schedule(() => OnUserStateChanged(userId, newState)); updateGameplayState(userId); break; diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 6862cda88c..1322a99ea7 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Spectator /// /// The user to end play for. /// The spectator state to end play with. - public void EndPlay(int userId, SpectatingUserState state = SpectatingUserState.Quit) + public void EndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) { if (!userBeatmapDictionary.ContainsKey(userId)) return; @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.Spectator { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, - State = SpectatingUserState.Playing + State = SpectatedUserState.Playing }); } } From 18251c9285a559ac3f4070cdc8538211175e18ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Feb 2022 12:20:07 +0900 Subject: [PATCH 18/21] Clean up SpectatorScreen based on suggestions --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- osu.Game/Screens/Play/SoloSpectator.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 571b6b4324..e5eeeb3448 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -207,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) { } diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index a710db6d24..a0b07fcbd9 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -166,7 +166,7 @@ namespace osu.Game.Screens.Play automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) { clearDisplay(); showBeatmapPanel(spectatorState); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 9d0f8abddc..9eb374f0f7 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Spectate continue; if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID)) - updateGameplayState(userId); + startGameplay(userId); } } @@ -141,8 +141,8 @@ namespace osu.Game.Screens.Spectate break; case SpectatedUserState.Playing: - Schedule(() => OnUserStateChanged(userId, newState)); - updateGameplayState(userId); + Schedule(() => OnNewPlayingUserState(userId, newState)); + startGameplay(userId); break; } } @@ -161,7 +161,7 @@ namespace osu.Game.Screens.Spectate Schedule(() => EndGameplay(userId, state)); } - private void updateGameplayState(int userId) + private void startGameplay(int userId) { Debug.Assert(userMap.ContainsKey(userId)); @@ -195,11 +195,11 @@ namespace osu.Game.Screens.Spectate } /// - /// Invoked when a spectated user's state has changed. + /// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing. /// /// The user whose state has changed. /// The new state. - protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState); + protected abstract void OnNewPlayingUserState(int userId, [NotNull] SpectatorState spectatorState); /// /// Starts gameplay for a user. From a3896a8ebdc87b95c9d97a1f5112c65bafe761b9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Feb 2022 14:18:29 +0900 Subject: [PATCH 19/21] Remove allowance of null dependency --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 277040b2a6..0a94b55221 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI public int RecordFrameRate = 60; - [Resolved(canBeNull: true)] + [Resolved] private SpectatorClient spectatorClient { get; set; } [Resolved] @@ -48,8 +48,7 @@ namespace osu.Game.Rulesets.UI base.LoadComplete(); inputManager = GetContainingInputManager(); - - spectatorClient?.BeginPlaying(gameplayState, target); + spectatorClient.BeginPlaying(gameplayState, target); } protected override void Dispose(bool isDisposing) From f7fb7825cc322c3ac08a0c310928591ed96e3142 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Feb 2022 14:21:33 +0900 Subject: [PATCH 20/21] Simplify disposal --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 0a94b55221..6843beef7b 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -54,9 +55,7 @@ namespace osu.Game.Rulesets.UI protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - - if (spectatorClient != null && gameplayState != null) - spectatorClient.EndPlaying(gameplayState); + spectatorClient?.EndPlaying(gameplayState); } protected override void Update() From ebd105422fd6c2903bced01cb820d7e0d542789a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Feb 2022 14:22:08 +0900 Subject: [PATCH 21/21] Remove unused using --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 6843beef7b..dcd8f12028 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics;