2020-10-22 09:10:27 +00: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.
2020-10-26 07:31:39 +00:00
using System ;
2020-10-22 09:10:27 +00:00
using System.Collections.Generic ;
2020-10-26 06:24:12 +00:00
using System.Linq ;
2020-10-22 09:10:27 +00:00
using NUnit.Framework ;
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Graphics.Shapes ;
using osu.Framework.Input.Bindings ;
using osu.Framework.Input.Events ;
using osu.Framework.Input.StateChanges ;
2020-10-26 06:24:12 +00:00
using osu.Framework.Logging ;
2020-10-23 05:47:08 +00:00
using osu.Framework.Testing ;
2020-10-26 06:24:12 +00:00
using osu.Framework.Timing ;
2020-10-22 09:10:27 +00:00
using osu.Game.Beatmaps ;
using osu.Game.Graphics.Sprites ;
using osu.Game.Online.Spectator ;
using osu.Game.Replays ;
using osu.Game.Replays.Legacy ;
using osu.Game.Rulesets ;
2021-10-01 17:22:23 +00:00
using osu.Game.Rulesets.Mods ;
using osu.Game.Rulesets.Osu ;
2020-10-22 09:10:27 +00:00
using osu.Game.Rulesets.Replays ;
using osu.Game.Rulesets.Replays.Types ;
using osu.Game.Rulesets.UI ;
2020-12-14 07:52:14 +00:00
using osu.Game.Scoring ;
2020-10-26 23:05:03 +00:00
using osu.Game.Screens.Play ;
2022-02-04 10:03:52 +00:00
using osu.Game.Tests.Visual.Spectator ;
2020-10-22 09:10:27 +00:00
using osu.Game.Tests.Visual.UserInterface ;
using osuTK ;
using osuTK.Graphics ;
namespace osu.Game.Tests.Visual.Gameplay
{
2020-10-26 06:24:28 +00:00
public class TestSceneSpectatorPlayback : OsuManualInputManagerTestScene
2020-10-22 09:10:27 +00:00
{
private TestRulesetInputManager playbackManager ;
private TestRulesetInputManager recordingManager ;
private Replay replay ;
2022-02-23 17:18:42 +00:00
private TestSpectatorClient spectatorClient ;
2021-12-06 07:35:06 +00:00
private ManualClock manualClock ;
2020-10-26 06:24:12 +00:00
2022-02-23 17:18:42 +00:00
private TestReplayRecorder recorder ;
2020-10-26 06:24:12 +00:00
private OsuSpriteText latencyDisplay ;
2020-10-26 07:31:39 +00:00
private TestFramedReplayInputHandler replayHandler ;
2021-12-06 07:35:06 +00:00
[SetUpSteps]
public void SetUpSteps ( )
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
AddStep ( "Setup containers" , ( ) = >
2020-10-22 09:37:19 +00:00
{
2021-12-06 07:35:06 +00:00
replay = new Replay ( ) ;
manualClock = new ManualClock ( ) ;
2020-11-20 23:06:20 +00:00
2022-02-04 10:03:52 +00:00
Child = new DependencyProvidingContainer
2021-12-06 07:35:06 +00:00
{
2022-02-04 10:03:52 +00:00
RelativeSizeAxes = Axes . Both ,
CachedDependencies = new [ ]
2021-12-06 07:35:06 +00:00
{
2022-02-04 10:03:52 +00:00
( typeof ( SpectatorClient ) , ( object ) ( spectatorClient = new TestSpectatorClient ( ) ) ) ,
( typeof ( GameplayState ) , new GameplayState ( new Beatmap ( ) , new OsuRuleset ( ) , Array . Empty < Mod > ( ) ) )
} ,
Children = new Drawable [ ]
2020-10-22 09:10:27 +00:00
{
2022-02-04 10:03:52 +00:00
spectatorClient ,
new GridContainer
2020-10-22 09:10:27 +00:00
{
2022-02-04 10:03:52 +00:00
RelativeSizeAxes = Axes . Both ,
Content = new [ ]
2020-10-22 09:10:27 +00:00
{
2022-02-04 10:03:52 +00:00
new Drawable [ ]
2020-10-22 09:10:27 +00:00
{
2022-02-04 10:03:52 +00:00
recordingManager = new TestRulesetInputManager ( TestSceneModSettings . CreateTestRulesetInfo ( ) , 0 , SimultaneousBindingMode . Unique )
2020-10-22 09:10:27 +00:00
{
2022-02-23 17:18:42 +00:00
Recorder = recorder = new TestReplayRecorder
2021-12-06 07:35:06 +00:00
{
2022-02-04 10:03:52 +00:00
ScreenSpaceToGamefield = pos = > recordingManager . ToLocalSpace ( pos ) ,
} ,
Child = new Container
{
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
2021-12-06 07:35:06 +00:00
{
2022-02-04 10:03:52 +00:00
new Box
{
Colour = Color4 . Brown ,
RelativeSizeAxes = Axes . Both ,
} ,
new OsuSpriteText
{
Text = "Sending" ,
Scale = new Vector2 ( 3 ) ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
} ,
new TestInputConsumer ( )
}
} ,
}
} ,
new Drawable [ ]
2020-10-22 09:10:27 +00:00
{
2022-02-04 10:03:52 +00:00
playbackManager = new TestRulesetInputManager ( TestSceneModSettings . CreateTestRulesetInfo ( ) , 0 , SimultaneousBindingMode . Unique )
2020-10-22 09:10:27 +00:00
{
2022-02-04 10:03:52 +00:00
Clock = new FramedClock ( manualClock ) ,
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler ( replay )
2021-12-06 07:35:06 +00:00
{
2022-02-04 10:03:52 +00:00
GamefieldToScreenSpace = pos = > playbackManager . ToScreenSpace ( pos ) ,
} ,
Child = new Container
{
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
2021-12-06 07:35:06 +00:00
{
2022-02-04 10:03:52 +00:00
new Box
{
Colour = Color4 . DarkBlue ,
RelativeSizeAxes = Axes . Both ,
} ,
new OsuSpriteText
{
Text = "Receiving" ,
Scale = new Vector2 ( 3 ) ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
} ,
new TestInputConsumer ( )
}
} ,
}
2020-10-22 09:10:27 +00:00
}
2021-12-06 07:35:06 +00:00
}
2022-02-04 10:03:52 +00:00
} ,
latencyDisplay = new OsuSpriteText ( )
}
2021-12-06 07:35:06 +00:00
} ;
2022-02-04 10:03:52 +00:00
spectatorClient . OnNewFrames + = onNewFrames ;
2020-10-22 09:10:27 +00:00
} ) ;
2021-12-06 07:35:06 +00:00
}
2020-10-22 09:10:27 +00:00
2022-02-23 17:18:42 +00:00
[Test]
public void TestBasic ( )
2020-10-26 06:24:12 +00:00
{
2022-02-23 17:18:42 +00:00
AddUntilStep ( "received frames" , ( ) = > replay . Frames . Count > 50 ) ;
AddStep ( "stop sending frames" , ( ) = > recorder . Expire ( ) ) ;
AddUntilStep ( "wait for all frames received" , ( ) = > replay . Frames . Count = = recorder . SentFrames . Count ) ;
}
[Test]
public void TestWithSendFailure ( )
{
AddUntilStep ( "received frames" , ( ) = > replay . Frames . Count > 50 ) ;
int framesReceivedSoFar = 0 ;
int frameSendAttemptsSoFar = 0 ;
AddStep ( "start failing sends" , ( ) = >
{
spectatorClient . ShouldFailSendingFrames = true ;
framesReceivedSoFar = replay . Frames . Count ;
frameSendAttemptsSoFar = spectatorClient . FrameSendAttempts ;
} ) ;
2020-10-26 06:24:12 +00:00
2022-02-23 17:18:42 +00:00
AddUntilStep ( "wait for send attempts" , ( ) = > spectatorClient . FrameSendAttempts > frameSendAttemptsSoFar + 5 ) ;
AddAssert ( "frames did not increase" , ( ) = > framesReceivedSoFar = = replay . Frames . Count ) ;
AddStep ( "stop failing sends" , ( ) = > spectatorClient . ShouldFailSendingFrames = false ) ;
AddUntilStep ( "wait for next frames" , ( ) = > framesReceivedSoFar < replay . Frames . Count ) ;
AddStep ( "stop sending frames" , ( ) = > recorder . Expire ( ) ) ;
AddUntilStep ( "wait for all frames received" , ( ) = > replay . Frames . Count = = recorder . SentFrames . Count ) ;
AddAssert ( "ensure frames were received in the correct sequence" , ( ) = > replay . Frames . Select ( f = > f . Time ) . SequenceEqual ( recorder . SentFrames . Select ( f = > f . Time ) ) ) ;
}
private void onNewFrames ( int userId , FrameDataBundle frames )
{
2020-10-26 06:24:12 +00:00
foreach ( var legacyFrame in frames . Frames )
{
var frame = new TestReplayFrame ( ) ;
2021-07-05 15:52:39 +00:00
frame . FromLegacy ( legacyFrame , null ) ;
2020-10-26 06:24:12 +00:00
replay . Frames . Add ( frame ) ;
}
2022-02-23 17:18:42 +00:00
Logger . Log ( $"Received {frames.Frames.Count} new frames (total {replay.Frames.Count} of {recorder.SentFrames.Count})" ) ;
2020-10-22 09:10:27 +00:00
}
2021-05-20 06:55:07 +00:00
private double latency = SpectatorClient . TIME_BETWEEN_SENDS ;
2020-10-26 07:31:39 +00:00
2020-10-22 09:10:27 +00:00
protected override void Update ( )
{
base . Update ( ) ;
2020-10-26 06:24:12 +00:00
2020-10-26 07:31:39 +00:00
if ( latencyDisplay = = null ) return ;
2020-10-26 06:24:12 +00:00
2020-10-26 07:31:39 +00:00
// propagate initial time value
if ( manualClock . CurrentTime = = 0 )
2020-10-26 06:24:12 +00:00
{
2020-10-26 07:31:39 +00:00
manualClock . CurrentTime = Time . Current ;
return ;
2020-10-26 06:24:12 +00:00
}
2020-10-26 07:31:39 +00:00
2021-04-12 02:17:56 +00:00
if ( ! replayHandler . HasFrames )
return ;
2020-10-26 07:31:39 +00:00
2021-04-12 02:17:56 +00:00
var lastFrame = replay . Frames . LastOrDefault ( ) ;
2020-10-26 07:31:39 +00:00
2021-04-12 02:17:56 +00:00
// this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved).
// in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation.
if ( lastFrame ! = null )
latency = Math . Max ( latency , Time . Current - lastFrame . Time ) ;
2020-10-26 07:31:39 +00:00
2021-04-12 02:17:56 +00:00
latencyDisplay . Text = $"latency: {latency:N1}" ;
2020-10-26 07:31:39 +00:00
2021-04-12 02:17:56 +00:00
double proposedTime = Time . Current - latency + Time . Elapsed ;
2020-10-26 07:31:39 +00:00
2021-04-12 02:17:56 +00:00
// this will either advance by one or zero frames.
double? time = replayHandler . SetFrameFromTime ( proposedTime ) ;
2020-10-26 07:31:39 +00:00
2021-04-12 02:17:56 +00:00
if ( time = = null )
return ;
manualClock . CurrentTime = time . Value ;
2020-10-22 09:10:27 +00:00
}
public class TestFramedReplayInputHandler : FramedReplayInputHandler < TestReplayFrame >
{
public TestFramedReplayInputHandler ( Replay replay )
: base ( replay )
{
}
2022-01-31 09:37:51 +00:00
protected override void CollectReplayInputs ( List < IInput > inputs )
2020-10-22 09:10:27 +00:00
{
inputs . Add ( new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace ( CurrentFrame ? . Position ? ? Vector2 . Zero ) } ) ;
inputs . Add ( new ReplayState < TestAction > { PressedActions = CurrentFrame ? . Actions ? ? new List < TestAction > ( ) } ) ;
}
}
public class TestInputConsumer : CompositeDrawable , IKeyBindingHandler < TestAction >
{
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = > Parent . ReceivePositionalInputAt ( screenSpacePos ) ;
private readonly Box box ;
public TestInputConsumer ( )
{
Size = new Vector2 ( 30 ) ;
Origin = Anchor . Centre ;
InternalChildren = new Drawable [ ]
{
box = new Box
{
Colour = Color4 . Black ,
RelativeSizeAxes = Axes . Both ,
} ,
} ;
}
protected override bool OnMouseMove ( MouseMoveEvent e )
{
Position = e . MousePosition ;
return base . OnMouseMove ( e ) ;
}
2021-09-16 09:26:12 +00:00
public bool OnPressed ( KeyBindingPressEvent < TestAction > e )
2020-10-22 09:10:27 +00:00
{
2021-11-18 03:36:52 +00:00
if ( e . Repeat )
return false ;
2020-10-22 09:10:27 +00:00
box . Colour = Color4 . White ;
return true ;
}
2021-09-16 09:26:12 +00:00
public void OnReleased ( KeyBindingReleaseEvent < TestAction > e )
2020-10-22 09:10:27 +00:00
{
box . Colour = Color4 . Black ;
}
}
public class TestRulesetInputManager : RulesetInputManager < TestAction >
{
public TestRulesetInputManager ( RulesetInfo ruleset , int variant , SimultaneousBindingMode unique )
: base ( ruleset , variant , unique )
{
}
protected override KeyBindingContainer < TestAction > CreateKeyBindingContainer ( RulesetInfo ruleset , int variant , SimultaneousBindingMode unique )
= > new TestKeyBindingContainer ( ) ;
internal class TestKeyBindingContainer : KeyBindingContainer < TestAction >
{
2021-01-15 04:41:35 +00:00
public override IEnumerable < IKeyBinding > DefaultKeyBindings = > new [ ]
2020-10-22 09:10:27 +00:00
{
new KeyBinding ( InputKey . MouseLeft , TestAction . Down ) ,
} ;
}
}
public class TestReplayFrame : ReplayFrame , IConvertibleReplayFrame
{
public Vector2 Position ;
public List < TestAction > Actions = new List < TestAction > ( ) ;
public TestReplayFrame ( double time , Vector2 position , params TestAction [ ] actions )
: base ( time )
{
Position = position ;
Actions . AddRange ( actions ) ;
}
public TestReplayFrame ( )
{
}
public void FromLegacy ( LegacyReplayFrame currentFrame , IBeatmap beatmap , ReplayFrame lastFrame = null )
{
Position = currentFrame . Position ;
Time = currentFrame . Time ;
if ( currentFrame . MouseLeft )
Actions . Add ( TestAction . Down ) ;
}
public LegacyReplayFrame ToLegacy ( IBeatmap beatmap )
{
ReplayButtonState state = ReplayButtonState . None ;
if ( Actions . Contains ( TestAction . Down ) )
state | = ReplayButtonState . Left1 ;
return new LegacyReplayFrame ( Time , Position . X , Position . Y , state ) ;
}
}
public enum TestAction
{
Down ,
}
internal class TestReplayRecorder : ReplayRecorder < TestAction >
{
2022-02-23 17:18:42 +00:00
public List < ReplayFrame > SentFrames = new List < ReplayFrame > ( ) ;
2020-10-22 09:10:27 +00:00
public TestReplayRecorder ( )
2022-01-10 06:06:41 +00:00
: base ( new Score
{
ScoreInfo =
{
BeatmapInfo = new BeatmapInfo ( ) ,
Ruleset = new OsuRuleset ( ) . RulesetInfo ,
}
} )
2020-10-22 09:10:27 +00:00
{
}
protected override ReplayFrame HandleFrame ( Vector2 mousePosition , List < TestAction > actions , ReplayFrame previousFrame )
{
2022-02-23 17:18:42 +00:00
var testReplayFrame = new TestReplayFrame ( Time . Current , mousePosition , actions . ToArray ( ) ) ;
SentFrames . Add ( testReplayFrame ) ;
return testReplayFrame ;
2020-10-22 09:10:27 +00:00
}
}
}
}