2020-03-23 16:33:02 +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
2022-07-25 10:21:27 +08:00
using System ;
2020-03-23 16:33:02 +08:00
using System.Collections.Generic ;
2020-03-24 15:22:54 +08:00
using System.Linq ;
using NUnit.Framework ;
2020-03-23 16:33:02 +08: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 17:50:16 +08:00
using osu.Framework.Input.StateChanges ;
2020-03-24 15:22:54 +08:00
using osu.Framework.Testing ;
using osu.Framework.Threading ;
2025-01-09 12:24:12 +08:00
using osu.Framework.Timing ;
2020-03-23 17:50:16 +08:00
using osu.Game.Graphics.Sprites ;
using osu.Game.Replays ;
2020-03-23 16:33:02 +08:00
using osu.Game.Rulesets ;
2021-10-02 01:22:23 +08:00
using osu.Game.Rulesets.Osu ;
2020-03-23 17:50:16 +08:00
using osu.Game.Rulesets.Replays ;
2020-03-23 16:33:02 +08:00
using osu.Game.Rulesets.UI ;
2020-12-14 15:52:14 +08:00
using osu.Game.Scoring ;
2020-10-27 07:05:03 +08:00
using osu.Game.Screens.Play ;
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 ;
2020-03-23 16:33:02 +08:00
using osuTK ;
using osuTK.Graphics ;
2020-03-24 15:22:54 +08:00
using osuTK.Input ;
2020-03-23 16:33:02 +08:00
2020-03-30 08:59:52 +08:00
namespace osu.Game.Tests.Visual.Gameplay
2020-03-23 16:33:02 +08:00
{
2020-03-24 15:22:54 +08:00
public partial class TestSceneReplayRecorder : OsuManualInputManagerTestScene
2020-03-23 16:33:02 +08:00
{
2020-03-24 15:22:54 +08:00
private TestRulesetInputManager playbackManager ;
private TestRulesetInputManager recordingManager ;
2020-03-23 17:50:16 +08:00
2020-03-24 15:22:54 +08:00
private Replay replay ;
private TestReplayRecorder recorder ;
2022-07-25 10:21:27 +08:00
private GameplayState gameplayState ;
2020-10-27 07:05:03 +08:00
2025-01-09 12:24:12 +08:00
private Drawable content ;
2021-12-06 15:42:24 +08:00
[SetUpSteps]
public void SetUpSteps ( )
2020-03-23 16:33:02 +08:00
{
2021-12-06 15:42:24 +08:00
AddStep ( "Reset recorder state" , cleanUpState ) ;
2020-03-23 17:50:16 +08:00
2021-12-06 15:42:24 +08:00
AddStep ( "Setup containers" , ( ) = >
2020-03-23 16:33:02 +08:00
{
2021-12-06 15:42:24 +08:00
replay = new Replay ( ) ;
2022-07-25 10:21:27 +08:00
gameplayState = TestGameplayState . Create ( new OsuRuleset ( ) ) ;
gameplayState . Score . Replay = replay ;
Child = new DependencyProvidingContainer
2020-03-23 16:33:02 +08:00
{
2021-12-06 15:42:24 +08:00
RelativeSizeAxes = Axes . Both ,
2022-07-25 10:21:27 +08:00
CachedDependencies = new ( Type , object ) [ ] { ( typeof ( GameplayState ) , gameplayState ) } ,
2025-01-09 12:24:12 +08:00
Child = content = createContent ( ) ,
2022-07-25 10:21:27 +08:00
} ;
2020-03-23 16:33:02 +08:00
} ) ;
2021-12-06 15:42:24 +08:00
}
2020-03-24 15:22:54 +08:00
[Test]
public void TestBasic ( )
{
AddStep ( "move to center" , ( ) = > InputManager . MoveMouseTo ( recordingManager . ScreenSpaceDrawQuad . Centre ) ) ;
2025-01-09 12:24:12 +08:00
AddUntilStep ( "at least one frame recorded" , ( ) = > replay . Frames . Count , ( ) = > Is . GreaterThanOrEqualTo ( 0 ) ) ;
2021-04-19 16:02:59 +08:00
AddUntilStep ( "position matches" , ( ) = > playbackManager . ChildrenOfType < Box > ( ) . First ( ) . Position = = recordingManager . ChildrenOfType < Box > ( ) . First ( ) . Position ) ;
2020-03-24 15:22:54 +08:00
}
2025-01-09 12:24:12 +08:00
[Test]
2025-01-09 12:33:55 +08: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 12:24:12 +08: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 15:22:54 +08: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 12:24:12 +08:00
AddAssert ( "at least 60 frames recorded" , ( ) = > replay . Frames . Count , ( ) = > Is . GreaterThanOrEqualTo ( 60 ) ) ;
2020-03-24 15:22:54 +08:00
}
[Test]
public void TestLimitedFrameRate ( )
{
ScheduledDelegate moveFunction = null ;
2021-04-19 16:02:59 +08:00
int initialFrameCount = 0 ;
2020-03-24 15:22:54 +08:00
AddStep ( "lower rate" , ( ) = > recorder . RecordFrameRate = 2 ) ;
2021-04-19 16:02:59 +08:00
AddStep ( "count frames" , ( ) = > initialFrameCount = replay . Frames . Count ) ;
2020-03-24 15:22:54 +08: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 12:24:12 +08:00
AddAssert ( "less than 10 frames recorded" , ( ) = > replay . Frames . Count - initialFrameCount , ( ) = > Is . LessThan ( 10 ) ) ;
2020-03-24 15:22:54 +08: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 22:41:56 +08:00
InputManager . Click ( MouseButton . Left ) ;
2020-03-24 15:22:54 +08:00
} , 10 , true ) ) ;
AddWaitStep ( "move" , 10 ) ;
AddStep ( "stop move" , ( ) = > moveFunction . Cancel ( ) ) ;
2025-01-09 12:24:12 +08:00
AddAssert ( "at least 60 frames recorded" , ( ) = > replay . Frames . Count , ( ) = > Is . GreaterThanOrEqualTo ( 60 ) ) ;
2020-03-23 16:33:02 +08:00
}
2020-03-23 17:50:16 +08:00
protected override void Update ( )
2020-03-23 16:33:02 +08:00
{
2020-03-23 17:50:16 +08:00
base . Update ( ) ;
2024-02-02 18:48:13 +08:00
playbackManager ? . ReplayInputHandler ? . SetFrameFromTime ( Time . Current - 100 ) ;
2020-03-23 17:50:16 +08:00
}
2020-03-23 16:33:02 +08:00
2020-10-23 13:47:08 +08:00
[TearDownSteps]
public void TearDown ( )
{
2021-12-06 15:42:24 +08: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 13:47:08 +08:00
}
2022-07-25 10:21:27 +08: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 14:54:04 +08:00
public class TestFramedReplayInputHandler : FramedReplayInputHandler < TestReplayFrame >
2020-03-23 17:50:16 +08:00
{
2020-03-24 14:54:04 +08:00
public TestFramedReplayInputHandler ( Replay replay )
: base ( replay )
{
}
2020-03-23 17:50:16 +08:00
2022-01-31 17:37:51 +08:00
protected override void CollectReplayInputs ( List < IInput > inputs )
2020-03-23 16:33:02 +08:00
{
2020-07-19 10:04:33 +08: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 14:54:04 +08:00
}
2020-03-23 17:50:16 +08:00
}
2020-03-23 16:33:02 +08:00
2020-03-24 14:54:04 +08:00
public partial class TestInputConsumer : CompositeDrawable , IKeyBindingHandler < TestAction >
2020-03-23 17:50:16 +08:00
{
2023-10-17 16:40:44 +08:00
public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) = > Parent ! . ReceivePositionalInputAt ( screenSpacePos ) ;
2020-03-23 16:33:02 +08:00
2020-03-24 14:54:04 +08:00
private readonly Box box ;
2020-03-23 16:33:02 +08:00
2020-03-24 14:54:04 +08:00
public TestInputConsumer ( )
2020-03-23 16:33:02 +08:00
{
2020-03-24 14:54:04 +08:00
Size = new Vector2 ( 30 ) ;
Origin = Anchor . Centre ;
InternalChildren = new Drawable [ ]
2020-03-23 17:50:16 +08:00
{
2020-03-24 14:54:04 +08:00
box = new Box
{
Colour = Color4 . Black ,
RelativeSizeAxes = Axes . Both ,
} ,
} ;
}
2020-03-23 16:33:02 +08:00
2020-03-24 14:54:04 +08:00
protected override bool OnMouseMove ( MouseMoveEvent e )
{
Position = e . MousePosition ;
return base . OnMouseMove ( e ) ;
}
2020-03-23 17:50:16 +08:00
2021-09-16 17:26:12 +08:00
public bool OnPressed ( KeyBindingPressEvent < TestAction > e )
2020-03-24 14:54:04 +08:00
{
2021-11-18 11:36:52 +08:00
if ( e . Repeat )
return false ;
2020-03-24 14:54:04 +08:00
box . Colour = Color4 . White ;
return true ;
}
2020-03-23 17:50:16 +08:00
2021-09-16 17:26:12 +08:00
public void OnReleased ( KeyBindingReleaseEvent < TestAction > e )
2020-03-24 14:54:04 +08:00
{
box . Colour = Color4 . Black ;
}
2020-03-23 17:50:16 +08:00
}
2020-03-24 14:54:04 +08:00
public partial class TestRulesetInputManager : RulesetInputManager < TestAction >
2020-03-23 17:50:16 +08:00
{
2020-03-24 14:54:04 +08:00
public TestRulesetInputManager ( RulesetInfo ruleset , int variant , SimultaneousBindingMode unique )
: base ( ruleset , variant , unique )
{
}
2020-03-23 16:33:02 +08:00
2020-03-24 14:54:04 +08:00
protected override KeyBindingContainer < TestAction > CreateKeyBindingContainer ( RulesetInfo ruleset , int variant , SimultaneousBindingMode unique )
= > new TestKeyBindingContainer ( ) ;
2020-03-23 16:33:02 +08:00
2020-03-24 14:54:04 +08:00
internal partial class TestKeyBindingContainer : KeyBindingContainer < TestAction >
2020-03-23 16:33:02 +08:00
{
2021-01-15 12:41:35 +08:00
public override IEnumerable < IKeyBinding > DefaultKeyBindings = > new [ ]
2020-03-24 14:54:04 +08:00
{
new KeyBinding ( InputKey . MouseLeft , TestAction . Down ) ,
} ;
}
2020-03-23 17:50:16 +08:00
}
2020-03-24 14:54:04 +08:00
public class TestReplayFrame : ReplayFrame
{
public Vector2 Position ;
2020-03-23 17:50:16 +08:00
2020-03-24 14:54:04 +08:00
public List < TestAction > Actions = new List < TestAction > ( ) ;
2020-03-23 17:50:16 +08:00
2020-03-24 14:54:04 +08:00
public TestReplayFrame ( double time , Vector2 position , params TestAction [ ] actions )
: base ( time )
{
Position = position ;
Actions . AddRange ( actions ) ;
}
2020-03-23 17:50:16 +08:00
}
2020-03-24 14:54:04 +08:00
public enum TestAction
2020-03-23 17:50:16 +08:00
{
2020-03-24 14:54:04 +08:00
Down ,
2020-03-23 17:50:16 +08:00
}
2020-03-24 14:54:04 +08:00
internal partial class TestReplayRecorder : ReplayRecorder < TestAction >
{
2020-12-14 15:52:14 +08:00
public TestReplayRecorder ( Score target )
2020-03-24 14:54:04 +08:00
: base ( target )
{
}
2020-03-24 15:27:33 +08:00
protected override ReplayFrame HandleFrame ( Vector2 mousePosition , List < TestAction > actions , ReplayFrame previousFrame )
2020-10-22 18:31:56 +08:00
= > new TestReplayFrame ( Time . Current , mousePosition , actions . ToArray ( ) ) ;
2020-03-24 14:54:04 +08:00
}
2020-03-23 17:50:16 +08:00
}
2020-03-23 16:33:02 +08:00
}