2020-10-22 17:10:27 +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 15:31:39 +08:00
using System ;
2020-10-22 17:10:27 +08:00
using System.Collections.Generic ;
2020-10-26 14:24:12 +08:00
using System.Linq ;
2020-10-22 17:10:27 +08: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 14:24:12 +08:00
using osu.Framework.Logging ;
2020-10-23 13:47:08 +08:00
using osu.Framework.Testing ;
2020-10-26 14:24:12 +08:00
using osu.Framework.Timing ;
2020-10-22 17:10:27 +08: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-02 01:22:23 +08:00
using osu.Game.Rulesets.Osu ;
2020-10-22 17:10:27 +08:00
using osu.Game.Rulesets.Replays ;
using osu.Game.Rulesets.Replays.Types ;
using osu.Game.Rulesets.UI ;
2020-12-14 15:52:14 +08:00
using osu.Game.Scoring ;
2022-05-28 20:55:57 +08:00
using osu.Game.Tests.Gameplay ;
2022-05-11 03:43:57 +08:00
using osu.Game.Tests.Mods ;
2022-02-04 18:03:52 +08:00
using osu.Game.Tests.Visual.Spectator ;
2020-10-22 17:10:27 +08:00
using osuTK ;
using osuTK.Graphics ;
namespace osu.Game.Tests.Visual.Gameplay
{
2020-10-26 14:24:28 +08:00
public partial class TestSceneSpectatorPlayback : OsuManualInputManagerTestScene
2020-10-22 17:10:27 +08:00
{
private TestRulesetInputManager playbackManager ;
private TestRulesetInputManager recordingManager ;
2022-07-28 19:44:02 +08:00
private Score recordingScore ;
private Replay playbackReplay ;
2022-02-24 01:18:42 +08:00
private TestSpectatorClient spectatorClient ;
2021-12-06 15:35:06 +08:00
private ManualClock manualClock ;
2022-02-24 01:18:42 +08:00
private TestReplayRecorder recorder ;
2020-10-26 14:24:12 +08:00
private OsuSpriteText latencyDisplay ;
2020-10-26 15:31:39 +08:00
private TestFramedReplayInputHandler replayHandler ;
2021-12-06 15:35:06 +08:00
[SetUpSteps]
public void SetUpSteps ( )
2020-10-22 17:10:27 +08:00
{
2021-12-06 15:35:06 +08:00
AddStep ( "Setup containers" , ( ) = >
2020-10-22 17:37:19 +08:00
{
2022-07-28 19:44:02 +08:00
recordingScore = new Score
{
ScoreInfo =
{
BeatmapInfo = new BeatmapInfo ( ) ,
Ruleset = new OsuRuleset ( ) . RulesetInfo ,
}
} ;
playbackReplay = new Replay ( ) ;
2021-12-06 15:35:06 +08:00
manualClock = new ManualClock ( ) ;
2020-11-21 07:06:20 +08:00
2022-02-04 18:03:52 +08:00
Child = new DependencyProvidingContainer
2021-12-06 15:35:06 +08:00
{
2022-02-04 18:03:52 +08:00
RelativeSizeAxes = Axes . Both ,
CachedDependencies = new [ ]
2021-12-06 15:35:06 +08:00
{
2022-02-04 18:03:52 +08:00
( typeof ( SpectatorClient ) , ( object ) ( spectatorClient = new TestSpectatorClient ( ) ) ) ,
} ,
Children = new Drawable [ ]
2020-10-22 17:10:27 +08:00
{
2022-02-04 18:03:52 +08:00
spectatorClient ,
new GridContainer
2020-10-22 17:10:27 +08:00
{
2022-02-04 18:03:52 +08:00
RelativeSizeAxes = Axes . Both ,
Content = new [ ]
2020-10-22 17:10:27 +08:00
{
2022-02-04 18:03:52 +08:00
new Drawable [ ]
2020-10-22 17:10:27 +08:00
{
2022-05-11 03:43:57 +08:00
recordingManager = new TestRulesetInputManager ( TestCustomisableModRuleset . CreateTestRulesetInfo ( ) , 0 , SimultaneousBindingMode . Unique )
2020-10-22 17:10:27 +08:00
{
2022-07-28 19:44:02 +08:00
Recorder = recorder = new TestReplayRecorder ( recordingScore )
2021-12-06 15:35:06 +08:00
{
2022-02-04 18:03:52 +08:00
ScreenSpaceToGamefield = pos = > recordingManager . ToLocalSpace ( pos ) ,
} ,
Child = new Container
{
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
2021-12-06 15:35:06 +08:00
{
2022-02-04 18:03:52 +08: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 17:10:27 +08:00
{
2022-05-11 03:43:57 +08:00
playbackManager = new TestRulesetInputManager ( TestCustomisableModRuleset . CreateTestRulesetInfo ( ) , 0 , SimultaneousBindingMode . Unique )
2020-10-22 17:10:27 +08:00
{
2022-02-04 18:03:52 +08:00
Clock = new FramedClock ( manualClock ) ,
2022-07-28 19:44:02 +08:00
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler ( playbackReplay )
2021-12-06 15:35:06 +08:00
{
2022-02-04 18:03:52 +08:00
GamefieldToScreenSpace = pos = > playbackManager . ToScreenSpace ( pos ) ,
} ,
Child = new Container
{
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
2021-12-06 15:35:06 +08:00
{
2022-02-04 18:03:52 +08: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 17:10:27 +08:00
}
2021-12-06 15:35:06 +08:00
}
2022-02-04 18:03:52 +08:00
} ,
latencyDisplay = new OsuSpriteText ( )
}
2021-12-06 15:35:06 +08:00
} ;
2022-02-04 18:03:52 +08:00
2022-12-12 12:59:27 +08:00
spectatorClient . BeginPlaying ( 0 , TestGameplayState . Create ( new OsuRuleset ( ) ) , recordingScore ) ;
2022-02-04 18:03:52 +08:00
spectatorClient . OnNewFrames + = onNewFrames ;
2020-10-22 17:10:27 +08:00
} ) ;
2021-12-06 15:35:06 +08:00
}
2020-10-22 17:10:27 +08:00
2022-02-24 01:18:42 +08:00
[Test]
public void TestBasic ( )
2020-10-26 14:24:12 +08:00
{
2022-07-28 19:44:02 +08:00
AddUntilStep ( "received frames" , ( ) = > playbackReplay . Frames . Count > 50 ) ;
2022-02-24 01:18:42 +08:00
AddStep ( "stop sending frames" , ( ) = > recorder . Expire ( ) ) ;
2022-07-28 19:44:02 +08:00
AddUntilStep ( "wait for all frames received" , ( ) = > playbackReplay . Frames . Count = = recorder . SentFrames . Count ) ;
2022-02-24 01:18:42 +08:00
}
[Test]
public void TestWithSendFailure ( )
{
2022-07-28 19:44:02 +08:00
AddUntilStep ( "received frames" , ( ) = > playbackReplay . Frames . Count > 50 ) ;
2022-02-24 01:18:42 +08:00
int framesReceivedSoFar = 0 ;
int frameSendAttemptsSoFar = 0 ;
AddStep ( "start failing sends" , ( ) = >
{
spectatorClient . ShouldFailSendingFrames = true ;
frameSendAttemptsSoFar = spectatorClient . FrameSendAttempts ;
} ) ;
2020-10-26 14:24:12 +08:00
2022-06-28 13:58:33 +08:00
AddUntilStep ( "wait for next send attempt" , ( ) = >
{
2022-07-28 19:44:02 +08:00
framesReceivedSoFar = playbackReplay . Frames . Count ;
2022-06-28 13:58:33 +08:00
return spectatorClient . FrameSendAttempts > frameSendAttemptsSoFar + 1 ;
} ) ;
AddUntilStep ( "wait for more send attempts" , ( ) = > spectatorClient . FrameSendAttempts > frameSendAttemptsSoFar + 10 ) ;
2022-07-28 19:44:02 +08:00
AddAssert ( "frames did not increase" , ( ) = > framesReceivedSoFar = = playbackReplay . Frames . Count ) ;
2022-02-24 01:18:42 +08:00
AddStep ( "stop failing sends" , ( ) = > spectatorClient . ShouldFailSendingFrames = false ) ;
2022-07-28 19:44:02 +08:00
AddUntilStep ( "wait for next frames" , ( ) = > framesReceivedSoFar < playbackReplay . Frames . Count ) ;
2022-02-24 01:18:42 +08:00
AddStep ( "stop sending frames" , ( ) = > recorder . Expire ( ) ) ;
2022-07-28 19:44:02 +08:00
AddUntilStep ( "wait for all frames received" , ( ) = > playbackReplay . Frames . Count = = recorder . SentFrames . Count ) ;
AddAssert ( "ensure frames were received in the correct sequence" , ( ) = > playbackReplay . Frames . Select ( f = > f . Time ) . SequenceEqual ( recorder . SentFrames . Select ( f = > f . Time ) ) ) ;
2022-02-24 01:18:42 +08:00
}
private void onNewFrames ( int userId , FrameDataBundle frames )
{
2020-10-26 14:24:12 +08:00
foreach ( var legacyFrame in frames . Frames )
{
var frame = new TestReplayFrame ( ) ;
2024-02-02 18:48:13 +08:00
frame . FromLegacy ( legacyFrame , null ! ) ;
2022-07-28 19:44:02 +08:00
playbackReplay . Frames . Add ( frame ) ;
2020-10-26 14:24:12 +08:00
}
2022-07-28 19:44:02 +08:00
Logger . Log ( $"Received {frames.Frames.Count} new frames (total {playbackReplay.Frames.Count} of {recorder.SentFrames.Count})" ) ;
2020-10-22 17:10:27 +08:00
}
2021-05-20 14:55:07 +08:00
private double latency = SpectatorClient . TIME_BETWEEN_SENDS ;
2020-10-26 15:31:39 +08:00
2020-10-22 17:10:27 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2020-10-26 14:24:12 +08:00
2020-10-26 15:31:39 +08:00
if ( latencyDisplay = = null ) return ;
2020-10-26 14:24:12 +08:00
2020-10-26 15:31:39 +08:00
// propagate initial time value
if ( manualClock . CurrentTime = = 0 )
2020-10-26 14:24:12 +08:00
{
2020-10-26 15:31:39 +08:00
manualClock . CurrentTime = Time . Current ;
return ;
2020-10-26 14:24:12 +08:00
}
2020-10-26 15:31:39 +08:00
2021-04-12 10:17:56 +08:00
if ( ! replayHandler . HasFrames )
return ;
2020-10-26 15:31:39 +08:00
2022-07-28 19:44:02 +08:00
var lastFrame = playbackReplay . Frames . LastOrDefault ( ) ;
2020-10-26 15:31:39 +08:00
2021-04-12 10:17:56 +08: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 15:31:39 +08:00
2021-04-12 10:17:56 +08:00
latencyDisplay . Text = $"latency: {latency:N1}" ;
2020-10-26 15:31:39 +08:00
2021-04-12 10:17:56 +08:00
double proposedTime = Time . Current - latency + Time . Elapsed ;
2020-10-26 15:31:39 +08:00
2021-04-12 10:17:56 +08:00
// this will either advance by one or zero frames.
double? time = replayHandler . SetFrameFromTime ( proposedTime ) ;
2020-10-26 15:31:39 +08:00
2021-04-12 10:17:56 +08:00
if ( time = = null )
return ;
manualClock . CurrentTime = time . Value ;
2020-10-22 17:10:27 +08:00
}
public class TestFramedReplayInputHandler : FramedReplayInputHandler < TestReplayFrame >
{
public TestFramedReplayInputHandler ( Replay replay )
: base ( replay )
{
}
2022-01-31 17:37:51 +08:00
protected override void CollectReplayInputs ( List < IInput > inputs )
2020-10-22 17:10:27 +08:00
{
inputs . Add ( new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace ( CurrentFrame ? . Position ? ? Vector2 . Zero ) } ) ;
inputs . Add ( new ReplayState < TestAction > { PressedActions = CurrentFrame ? . Actions ? ? new List < TestAction > ( ) } ) ;
}
}
public partial class TestInputConsumer : CompositeDrawable , IKeyBindingHandler < TestAction >
{
2023-10-17 16:40:44 +08:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = > Parent ! . ReceivePositionalInputAt ( screenSpacePos ) ;
2020-10-22 17:10:27 +08:00
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 17:26:12 +08:00
public bool OnPressed ( KeyBindingPressEvent < TestAction > e )
2020-10-22 17:10:27 +08:00
{
2021-11-18 11:36:52 +08:00
if ( e . Repeat )
return false ;
2020-10-22 17:10:27 +08:00
box . Colour = Color4 . White ;
return true ;
}
2021-09-16 17:26:12 +08:00
public void OnReleased ( KeyBindingReleaseEvent < TestAction > e )
2020-10-22 17:10:27 +08:00
{
box . Colour = Color4 . Black ;
}
}
public partial 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 partial class TestKeyBindingContainer : KeyBindingContainer < TestAction >
{
2021-01-15 12:41:35 +08:00
public override IEnumerable < IKeyBinding > DefaultKeyBindings = > new [ ]
2020-10-22 17:10:27 +08: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 partial class TestReplayRecorder : ReplayRecorder < TestAction >
{
2022-02-24 01:18:42 +08:00
public List < ReplayFrame > SentFrames = new List < ReplayFrame > ( ) ;
2022-07-28 19:44:02 +08:00
public TestReplayRecorder ( Score score )
: base ( score )
2020-10-22 17:10:27 +08:00
{
}
protected override ReplayFrame HandleFrame ( Vector2 mousePosition , List < TestAction > actions , ReplayFrame previousFrame )
{
2022-02-24 01:18:42 +08:00
var testReplayFrame = new TestReplayFrame ( Time . Current , mousePosition , actions . ToArray ( ) ) ;
SentFrames . Add ( testReplayFrame ) ;
return testReplayFrame ;
2020-10-22 17:10:27 +08:00
}
}
}
}