2020-10-26 18:47:39 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2022-06-17 15:37:17 +08:00
#nullable disable
2020-10-26 20:22:01 +08:00
using System.Linq ;
2020-10-26 18:47:39 +08:00
using NUnit.Framework ;
2020-10-26 20:17:12 +08:00
using osu.Framework.Allocation ;
2022-01-03 16:31:12 +08:00
using osu.Framework.Extensions ;
2020-10-28 15:29:06 +08:00
using osu.Framework.Graphics ;
2020-10-26 20:45:37 +08:00
using osu.Framework.Screens ;
using osu.Framework.Testing ;
2020-10-26 20:22:01 +08:00
using osu.Game.Beatmaps ;
2021-04-01 22:48:26 +08:00
using osu.Game.Database ;
2021-11-04 17:02:44 +08:00
using osu.Game.Online.API.Requests.Responses ;
2020-10-26 20:17:12 +08:00
using osu.Game.Online.Spectator ;
2020-10-27 13:47:15 +08:00
using osu.Game.Rulesets.Osu ;
using osu.Game.Rulesets.Osu.Replays ;
2020-10-27 15:28:11 +08:00
using osu.Game.Rulesets.UI ;
2022-01-28 21:26:05 +08:00
using osu.Game.Scoring ;
2021-08-03 17:28:08 +08:00
using osu.Game.Screens ;
2020-10-26 18:47:39 +08:00
using osu.Game.Screens.Play ;
2020-10-26 20:45:37 +08:00
using osu.Game.Tests.Beatmaps.IO ;
2022-05-28 20:55:57 +08:00
using osu.Game.Tests.Gameplay ;
2021-04-26 16:22:16 +08:00
using osu.Game.Tests.Visual.Multiplayer ;
2021-05-12 11:17:42 +08:00
using osu.Game.Tests.Visual.Spectator ;
2022-01-28 21:26:05 +08:00
using osuTK ;
2020-10-26 18:47:39 +08:00
namespace osu.Game.Tests.Visual.Gameplay
{
2022-11-24 13:32:20 +08:00
public partial class TestSceneSpectator : ScreenTestScene
2020-10-26 18:47:39 +08:00
{
2021-11-04 17:02:44 +08:00
private readonly APIUser streamingUser = new APIUser { Id = MultiplayerTestScene . PLAYER_1_ID , Username = "Test user" } ;
2021-05-12 11:17:42 +08:00
2021-04-01 22:48:26 +08:00
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache ( ) ;
2020-10-27 18:23:35 +08:00
// used just to show beatmap card for the time being.
protected override bool UseOnlineAPI = > true ;
2020-10-26 20:22:01 +08:00
[Resolved]
private OsuGameBase game { get ; set ; }
2022-02-16 08:43:28 +08:00
private TestSpectatorClient spectatorClient = > dependenciesScreen . SpectatorClient ;
2022-02-15 20:08:27 +08:00
private DependenciesScreen dependenciesScreen ;
2021-08-03 17:28:08 +08:00
private SoloSpectator spectatorScreen ;
2020-10-28 21:50:57 +08:00
2021-08-03 17:28:08 +08:00
private BeatmapSetInfo importedBeatmap ;
2020-10-28 21:50:57 +08:00
private int importedBeatmapId ;
2021-08-03 17:28:08 +08:00
[SetUpSteps]
public void SetupSteps ( )
2020-10-26 20:45:37 +08:00
{
2021-08-03 17:28:08 +08:00
AddStep ( "load dependencies" , ( ) = >
2020-10-28 21:50:57 +08:00
{
2022-02-15 20:08:27 +08:00
LoadScreen ( dependenciesScreen = new DependenciesScreen ( ) ) ;
// The dependencies screen gets suspended so it stops receiving updates. So its children are manually added to the test scene instead.
Children = new Drawable [ ]
{
dependenciesScreen . UserLookupCache ,
2022-02-16 08:43:28 +08:00
dependenciesScreen . SpectatorClient ,
2022-02-15 20:08:27 +08:00
} ;
2020-10-28 21:50:57 +08:00
} ) ;
2020-10-26 20:45:37 +08:00
2021-08-03 17:28:08 +08:00
AddUntilStep ( "wait for dependencies to load" , ( ) = > dependenciesScreen . IsLoaded ) ;
AddStep ( "import beatmap" , ( ) = >
2020-10-26 20:45:37 +08:00
{
2021-12-17 17:26:12 +08:00
importedBeatmap = BeatmapImportHelper . LoadOszIntoOsu ( game , virtualTrack : true ) . GetResultSafely ( ) ;
2022-01-27 14:19:48 +08:00
importedBeatmapId = importedBeatmap . Beatmaps . First ( b = > b . Ruleset . OnlineID = = 0 ) . OnlineID ;
2020-10-26 20:45:37 +08:00
} ) ;
}
2022-03-17 18:23:49 +08:00
[Test]
public void TestSeekToGameplayStartFramesArriveAfterPlayerLoad ( )
{
const double gameplay_start = 10000 ;
loadSpectatingScreen ( ) ;
start ( ) ;
waitForPlayer ( ) ;
sendFrames ( startTime : gameplay_start ) ;
2022-08-19 00:19:24 +08:00
AddAssert ( "time is greater than seek target" , ( ) = > currentFrameStableTime , ( ) = > Is . GreaterThan ( gameplay_start ) ) ;
2022-03-17 18:23:49 +08:00
}
/// <summary>
/// Tests the same as <see cref="TestSeekToGameplayStartFramesArriveAfterPlayerLoad"/> but with the frames arriving just as <see cref="Player"/> is transitioning into existence.
/// </summary>
[Test]
public void TestSeekToGameplayStartFramesArriveAsPlayerLoaded ( )
{
const double gameplay_start = 10000 ;
loadSpectatingScreen ( ) ;
start ( ) ;
AddUntilStep ( "wait for player loader" , ( ) = > ( Stack . CurrentScreen as PlayerLoader ) ? . IsLoaded = = true ) ;
AddUntilStep ( "queue send frames on player load" , ( ) = >
{
var loadingPlayer = ( Stack . CurrentScreen as PlayerLoader ) ? . CurrentPlayer ;
if ( loadingPlayer = = null )
return false ;
loadingPlayer . OnLoadComplete + = _ = >
{
spectatorClient . SendFramesFromUser ( streamingUser . Id , 10 , gameplay_start ) ;
} ;
return true ;
} ) ;
waitForPlayer ( ) ;
AddUntilStep ( "state is playing" , ( ) = > spectatorClient . WatchedUserStates [ streamingUser . Id ] . State = = SpectatedUserState . Playing ) ;
2022-08-19 00:19:24 +08:00
AddAssert ( "time is greater than seek target" , ( ) = > currentFrameStableTime , ( ) = > Is . GreaterThan ( gameplay_start ) ) ;
2022-03-17 18:23:49 +08:00
}
2020-10-26 18:47:39 +08:00
[Test]
2020-10-29 14:10:11 +08:00
public void TestFrameStarvationAndResume ( )
2020-10-26 18:47:39 +08:00
{
2020-10-27 16:10:48 +08:00
loadSpectatingScreen ( ) ;
2020-10-27 17:28:49 +08:00
2021-04-01 21:08:52 +08:00
AddAssert ( "screen hasn't changed" , ( ) = > Stack . CurrentScreen is SoloSpectator ) ;
2020-10-27 17:28:49 +08:00
2020-10-27 17:32:05 +08:00
start ( ) ;
waitForPlayer ( ) ;
2021-06-03 16:27:24 +08:00
sendFrames ( ) ;
2020-10-27 13:47:15 +08:00
AddAssert ( "ensure frames arrived" , ( ) = > replayHandler . HasFrames ) ;
2020-10-27 13:57:23 +08:00
2021-04-12 17:50:25 +08:00
AddUntilStep ( "wait for frame starvation" , ( ) = > replayHandler . WaitingForFrame ) ;
2020-10-27 17:32:05 +08:00
checkPaused ( true ) ;
2020-10-29 13:57:36 +08:00
double? pausedTime = null ;
AddStep ( "store time" , ( ) = > pausedTime = currentFrameStableTime ) ;
2020-10-27 17:13:58 +08:00
sendFrames ( ) ;
2020-10-27 15:28:11 +08:00
2021-04-12 17:50:25 +08:00
AddUntilStep ( "wait for frame starvation" , ( ) = > replayHandler . WaitingForFrame ) ;
2020-10-27 17:32:05 +08:00
checkPaused ( true ) ;
2020-10-29 13:57:36 +08:00
2022-08-19 00:19:24 +08:00
AddAssert ( "time advanced" , ( ) = > currentFrameStableTime , ( ) = > Is . GreaterThan ( pausedTime ) ) ;
2020-10-26 20:17:12 +08:00
}
2020-10-27 17:28:49 +08:00
[Test]
public void TestPlayStartsWithNoFrames ( )
{
loadSpectatingScreen ( ) ;
2020-10-27 17:32:05 +08:00
start ( ) ;
waitForPlayer ( ) ;
2020-10-28 21:59:54 +08:00
checkPaused ( true ) ;
2020-10-27 17:28:49 +08:00
2021-06-10 21:41:38 +08:00
// send enough frames to ensure play won't be paused
sendFrames ( 100 ) ;
2020-10-27 17:28:49 +08:00
2020-10-27 17:32:05 +08:00
checkPaused ( false ) ;
2020-10-27 17:28:49 +08:00
}
2020-10-26 20:27:05 +08:00
[Test]
public void TestSpectatingDuringGameplay ( )
{
2020-10-27 17:32:05 +08:00
start ( ) ;
2021-06-10 21:41:38 +08:00
sendFrames ( 300 ) ;
2020-10-27 17:56:28 +08:00
2020-10-27 16:10:48 +08:00
loadSpectatingScreen ( ) ;
2021-06-03 16:27:24 +08:00
waitForPlayer ( ) ;
2020-10-27 17:56:28 +08:00
2021-06-10 21:41:38 +08:00
sendFrames ( 300 ) ;
2020-10-27 17:56:28 +08:00
2022-08-19 00:19:24 +08:00
AddUntilStep ( "playing from correct point in time" , ( ) = > player . ChildrenOfType < DrawableRuleset > ( ) . First ( ) . FrameStableClock . CurrentTime , ( ) = > Is . GreaterThan ( 30000 ) ) ;
2020-10-26 20:27:05 +08:00
}
[Test]
2020-10-29 14:08:06 +08:00
public void TestHostRetriesWhileWatching ( )
2020-10-26 20:27:05 +08:00
{
2020-10-27 16:10:48 +08:00
loadSpectatingScreen ( ) ;
2020-10-26 20:45:37 +08:00
2020-10-27 17:32:05 +08:00
start ( ) ;
2020-10-27 15:28:11 +08:00
sendFrames ( ) ;
2020-10-28 21:50:57 +08:00
2020-10-29 14:03:38 +08:00
waitForPlayer ( ) ;
Player lastPlayer = null ;
AddStep ( "store first player" , ( ) = > lastPlayer = player ) ;
2020-10-27 17:32:05 +08:00
start ( ) ;
2020-10-27 15:28:11 +08:00
sendFrames ( ) ;
2020-10-29 14:03:38 +08:00
waitForPlayer ( ) ;
AddAssert ( "player is different" , ( ) = > lastPlayer ! = player ) ;
2020-10-26 20:27:05 +08:00
}
[Test]
public void TestHostFails ( )
{
2020-10-27 16:10:48 +08:00
loadSpectatingScreen ( ) ;
2020-10-26 20:45:37 +08:00
2020-10-27 17:32:05 +08:00
start ( ) ;
2020-10-26 20:45:37 +08:00
2020-10-28 21:59:54 +08:00
waitForPlayer ( ) ;
checkPaused ( true ) ;
2022-02-02 22:09:38 +08:00
sendFrames ( ) ;
2020-10-28 21:59:54 +08:00
2022-02-09 11:09:04 +08:00
finish ( SpectatedUserState . Failed ) ;
2020-10-28 21:59:54 +08:00
2022-02-02 22:09:38 +08:00
checkPaused ( false ) ; // Should continue playing until out of frames
2022-02-08 20:20:33 +08:00
checkPaused ( true ) ; // And eventually stop after running out of frames and fail.
// Todo: Should check for + display a failed message.
2020-10-26 20:27:05 +08:00
}
[Test]
public void TestStopWatchingDuringPlay ( )
{
2020-10-27 16:10:48 +08:00
loadSpectatingScreen ( ) ;
2020-10-26 20:45:37 +08:00
2020-10-27 17:32:05 +08:00
start ( ) ;
2020-10-27 15:28:11 +08:00
sendFrames ( ) ;
2020-10-27 17:32:05 +08:00
waitForPlayer ( ) ;
2020-10-27 18:23:35 +08:00
2020-10-26 20:45:37 +08:00
AddStep ( "stop spectating" , ( ) = > ( Stack . CurrentScreen as Player ) ? . Exit ( ) ) ;
2020-10-29 14:08:06 +08:00
AddUntilStep ( "spectating stopped" , ( ) = > spectatorScreen . GetChildScreen ( ) = = null ) ;
2020-10-26 20:45:37 +08:00
}
2020-10-29 14:09:12 +08:00
[Test]
public void TestStopWatchingThenHostRetries ( )
{
loadSpectatingScreen ( ) ;
start ( ) ;
sendFrames ( ) ;
waitForPlayer ( ) ;
AddStep ( "stop spectating" , ( ) = > ( Stack . CurrentScreen as Player ) ? . Exit ( ) ) ;
AddUntilStep ( "spectating stopped" , ( ) = > spectatorScreen . GetChildScreen ( ) = = null ) ;
// host starts playing a new session
start ( ) ;
waitForPlayer ( ) ;
}
2020-10-26 20:45:37 +08:00
[Test]
public void TestWatchingBeatmapThatDoesntExistLocally ( )
{
2020-10-27 16:10:48 +08:00
loadSpectatingScreen ( ) ;
2020-10-26 20:45:37 +08:00
2020-10-28 21:51:35 +08:00
start ( - 1234 ) ;
2020-10-27 15:28:11 +08:00
sendFrames ( ) ;
2020-10-27 18:23:35 +08:00
2021-04-01 21:08:52 +08:00
AddAssert ( "screen didn't change" , ( ) = > Stack . CurrentScreen is SoloSpectator ) ;
2020-10-26 20:27:05 +08:00
}
2022-01-28 21:26:05 +08:00
[Test]
public void TestFinalFramesPurgedBeforeEndingPlay ( )
{
2022-05-28 20:55:57 +08:00
AddStep ( "begin playing" , ( ) = > spectatorClient . BeginPlaying ( TestGameplayState . Create ( new OsuRuleset ( ) ) , new Score ( ) ) ) ;
2022-01-28 21:26:05 +08:00
AddStep ( "send frames and finish play" , ( ) = >
{
spectatorClient . HandleFrame ( new OsuReplayFrame ( 1000 , Vector2 . Zero ) ) ;
2022-05-28 20:55:57 +08:00
var completedGameplayState = TestGameplayState . Create ( new OsuRuleset ( ) ) ;
completedGameplayState . HasPassed = true ;
spectatorClient . EndPlaying ( completedGameplayState ) ;
2022-01-28 21:26:05 +08:00
} ) ;
// We can't access API because we're an "online" test.
2022-01-31 14:12:08 +08:00
AddAssert ( "last received frame has time = 1000" , ( ) = > spectatorClient . LastReceivedUserFrames . First ( ) . Value . Time = = 1000 ) ;
2022-01-28 21:26:05 +08:00
}
2022-01-31 17:32:17 +08:00
[Test]
public void TestFinalFrameInBundleHasHeader ( )
{
FrameDataBundle lastBundle = null ;
AddStep ( "bind to client" , ( ) = > spectatorClient . OnNewFrames + = ( _ , bundle ) = > lastBundle = bundle ) ;
start ( - 1234 ) ;
sendFrames ( ) ;
finish ( ) ;
AddUntilStep ( "bundle received" , ( ) = > lastBundle ! = null ) ;
AddAssert ( "first frame does not have header" , ( ) = > lastBundle . Frames [ 0 ] . Header = = null ) ;
AddAssert ( "last frame has header" , ( ) = > lastBundle . Frames [ ^ 1 ] . Header ! = null ) ;
}
2022-02-08 19:27:08 +08:00
[Test]
public void TestPlayingState ( )
{
loadSpectatingScreen ( ) ;
start ( ) ;
sendFrames ( ) ;
waitForPlayer ( ) ;
2022-02-09 11:09:04 +08:00
AddUntilStep ( "state is playing" , ( ) = > spectatorClient . WatchedUserStates [ streamingUser . Id ] . State = = SpectatedUserState . Playing ) ;
2022-02-08 19:27:08 +08:00
}
[Test]
2022-02-08 19:29:49 +08:00
public void TestPassedState ( )
2022-02-08 19:27:08 +08:00
{
loadSpectatingScreen ( ) ;
start ( ) ;
sendFrames ( ) ;
waitForPlayer ( ) ;
2022-02-22 16:56:07 +08:00
AddStep ( "send passed" , ( ) = > spectatorClient . SendEndPlay ( streamingUser . Id , SpectatedUserState . Passed ) ) ;
2022-02-09 11:09:04 +08:00
AddUntilStep ( "state is passed" , ( ) = > spectatorClient . WatchedUserStates [ streamingUser . Id ] . State = = SpectatedUserState . Passed ) ;
2022-02-08 19:27:08 +08:00
start ( ) ;
sendFrames ( ) ;
waitForPlayer ( ) ;
2022-02-09 11:09:04 +08:00
AddUntilStep ( "state is playing" , ( ) = > spectatorClient . WatchedUserStates [ streamingUser . Id ] . State = = SpectatedUserState . Playing ) ;
2022-02-08 19:27:08 +08:00
}
[Test]
public void TestQuitState ( )
{
loadSpectatingScreen ( ) ;
start ( ) ;
sendFrames ( ) ;
waitForPlayer ( ) ;
2022-02-22 16:56:07 +08:00
AddStep ( "send quit" , ( ) = > spectatorClient . SendEndPlay ( streamingUser . Id ) ) ;
2022-02-09 11:09:04 +08:00
AddUntilStep ( "state is quit" , ( ) = > spectatorClient . WatchedUserStates [ streamingUser . Id ] . State = = SpectatedUserState . Quit ) ;
2022-02-08 19:27:08 +08:00
start ( ) ;
sendFrames ( ) ;
waitForPlayer ( ) ;
2022-02-09 11:09:04 +08:00
AddUntilStep ( "state is playing" , ( ) = > spectatorClient . WatchedUserStates [ streamingUser . Id ] . State = = SpectatedUserState . Playing ) ;
2022-02-08 19:27:08 +08:00
}
[Test]
public void TestFailedState ( )
{
loadSpectatingScreen ( ) ;
start ( ) ;
sendFrames ( ) ;
waitForPlayer ( ) ;
2022-02-22 16:56:07 +08:00
AddStep ( "send failed" , ( ) = > spectatorClient . SendEndPlay ( streamingUser . Id , SpectatedUserState . Failed ) ) ;
2022-02-09 11:09:04 +08:00
AddUntilStep ( "state is failed" , ( ) = > spectatorClient . WatchedUserStates [ streamingUser . Id ] . State = = SpectatedUserState . Failed ) ;
2022-02-08 19:27:08 +08:00
start ( ) ;
sendFrames ( ) ;
waitForPlayer ( ) ;
2022-02-09 11:09:04 +08:00
AddUntilStep ( "state is playing" , ( ) = > spectatorClient . WatchedUserStates [ streamingUser . Id ] . State = = SpectatedUserState . Playing ) ;
2022-02-08 19:27:08 +08:00
}
2020-10-29 14:10:42 +08:00
private OsuFramedReplayInputHandler replayHandler = >
( OsuFramedReplayInputHandler ) Stack . ChildrenOfType < OsuInputManager > ( ) . First ( ) . ReplayInputHandler ;
private Player player = > Stack . CurrentScreen as Player ;
private double currentFrameStableTime
2022-08-15 17:53:10 +08:00
= > player . ChildrenOfType < FrameStabilityContainer > ( ) . First ( ) . CurrentTime ;
2020-10-29 14:10:42 +08:00
2021-05-31 19:21:26 +08:00
private void waitForPlayer ( ) = > AddUntilStep ( "wait for player" , ( ) = > ( Stack . CurrentScreen as Player ) ? . IsLoaded = = true ) ;
2020-10-27 17:32:05 +08:00
2022-02-22 16:56:07 +08:00
private void start ( int? beatmapId = null ) = > AddStep ( "start play" , ( ) = > spectatorClient . SendStartPlay ( streamingUser . Id , beatmapId ? ? importedBeatmapId ) ) ;
2020-10-27 17:32:05 +08:00
2022-02-22 16:56:07 +08:00
private void finish ( SpectatedUserState state = SpectatedUserState . Quit ) = > AddStep ( "end play" , ( ) = > spectatorClient . SendEndPlay ( streamingUser . Id , state ) ) ;
2020-10-28 21:59:54 +08:00
2020-10-27 17:32:05 +08:00
private void checkPaused ( bool state ) = >
2020-10-28 21:59:54 +08:00
AddUntilStep ( $"game is {(state ? " paused " : " playing ")}" , ( ) = > player . ChildrenOfType < DrawableRuleset > ( ) . First ( ) . IsPaused . Value = = state ) ;
2020-10-27 17:32:05 +08:00
2022-03-17 18:23:49 +08:00
private void sendFrames ( int count = 10 , double startTime = 0 )
2020-10-27 17:13:58 +08:00
{
2022-03-17 18:23:49 +08:00
AddStep ( "send frames" , ( ) = > spectatorClient . SendFramesFromUser ( streamingUser . Id , count , startTime ) ) ;
2020-10-27 17:13:58 +08:00
}
2020-10-28 15:29:06 +08:00
private void loadSpectatingScreen ( )
{
2021-08-03 17:28:08 +08:00
AddStep ( "load spectator" , ( ) = > LoadScreen ( spectatorScreen = new SoloSpectator ( streamingUser ) ) ) ;
2020-10-28 15:29:06 +08:00
AddUntilStep ( "wait for screen load" , ( ) = > spectatorScreen . LoadState = = LoadState . Loaded ) ;
}
2021-08-03 17:28:08 +08:00
/// <summary>
/// Used for the sole purpose of adding <see cref="TestSpectatorClient"/> as a resolvable dependency.
/// </summary>
2022-11-24 13:32:20 +08:00
private partial class DependenciesScreen : OsuScreen
2021-08-03 17:28:08 +08:00
{
[Cached(typeof(SpectatorClient))]
2022-02-16 08:43:28 +08:00
public readonly TestSpectatorClient SpectatorClient = new TestSpectatorClient ( ) ;
2021-08-03 17:28:08 +08:00
2022-02-15 20:08:27 +08:00
[Cached(typeof(UserLookupCache))]
public readonly TestUserLookupCache UserLookupCache = new TestUserLookupCache ( ) ;
2021-08-03 17:28:08 +08:00
}
2020-10-26 18:47:39 +08:00
}
}