2020-03-23 17:33:02 +09: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 16:37:17 +09:00
#nullable disable
2022-07-25 05:21:27 +03:00
using System ;
2020-03-23 17:33:02 +09:00
using System.Collections.Generic ;
2020-03-24 16:22:54 +09:00
using System.Linq ;
using NUnit.Framework ;
2020-03-23 17:33:02 +09: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 ;
2020-03-23 18:50:16 +09:00
using osu.Framework.Input.StateChanges ;
2020-03-24 16:22:54 +09:00
using osu.Framework.Testing ;
using osu.Framework.Threading ;
2025-01-09 13:24:12 +09:00
using osu.Framework.Timing ;
2020-03-23 18:50:16 +09:00
using osu.Game.Graphics.Sprites ;
using osu.Game.Replays ;
2020-03-23 17:33:02 +09:00
using osu.Game.Rulesets ;
2021-10-02 02:22:23 +09:00
using osu.Game.Rulesets.Osu ;
2020-03-23 18:50:16 +09:00
using osu.Game.Rulesets.Replays ;
2020-03-23 17:33:02 +09:00
using osu.Game.Rulesets.UI ;
2020-12-14 16:52:14 +09:00
using osu.Game.Scoring ;
2020-10-27 00:05:03 +01:00
using osu.Game.Screens.Play ;
2022-05-28 14:55:57 +02:00
using osu.Game.Tests.Gameplay ;
2022-05-10 21:43:57 +02:00
using osu.Game.Tests.Mods ;
2020-03-23 17:33:02 +09:00
using osuTK ;
using osuTK.Graphics ;
2020-03-24 16:22:54 +09:00
using osuTK.Input ;
2020-03-23 17:33:02 +09:00
2020-03-30 09:59:52 +09:00
namespace osu.Game.Tests.Visual.Gameplay
2020-03-23 17:33:02 +09:00
{
2020-03-24 16:22:54 +09:00
public partial class TestSceneReplayRecorder : OsuManualInputManagerTestScene
2020-03-23 17:33:02 +09:00
{
2020-03-24 16:22:54 +09:00
private TestRulesetInputManager playbackManager ;
private TestRulesetInputManager recordingManager ;
2020-03-23 18:50:16 +09:00
2020-03-24 16:22:54 +09:00
private Replay replay ;
private TestReplayRecorder recorder ;
2022-07-25 05:21:27 +03:00
private GameplayState gameplayState ;
2020-10-27 00:05:03 +01:00
2025-01-09 13:24:12 +09:00
private Drawable content ;
2021-12-06 16:42:24 +09:00
[SetUpSteps]
public void SetUpSteps ( )
2020-03-23 17:33:02 +09:00
{
2021-12-06 16:42:24 +09:00
AddStep ( "Reset recorder state" , cleanUpState ) ;
2020-03-23 18:50:16 +09:00
2021-12-06 16:42:24 +09:00
AddStep ( "Setup containers" , ( ) = >
2020-03-23 17:33:02 +09:00
{
2021-12-06 16:42:24 +09:00
replay = new Replay ( ) ;
2022-07-25 05:21:27 +03:00
gameplayState = TestGameplayState . Create ( new OsuRuleset ( ) ) ;
gameplayState . Score . Replay = replay ;
Child = new DependencyProvidingContainer
2020-03-23 17:33:02 +09:00
{
2021-12-06 16:42:24 +09:00
RelativeSizeAxes = Axes . Both ,
2022-07-25 05:21:27 +03:00
CachedDependencies = new ( Type , object ) [ ] { ( typeof ( GameplayState ) , gameplayState ) } ,
2025-01-09 13:24:12 +09:00
Child = content = createContent ( ) ,
2022-07-25 05:21:27 +03:00
} ;
2020-03-23 17:33:02 +09:00
} ) ;
2021-12-06 16:42:24 +09:00
}
2020-03-24 16:22:54 +09:00
[Test]
public void TestBasic ( )
{
AddStep ( "move to center" , ( ) = > InputManager . MoveMouseTo ( recordingManager . ScreenSpaceDrawQuad . Centre ) ) ;
2025-01-09 13:24:12 +09:00
AddUntilStep ( "at least one frame recorded" , ( ) = > replay . Frames . Count , ( ) = > Is . GreaterThanOrEqualTo ( 0 ) ) ;
2021-04-19 17:02:59 +09:00
AddUntilStep ( "position matches" , ( ) = > playbackManager . ChildrenOfType < Box > ( ) . First ( ) . Position = = recordingManager . ChildrenOfType < Box > ( ) . First ( ) . Position ) ;
2020-03-24 16:22:54 +09:00
}
2025-01-09 13:24:12 +09:00
[Test]
2025-01-09 13:33:55 +09:00
[Explicit("Making this test work in a headless context is high effort due to rate adjustment requirements not aligning with the global fast clock. StopwatchClock usage would need to be replace with a rate adjusting clock that still reads from the parent clock. High effort for a test which likely will not see any changes to covered code for some years.")]
2025-01-09 13:24:12 +09:00
public void TestSlowClockStillRecordsFramesInRealtime ( )
{
ScheduledDelegate moveFunction = null ;
AddStep ( "set slow running clock" , ( ) = >
{
var stopwatchClock = new StopwatchClock ( true ) { Rate = 0.01 } ;
stopwatchClock . Seek ( Clock . CurrentTime ) ;
content . Clock = new FramedClock ( stopwatchClock ) ;
} ) ;
AddStep ( "move to center" , ( ) = > InputManager . MoveMouseTo ( recordingManager . ScreenSpaceDrawQuad . Centre ) ) ;
AddStep ( "much move" , ( ) = > moveFunction = Scheduler . AddDelayed ( ( ) = >
InputManager . MoveMouseTo ( InputManager . CurrentState . Mouse . Position + new Vector2 ( - 1 , 0 ) ) , 10 , true ) ) ;
AddWaitStep ( "move" , 10 ) ;
AddStep ( "stop move" , ( ) = > moveFunction . Cancel ( ) ) ;
AddAssert ( "at least 60 frames recorded" , ( ) = > replay . Frames . Count , ( ) = > Is . GreaterThanOrEqualTo ( 60 ) ) ;
}
2020-03-24 16:22:54 +09:00
[Test]
public void TestHighFrameRate ( )
{
ScheduledDelegate moveFunction = null ;
AddStep ( "move to center" , ( ) = > InputManager . MoveMouseTo ( recordingManager . ScreenSpaceDrawQuad . Centre ) ) ;
AddStep ( "much move" , ( ) = > moveFunction = Scheduler . AddDelayed ( ( ) = >
InputManager . MoveMouseTo ( InputManager . CurrentState . Mouse . Position + new Vector2 ( - 1 , 0 ) ) , 10 , true ) ) ;
AddWaitStep ( "move" , 10 ) ;
AddStep ( "stop move" , ( ) = > moveFunction . Cancel ( ) ) ;
2025-01-09 13:24:12 +09:00
AddAssert ( "at least 60 frames recorded" , ( ) = > replay . Frames . Count , ( ) = > Is . GreaterThanOrEqualTo ( 60 ) ) ;
2020-03-24 16:22:54 +09:00
}
[Test]
public void TestLimitedFrameRate ( )
{
ScheduledDelegate moveFunction = null ;
2021-04-19 17:02:59 +09:00
int initialFrameCount = 0 ;
2020-03-24 16:22:54 +09:00
AddStep ( "lower rate" , ( ) = > recorder . RecordFrameRate = 2 ) ;
2021-04-19 17:02:59 +09:00
AddStep ( "count frames" , ( ) = > initialFrameCount = replay . Frames . Count ) ;
2020-03-24 16:22:54 +09:00
AddStep ( "move to center" , ( ) = > InputManager . MoveMouseTo ( recordingManager . ScreenSpaceDrawQuad . Centre ) ) ;
AddStep ( "much move" , ( ) = > moveFunction = Scheduler . AddDelayed ( ( ) = >
InputManager . MoveMouseTo ( InputManager . CurrentState . Mouse . Position + new Vector2 ( - 1 , 0 ) ) , 10 , true ) ) ;
AddWaitStep ( "move" , 10 ) ;
AddStep ( "stop move" , ( ) = > moveFunction . Cancel ( ) ) ;
2025-01-09 13:24:12 +09:00
AddAssert ( "less than 10 frames recorded" , ( ) = > replay . Frames . Count - initialFrameCount , ( ) = > Is . LessThan ( 10 ) ) ;
2020-03-24 16:22:54 +09:00
}
[Test]
public void TestLimitedFrameRateWithImportantFrames ( )
{
ScheduledDelegate moveFunction = null ;
AddStep ( "lower rate" , ( ) = > recorder . RecordFrameRate = 2 ) ;
AddStep ( "move to center" , ( ) = > InputManager . MoveMouseTo ( recordingManager . ScreenSpaceDrawQuad . Centre ) ) ;
AddStep ( "much move with press" , ( ) = > moveFunction = Scheduler . AddDelayed ( ( ) = >
{
InputManager . MoveMouseTo ( InputManager . CurrentState . Mouse . Position + new Vector2 ( - 1 , 0 ) ) ;
2020-11-05 06:41:56 -08:00
InputManager . Click ( MouseButton . Left ) ;
2020-03-24 16:22:54 +09:00
} , 10 , true ) ) ;
AddWaitStep ( "move" , 10 ) ;
AddStep ( "stop move" , ( ) = > moveFunction . Cancel ( ) ) ;
2025-01-09 13:24:12 +09:00
AddAssert ( "at least 60 frames recorded" , ( ) = > replay . Frames . Count , ( ) = > Is . GreaterThanOrEqualTo ( 60 ) ) ;
2020-03-23 17:33:02 +09:00
}
2020-03-23 18:50:16 +09:00
protected override void Update ( )
2020-03-23 17:33:02 +09:00
{
2020-03-23 18:50:16 +09:00
base . Update ( ) ;
2024-02-02 19:48:13 +09:00
playbackManager ? . ReplayInputHandler ? . SetFrameFromTime ( Time . Current - 100 ) ;
2020-03-23 18:50:16 +09:00
}
2020-03-23 17:33:02 +09:00
2020-10-23 14:47:08 +09:00
[TearDownSteps]
public void TearDown ( )
{
2021-12-06 16:42:24 +09:00
AddStep ( "stop recorder" , cleanUpState ) ;
}
private void cleanUpState ( )
{
// Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`.
recorder ? . RemoveAndDisposeImmediately ( ) ;
recorder = null ;
2020-10-23 14:47:08 +09:00
}
2022-07-25 05:21:27 +03:00
private Drawable createContent ( ) = > new GridContainer
{
RelativeSizeAxes = Axes . Both ,
Content = new [ ]
{
new Drawable [ ]
{
recordingManager = new TestRulesetInputManager ( TestCustomisableModRuleset . CreateTestRulesetInfo ( ) , 0 , SimultaneousBindingMode . Unique )
{
Recorder = recorder = new TestReplayRecorder ( gameplayState . Score )
{
ScreenSpaceToGamefield = pos = > recordingManager . ToLocalSpace ( pos ) ,
} ,
Child = new Container
{
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
{
new Box
{
Colour = Color4 . Brown ,
RelativeSizeAxes = Axes . Both ,
} ,
new OsuSpriteText
{
Text = "Recording" ,
Scale = new Vector2 ( 3 ) ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
} ,
new TestInputConsumer ( )
}
} ,
}
} ,
new Drawable [ ]
{
playbackManager = new TestRulesetInputManager ( TestCustomisableModRuleset . CreateTestRulesetInfo ( ) , 0 , SimultaneousBindingMode . Unique )
{
ReplayInputHandler = new TestFramedReplayInputHandler ( replay )
{
GamefieldToScreenSpace = pos = > playbackManager . ToScreenSpace ( pos ) ,
} ,
Child = new Container
{
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
{
new Box
{
Colour = Color4 . DarkBlue ,
RelativeSizeAxes = Axes . Both ,
} ,
new OsuSpriteText
{
Text = "Playback" ,
Scale = new Vector2 ( 3 ) ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
} ,
new TestInputConsumer ( )
}
} ,
}
}
}
} ;
2020-03-24 15:54:04 +09:00
public class TestFramedReplayInputHandler : FramedReplayInputHandler < TestReplayFrame >
2020-03-23 18:50:16 +09:00
{
2020-03-24 15:54:04 +09:00
public TestFramedReplayInputHandler ( Replay replay )
: base ( replay )
{
}
2020-03-23 18:50:16 +09:00
2022-01-31 18:37:51 +09:00
protected override void CollectReplayInputs ( List < IInput > inputs )
2020-03-23 17:33:02 +09:00
{
2020-07-19 11:04:33 +09:00
inputs . Add ( new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace ( CurrentFrame ? . Position ? ? Vector2 . Zero ) } ) ;
inputs . Add ( new ReplayState < TestAction > { PressedActions = CurrentFrame ? . Actions ? ? new List < TestAction > ( ) } ) ;
2020-03-24 15:54:04 +09:00
}
2020-03-23 18:50:16 +09:00
}
2020-03-23 17:33:02 +09:00
2020-03-24 15:54:04 +09:00
public partial class TestInputConsumer : CompositeDrawable , IKeyBindingHandler < TestAction >
2020-03-23 18:50:16 +09:00
{
2023-10-17 17:40:44 +09:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = > Parent ! . ReceivePositionalInputAt ( screenSpacePos ) ;
2020-03-23 17:33:02 +09:00
2020-03-24 15:54:04 +09:00
private readonly Box box ;
2020-03-23 17:33:02 +09:00
2020-03-24 15:54:04 +09:00
public TestInputConsumer ( )
2020-03-23 17:33:02 +09:00
{
2020-03-24 15:54:04 +09:00
Size = new Vector2 ( 30 ) ;
Origin = Anchor . Centre ;
InternalChildren = new Drawable [ ]
2020-03-23 18:50:16 +09:00
{
2020-03-24 15:54:04 +09:00
box = new Box
{
Colour = Color4 . Black ,
RelativeSizeAxes = Axes . Both ,
} ,
} ;
}
2020-03-23 17:33:02 +09:00
2020-03-24 15:54:04 +09:00
protected override bool OnMouseMove ( MouseMoveEvent e )
{
Position = e . MousePosition ;
return base . OnMouseMove ( e ) ;
}
2020-03-23 18:50:16 +09:00
2021-09-16 18:26:12 +09:00
public bool OnPressed ( KeyBindingPressEvent < TestAction > e )
2020-03-24 15:54:04 +09:00
{
2021-11-18 12:36:52 +09:00
if ( e . Repeat )
return false ;
2020-03-24 15:54:04 +09:00
box . Colour = Color4 . White ;
return true ;
}
2020-03-23 18:50:16 +09:00
2021-09-16 18:26:12 +09:00
public void OnReleased ( KeyBindingReleaseEvent < TestAction > e )
2020-03-24 15:54:04 +09:00
{
box . Colour = Color4 . Black ;
}
2020-03-23 18:50:16 +09:00
}
2020-03-24 15:54:04 +09:00
public partial class TestRulesetInputManager : RulesetInputManager < TestAction >
2020-03-23 18:50:16 +09:00
{
2020-03-24 15:54:04 +09:00
public TestRulesetInputManager ( RulesetInfo ruleset , int variant , SimultaneousBindingMode unique )
: base ( ruleset , variant , unique )
{
}
2020-03-23 17:33:02 +09:00
2020-03-24 15:54:04 +09:00
protected override KeyBindingContainer < TestAction > CreateKeyBindingContainer ( RulesetInfo ruleset , int variant , SimultaneousBindingMode unique )
= > new TestKeyBindingContainer ( ) ;
2020-03-23 17:33:02 +09:00
2020-03-24 15:54:04 +09:00
internal partial class TestKeyBindingContainer : KeyBindingContainer < TestAction >
2020-03-23 17:33:02 +09:00
{
2021-01-15 13:41:35 +09:00
public override IEnumerable < IKeyBinding > DefaultKeyBindings = > new [ ]
2020-03-24 15:54:04 +09:00
{
new KeyBinding ( InputKey . MouseLeft , TestAction . Down ) ,
} ;
}
2020-03-23 18:50:16 +09:00
}
2020-03-24 15:54:04 +09:00
public class TestReplayFrame : ReplayFrame
{
public Vector2 Position ;
2020-03-23 18:50:16 +09:00
2020-03-24 15:54:04 +09:00
public List < TestAction > Actions = new List < TestAction > ( ) ;
2020-03-23 18:50:16 +09:00
2020-03-24 15:54:04 +09:00
public TestReplayFrame ( double time , Vector2 position , params TestAction [ ] actions )
: base ( time )
{
Position = position ;
Actions . AddRange ( actions ) ;
}
2020-03-23 18:50:16 +09:00
}
2020-03-24 15:54:04 +09:00
public enum TestAction
2020-03-23 18:50:16 +09:00
{
2020-03-24 15:54:04 +09:00
Down ,
2020-03-23 18:50:16 +09:00
}
2020-03-24 15:54:04 +09:00
internal partial class TestReplayRecorder : ReplayRecorder < TestAction >
{
2020-12-14 16:52:14 +09:00
public TestReplayRecorder ( Score target )
2020-03-24 15:54:04 +09:00
: base ( target )
{
}
2020-03-24 16:27:33 +09:00
protected override ReplayFrame HandleFrame ( Vector2 mousePosition , List < TestAction > actions , ReplayFrame previousFrame )
2020-10-22 19:31:56 +09:00
= > new TestReplayFrame ( Time . Current , mousePosition , actions . ToArray ( ) ) ;
2020-03-24 15:54:04 +09:00
}
2020-03-23 18:50:16 +09:00
}
2020-03-23 17:33:02 +09:00
}