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-22 09:37:19 +00:00
using System.Collections.Specialized ;
2020-11-20 23:06:20 +00:00
using System.Diagnostics ;
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.Allocation ;
2020-10-22 09:37:19 +00:00
using osu.Framework.Bindables ;
2020-10-22 09:10:27 +00:00
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 ;
2020-10-26 06:25:09 +00:00
using osu.Game.Online.API ;
2020-10-22 09:10:27 +00:00
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 ;
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
{
protected override bool UseOnlineAPI = > true ;
private TestRulesetInputManager playbackManager ;
private TestRulesetInputManager recordingManager ;
private Replay replay ;
2020-11-04 06:35:42 +00:00
private readonly IBindableList < int > users = new BindableList < int > ( ) ;
2020-10-22 09:10:27 +00:00
2020-10-23 05:47:08 +00:00
private TestReplayRecorder recorder ;
2021-12-06 07:35:06 +00:00
private ManualClock manualClock ;
2020-10-26 06:24:12 +00:00
private OsuSpriteText latencyDisplay ;
2020-10-26 07:31:39 +00:00
private TestFramedReplayInputHandler replayHandler ;
2020-10-26 06:25:09 +00:00
[Resolved]
private IAPIProvider api { get ; set ; }
2020-10-22 09:10:27 +00:00
[Resolved]
2021-05-20 06:55:07 +00:00
private SpectatorClient spectatorClient { get ; set ; }
2020-10-22 09:10:27 +00:00
2020-10-26 23:05:03 +00:00
[Cached]
2021-10-01 17:22:23 +00:00
private GameplayState gameplayState = new GameplayState ( new Beatmap ( ) , new OsuRuleset ( ) , Array . Empty < Mod > ( ) ) ;
2020-10-26 23:05:03 +00:00
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 ( "Reset recorder state" , cleanUpState ) ;
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
2021-12-06 07:35:06 +00:00
spectatorClient . OnNewFrames + = onNewFrames ;
2020-10-26 06:25:09 +00:00
2021-12-06 07:35:06 +00:00
users . BindTo ( spectatorClient . PlayingUsers ) ;
users . BindCollectionChanged ( ( obj , args ) = >
{
switch ( args . Action )
{
case NotifyCollectionChangedAction . Add :
Debug . Assert ( args . NewItems ! = null ) ;
2020-10-22 10:16:34 +00:00
2021-12-06 07:35:06 +00:00
foreach ( int user in args . NewItems )
{
if ( user = = api . LocalUser . Value . Id )
spectatorClient . WatchUser ( user ) ;
}
2020-11-20 23:06:20 +00:00
2021-12-06 07:35:06 +00:00
break ;
2020-10-26 06:25:09 +00:00
2021-12-06 07:35:06 +00:00
case NotifyCollectionChangedAction . Remove :
Debug . Assert ( args . OldItems ! = null ) ;
2020-10-22 09:37:19 +00:00
2021-12-06 07:35:06 +00:00
foreach ( int user in args . OldItems )
{
if ( user = = api . LocalUser . Value . Id )
spectatorClient . StopWatchingUser ( user ) ;
}
2020-10-22 09:10:27 +00:00
2021-12-06 07:35:06 +00:00
break ;
}
} , true ) ;
Children = new Drawable [ ]
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
new GridContainer
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
RelativeSizeAxes = Axes . Both ,
Content = new [ ]
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
new Drawable [ ]
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
recordingManager = new TestRulesetInputManager ( TestSceneModSettings . CreateTestRulesetInfo ( ) , 0 , SimultaneousBindingMode . Unique )
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
Recorder = recorder = new TestReplayRecorder
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
ScreenSpaceToGamefield = pos = > recordingManager . ToLocalSpace ( pos ) ,
2020-10-22 09:10:27 +00:00
} ,
2021-12-06 07:35:06 +00:00
Child = new Container
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
{
new Box
{
Colour = Color4 . Brown ,
RelativeSizeAxes = Axes . Both ,
} ,
new OsuSpriteText
{
Text = "Sending" ,
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
new Drawable [ ]
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
playbackManager = new TestRulesetInputManager ( TestSceneModSettings . CreateTestRulesetInfo ( ) , 0 , SimultaneousBindingMode . Unique )
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
Clock = new FramedClock ( manualClock ) ,
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler ( replay )
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
GamefieldToScreenSpace = pos = > playbackManager . ToScreenSpace ( pos ) ,
2020-10-22 09:10:27 +00:00
} ,
2021-12-06 07:35:06 +00:00
Child = new Container
2020-10-22 09:10:27 +00:00
{
2021-12-06 07:35:06 +00:00
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
{
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
}
2020-10-22 09:10:27 +00:00
}
2021-12-06 07:35:06 +00:00
} ,
latencyDisplay = new OsuSpriteText ( )
} ;
2020-10-22 09:10:27 +00:00
} ) ;
2021-12-06 07:35:06 +00:00
}
2020-10-22 09:10:27 +00:00
2020-10-26 06:24:12 +00:00
private void onNewFrames ( int userId , FrameDataBundle frames )
{
Logger . Log ( $"Received {frames.Frames.Count()} new frames ({string.Join(',', frames.Frames.Select(f => ((int)f.Time).ToString()))})" ) ;
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 ) ;
}
}
2020-10-22 09:10:27 +00:00
[Test]
public void TestBasic ( )
{
2021-12-06 07:35:06 +00:00
AddStep ( "Wait for user input" , ( ) = > { } ) ;
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
}
2020-10-23 05:47:08 +00:00
[TearDownSteps]
public void TearDown ( )
{
2021-12-06 07:35:06 +00:00
AddStep ( "stop recorder" , cleanUpState ) ;
}
private void cleanUpState ( )
{
2021-12-06 08:38:57 +00:00
// Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`.
recorder ? . RemoveAndDisposeImmediately ( ) ;
recorder = null ;
spectatorClient . OnNewFrames - = onNewFrames ;
2020-10-23 05:47:08 +00:00
}
2020-10-22 09:10:27 +00:00
public class TestFramedReplayInputHandler : FramedReplayInputHandler < TestReplayFrame >
{
public TestFramedReplayInputHandler ( Replay replay )
: base ( replay )
{
}
public override void CollectPendingInputs ( List < IInput > inputs )
{
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 >
{
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 )
{
return new TestReplayFrame ( Time . Current , mousePosition , actions . ToArray ( ) ) ;
}
}
}
}