2019-08-26 07:31:46 +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.
using System ;
using System.Collections.Generic ;
using System.Linq ;
2020-09-17 08:40:05 +00:00
using System.Threading ;
2019-08-26 07:31:46 +00:00
using NUnit.Framework ;
2019-09-24 07:49:42 +00:00
using osu.Framework.Bindables ;
2019-08-26 07:31:46 +00:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Graphics.Shapes ;
using osu.Framework.Input ;
2020-01-09 04:43:44 +00:00
using osu.Framework.Utils ;
2019-08-26 07:31:46 +00:00
using osu.Framework.Timing ;
using osu.Game.Beatmaps ;
using osu.Game.Beatmaps.ControlPoints ;
using osu.Game.Configuration ;
using osu.Game.Rulesets ;
using osu.Game.Rulesets.Difficulty ;
using osu.Game.Rulesets.Mods ;
using osu.Game.Rulesets.Objects ;
using osu.Game.Rulesets.Objects.Drawables ;
using osu.Game.Rulesets.Objects.Types ;
using osu.Game.Rulesets.Osu ;
using osu.Game.Rulesets.UI ;
using osu.Game.Rulesets.UI.Scrolling ;
using osuTK ;
using osuTK.Graphics ;
2020-11-24 03:50:37 +00:00
using JetBrains.Annotations ;
2019-08-26 07:31:46 +00:00
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneDrawableScrollingRuleset : OsuTestScene
{
/// <summary>
/// The amount of time visible by the "view window" of the playfield.
/// All hitobjects added through <see cref="createBeatmap"/> are spaced apart by this value, such that for a beat length of 1000,
/// there will be at most 2 hitobjects visible in the "view window".
/// </summary>
private const double time_range = 1000 ;
private readonly ManualClock testClock = new ManualClock ( ) ;
private TestDrawableScrollingRuleset drawableRuleset ;
[SetUp]
public void Setup ( ) = > Schedule ( ( ) = > testClock . CurrentTime = 0 ) ;
2020-11-24 03:50:37 +00:00
[Test]
public void TestHitObjectPooling ( )
{
var beatmap = createBeatmap ( _ = > new TestPooledHitObject ( ) ) ;
beatmap . ControlPointInfo . Add ( 0 , new TimingControlPoint { BeatLength = time_range } ) ;
createTest ( beatmap ) ;
assertPosition ( 0 , 0f ) ;
assertDead ( 3 ) ;
setTime ( 3 * time_range ) ;
assertPosition ( 3 , 0f ) ;
assertDead ( 0 ) ;
setTime ( 0 * time_range ) ;
assertPosition ( 0 , 0f ) ;
assertDead ( 3 ) ;
}
[TestCase("pooled")]
[TestCase("non-pooled")]
public void TestNestedHitObject ( string pooled )
{
var beatmap = createBeatmap ( i = >
{
var h = pooled = = "pooled" ? new TestPooledParentHitObject ( ) : new TestParentHitObject ( ) ;
h . Duration = 300 ;
h . ChildTimeOffset = i % 3 * 100 ;
return h ;
} ) ;
beatmap . ControlPointInfo . Add ( 0 , new TimingControlPoint { BeatLength = time_range } ) ;
createTest ( beatmap ) ;
assertPosition ( 0 , 0f ) ;
assertHeight ( 0 ) ;
assertChildPosition ( 0 ) ;
setTime ( 5 * time_range ) ;
assertPosition ( 5 , 0f ) ;
assertHeight ( 5 ) ;
assertChildPosition ( 5 ) ;
}
private void assertDead ( int index ) = > AddAssert ( $"hitobject {index} is dead" , ( ) = > getDrawableHitObject ( index ) = = null ) ;
private void assertHeight ( int index ) = > AddAssert ( $"hitobject {index} height" , ( ) = >
{
var d = getDrawableHitObject ( index ) ;
return d ! = null & & Precision . AlmostEquals ( d . DrawHeight , yScale * ( float ) ( d . HitObject . Duration / time_range ) , 0.1f ) ;
} ) ;
private void assertChildPosition ( int index ) = > AddAssert ( $"hitobject {index} child position" , ( ) = >
{
var d = getDrawableHitObject ( index ) ;
return d is DrawableTestParentHitObject & & Precision . AlmostEquals (
d . NestedHitObjects . First ( ) . DrawPosition . Y ,
yScale * ( float ) ( ( TestParentHitObject ) d . HitObject ) . ChildTimeOffset / time_range , 0.1f ) ;
} ) ;
2019-08-26 07:31:46 +00:00
[Test]
public void TestRelativeBeatLengthScaleSingleTimingPoint ( )
{
2019-10-25 10:48:01 +00:00
var beatmap = createBeatmap ( ) ;
beatmap . ControlPointInfo . Add ( 0 , new TimingControlPoint { BeatLength = time_range / 2 } ) ;
2019-08-26 07:31:46 +00:00
createTest ( beatmap , d = > d . RelativeScaleBeatLengthsOverride = true ) ;
assertPosition ( 0 , 0f ) ;
// The single timing point is 1x speed relative to itself, such that the hitobject occurring time_range milliseconds later should appear
// at the bottom of the view window regardless of the timing point's beat length
assertPosition ( 1 , 1f ) ;
}
[Test]
public void TestRelativeBeatLengthScaleTimingPointBeyondEndDoesNotBecomeDominant ( )
{
2019-10-25 10:48:01 +00:00
var beatmap = createBeatmap ( ) ;
beatmap . ControlPointInfo . Add ( 0 , new TimingControlPoint { BeatLength = time_range / 2 } ) ;
beatmap . ControlPointInfo . Add ( 12000 , new TimingControlPoint { BeatLength = time_range } ) ;
beatmap . ControlPointInfo . Add ( 100000 , new TimingControlPoint { BeatLength = time_range } ) ;
2019-08-26 07:31:46 +00:00
createTest ( beatmap , d = > d . RelativeScaleBeatLengthsOverride = true ) ;
assertPosition ( 0 , 0f ) ;
assertPosition ( 1 , 1f ) ;
}
[Test]
public void TestRelativeBeatLengthScaleFromSecondTimingPoint ( )
{
2019-10-25 10:48:01 +00:00
var beatmap = createBeatmap ( ) ;
beatmap . ControlPointInfo . Add ( 0 , new TimingControlPoint { BeatLength = time_range } ) ;
beatmap . ControlPointInfo . Add ( 3 * time_range , new TimingControlPoint { BeatLength = time_range / 2 } ) ;
2019-08-26 07:31:46 +00:00
createTest ( beatmap , d = > d . RelativeScaleBeatLengthsOverride = true ) ;
// The first timing point should have a relative velocity of 2
assertPosition ( 0 , 0f ) ;
assertPosition ( 1 , 0.5f ) ;
assertPosition ( 2 , 1f ) ;
// Move to the second timing point
setTime ( 3 * time_range ) ;
assertPosition ( 3 , 0f ) ;
// As above, this is the timing point that is 1x speed relative to itself, so the hitobject occurring time_range milliseconds later should be at the bottom of the view window
assertPosition ( 4 , 1f ) ;
}
[Test]
public void TestNonRelativeScale ( )
{
2019-10-25 10:48:01 +00:00
var beatmap = createBeatmap ( ) ;
beatmap . ControlPointInfo . Add ( 0 , new TimingControlPoint { BeatLength = time_range } ) ;
beatmap . ControlPointInfo . Add ( 3 * time_range , new TimingControlPoint { BeatLength = time_range / 2 } ) ;
2019-08-26 07:31:46 +00:00
createTest ( beatmap ) ;
assertPosition ( 0 , 0f ) ;
assertPosition ( 1 , 1 ) ;
// Move to the second timing point
setTime ( 3 * time_range ) ;
assertPosition ( 3 , 0f ) ;
// For a beat length of 500, the view window of this timing point is elongated 2x (1000 / 500), such that the second hitobject is two TimeRanges away (offscreen)
// To bring it on-screen, half TimeRange is added to the current time, bringing the second half of the view window into view, and the hitobject should appear at the bottom
setTime ( 3 * time_range + time_range / 2 ) ;
assertPosition ( 4 , 1f ) ;
}
2019-09-24 07:49:42 +00:00
[Test]
2019-09-25 11:03:03 +00:00
public void TestSliderMultiplierDoesNotAffectRelativeBeatLength ( )
2019-09-24 07:49:42 +00:00
{
2019-10-25 10:48:01 +00:00
var beatmap = createBeatmap ( ) ;
beatmap . ControlPointInfo . Add ( 0 , new TimingControlPoint { BeatLength = time_range } ) ;
2019-09-24 07:49:42 +00:00
beatmap . BeatmapInfo . BaseDifficulty . SliderMultiplier = 2 ;
createTest ( beatmap , d = > d . RelativeScaleBeatLengthsOverride = true ) ;
AddStep ( "adjust time range" , ( ) = > drawableRuleset . TimeRange . Value = 5000 ) ;
for ( int i = 0 ; i < 5 ; i + + )
assertPosition ( i , i / 5f ) ;
}
2019-09-25 11:12:01 +00:00
[Test]
public void TestSliderMultiplierAffectsNonRelativeBeatLength ( )
{
2019-10-25 10:48:01 +00:00
var beatmap = createBeatmap ( ) ;
beatmap . ControlPointInfo . Add ( 0 , new TimingControlPoint { BeatLength = time_range } ) ;
2019-09-25 11:12:01 +00:00
beatmap . BeatmapInfo . BaseDifficulty . SliderMultiplier = 2 ;
createTest ( beatmap ) ;
AddStep ( "adjust time range" , ( ) = > drawableRuleset . TimeRange . Value = 2000 ) ;
assertPosition ( 0 , 0 ) ;
assertPosition ( 1 , 1 ) ;
}
2020-11-24 03:50:37 +00:00
/// <summary>
/// Get a <see cref="DrawableTestHitObject" /> corresponding to the <paramref name="index"/>'th <see cref="TestHitObject"/>.
/// When a pooling is used and the hit object is not alive, `null` is returned.
/// </summary>
[CanBeNull]
private DrawableTestHitObject getDrawableHitObject ( int index )
{
var hitObject = drawableRuleset . Beatmap . HitObjects . ElementAt ( index ) ;
return ( DrawableTestHitObject ) drawableRuleset . Playfield . HitObjectContainer . Objects . FirstOrDefault ( obj = > obj . HitObject = = hitObject ) ;
}
private float yScale = > drawableRuleset . Playfield . HitObjectContainer . DrawHeight ;
2019-08-26 07:31:46 +00:00
private void assertPosition ( int index , float relativeY ) = > AddAssert ( $"hitobject {index} at {relativeY}" ,
2020-11-24 03:50:37 +00:00
( ) = > Precision . AlmostEquals ( getDrawableHitObject ( index ) ? . DrawPosition . Y ? ? - 1 , yScale * relativeY ) ) ;
2019-08-26 07:31:46 +00:00
private void setTime ( double time )
{
AddStep ( $"set time = {time}" , ( ) = > testClock . CurrentTime = time ) ;
}
/// <summary>
/// Creates an <see cref="IBeatmap"/>, containing 10 hitobjects and user-provided timing points.
/// The hitobjects are spaced <see cref="time_range"/> milliseconds apart.
/// </summary>
/// <returns>The <see cref="IBeatmap"/>.</returns>
2020-11-24 03:50:37 +00:00
private IBeatmap createBeatmap ( Func < int , TestHitObject > createAction = null )
2019-08-26 07:31:46 +00:00
{
2020-11-24 03:50:37 +00:00
var beatmap = new Beatmap < TestHitObject > { BeatmapInfo = { Ruleset = new OsuRuleset ( ) . RulesetInfo } } ;
2019-08-26 07:31:46 +00:00
for ( int i = 0 ; i < 10 ; i + + )
2020-11-24 03:50:37 +00:00
{
var h = createAction ? . Invoke ( i ) ? ? new TestHitObject ( ) ;
h . StartTime = i * time_range ;
beatmap . HitObjects . Add ( h ) ;
}
2019-08-26 07:31:46 +00:00
return beatmap ;
}
private void createTest ( IBeatmap beatmap , Action < TestDrawableScrollingRuleset > overrideAction = null ) = > AddStep ( "create test" , ( ) = >
{
var ruleset = new TestScrollingRuleset ( ) ;
2019-12-12 07:48:33 +00:00
drawableRuleset = ( TestDrawableScrollingRuleset ) ruleset . CreateDrawableRulesetWith ( CreateWorkingBeatmap ( beatmap ) . GetPlayableBeatmap ( ruleset . RulesetInfo ) ) ;
2019-08-26 07:31:46 +00:00
drawableRuleset . FrameStablePlayback = false ;
overrideAction ? . Invoke ( drawableRuleset ) ;
Child = new Container
{
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . Y ,
Height = 0.75f ,
Width = 400 ,
Masking = true ,
Clock = new FramedClock ( testClock ) ,
Child = drawableRuleset
} ;
} ) ;
#region Ruleset
private class TestScrollingRuleset : Ruleset
{
public override IEnumerable < Mod > GetModsFor ( ModType type ) = > throw new NotImplementedException ( ) ;
2019-12-12 06:58:11 +00:00
public override DrawableRuleset CreateDrawableRulesetWith ( IBeatmap beatmap , IReadOnlyList < Mod > mods = null ) = > new TestDrawableScrollingRuleset ( this , beatmap , mods ) ;
2019-08-26 07:31:46 +00:00
2019-12-24 07:02:16 +00:00
public override IBeatmapConverter CreateBeatmapConverter ( IBeatmap beatmap ) = > new TestBeatmapConverter ( beatmap , null ) ;
2019-08-26 07:31:46 +00:00
public override DifficultyCalculator CreateDifficultyCalculator ( WorkingBeatmap beatmap ) = > throw new NotImplementedException ( ) ;
public override string Description { get ; } = string . Empty ;
public override string ShortName { get ; } = string . Empty ;
}
private class TestDrawableScrollingRuleset : DrawableScrollingRuleset < TestHitObject >
{
public bool RelativeScaleBeatLengthsOverride { get ; set ; }
protected override bool RelativeScaleBeatLengths = > RelativeScaleBeatLengthsOverride ;
protected override ScrollVisualisationMethod VisualisationMethod = > ScrollVisualisationMethod . Overlapping ;
2019-09-24 07:49:42 +00:00
public new Bindable < double > TimeRange = > base . TimeRange ;
2019-12-12 06:58:11 +00:00
public TestDrawableScrollingRuleset ( Ruleset ruleset , IBeatmap beatmap , IReadOnlyList < Mod > mods = null )
2019-08-26 07:31:46 +00:00
: base ( ruleset , beatmap , mods )
{
TimeRange . Value = time_range ;
}
2020-11-24 03:50:37 +00:00
public override DrawableHitObject < TestHitObject > CreateDrawableRepresentation ( TestHitObject h ) = >
h switch
{
TestPooledHitObject _ = > null ,
TestPooledParentHitObject _ = > null ,
TestParentHitObject p = > new DrawableTestParentHitObject ( p ) ,
_ = > new DrawableTestHitObject ( h ) ,
} ;
2019-08-26 07:31:46 +00:00
protected override PassThroughInputManager CreateInputManager ( ) = > new PassThroughInputManager ( ) ;
protected override Playfield CreatePlayfield ( ) = > new TestPlayfield ( ) ;
}
private class TestPlayfield : ScrollingPlayfield
{
public TestPlayfield ( )
{
AddInternal ( new Container
{
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
{
new Box
{
RelativeSizeAxes = Axes . Both ,
Alpha = 0.2f ,
} ,
new Container
{
RelativeSizeAxes = Axes . Both ,
Padding = new MarginPadding { Top = 150 } ,
Children = new Drawable [ ]
{
new Box
{
Anchor = Anchor . TopCentre ,
Origin = Anchor . Centre ,
RelativeSizeAxes = Axes . X ,
Height = 2 ,
Colour = Color4 . Green
} ,
HitObjectContainer
}
}
}
} ) ;
2020-11-24 03:50:37 +00:00
RegisterPool < TestPooledHitObject , DrawableTestPooledHitObject > ( 1 ) ;
RegisterPool < TestPooledParentHitObject , DrawableTestPooledParentHitObject > ( 1 ) ;
2019-08-26 07:31:46 +00:00
}
}
private class TestBeatmapConverter : BeatmapConverter < TestHitObject >
{
2019-12-24 07:02:16 +00:00
public TestBeatmapConverter ( IBeatmap beatmap , Ruleset ruleset )
: base ( beatmap , ruleset )
2019-08-26 07:31:46 +00:00
{
}
2019-12-23 08:44:18 +00:00
public override bool CanConvert ( ) = > true ;
2019-08-26 07:31:46 +00:00
2020-11-24 03:50:37 +00:00
protected override IEnumerable < TestHitObject > ConvertHitObject ( HitObject original , IBeatmap beatmap , CancellationToken cancellationToken ) = >
throw new NotImplementedException ( ) ;
2019-08-26 07:31:46 +00:00
}
#endregion
#region HitObject
2020-11-24 03:50:37 +00:00
private class TestHitObject : HitObject , IHasDuration
2019-08-26 07:31:46 +00:00
{
2020-05-27 03:37:44 +00:00
public double EndTime = > StartTime + Duration ;
2019-08-26 07:31:46 +00:00
2020-11-24 03:50:37 +00:00
public double Duration { get ; set ; } = 100 ;
}
private class TestPooledHitObject : TestHitObject
{
}
private class TestParentHitObject : TestHitObject
{
public double ChildTimeOffset ;
protected override void CreateNestedHitObjects ( CancellationToken cancellationToken )
{
AddNested ( new TestHitObject { StartTime = StartTime + ChildTimeOffset } ) ;
}
}
private class TestPooledParentHitObject : TestParentHitObject
{
protected override void CreateNestedHitObjects ( CancellationToken cancellationToken )
{
AddNested ( new TestPooledHitObject { StartTime = StartTime + ChildTimeOffset } ) ;
}
2019-08-26 07:31:46 +00:00
}
private class DrawableTestHitObject : DrawableHitObject < TestHitObject >
{
2020-11-24 03:50:37 +00:00
public DrawableTestHitObject ( [ CanBeNull ] TestHitObject hitObject )
2019-08-26 07:31:46 +00:00
: base ( hitObject )
{
Anchor = Anchor . TopCentre ;
Origin = Anchor . TopCentre ;
Size = new Vector2 ( 100 , 25 ) ;
AddRangeInternal ( new Drawable [ ]
{
new Box
{
RelativeSizeAxes = Axes . Both ,
Colour = Color4 . LightPink
} ,
new Box
{
Origin = Anchor . CentreLeft ,
RelativeSizeAxes = Axes . X ,
Height = 2 ,
Colour = Color4 . Red
}
} ) ;
}
}
2020-11-24 03:50:37 +00:00
private class DrawableTestPooledHitObject : DrawableTestHitObject
{
public DrawableTestPooledHitObject ( )
: base ( null )
{
InternalChildren [ 0 ] . Colour = Color4 . LightSkyBlue ;
InternalChildren [ 1 ] . Colour = Color4 . Blue ;
}
protected override void Update ( ) = > LifetimeEnd = HitObject . EndTime ;
}
private class DrawableTestParentHitObject : DrawableTestHitObject
{
private readonly Container < DrawableHitObject > container ;
public DrawableTestParentHitObject ( [ CanBeNull ] TestHitObject hitObject )
2020-11-24 04:57:20 +00:00
: base ( hitObject )
2020-11-24 03:50:37 +00:00
{
InternalChildren [ 0 ] . Colour = Color4 . LightYellow ;
InternalChildren [ 1 ] . Colour = Color4 . Yellow ;
AddInternal ( container = new Container < DrawableHitObject >
{
RelativeSizeAxes = Axes . Both ,
} ) ;
}
protected override DrawableHitObject CreateNestedHitObject ( HitObject hitObject ) = >
new DrawableTestHitObject ( ( TestHitObject ) hitObject ) ;
protected override void AddNestedHitObject ( DrawableHitObject hitObject ) = > container . Add ( hitObject ) ;
protected override void ClearNestedHitObjects ( ) = > container . Clear ( false ) ;
}
private class DrawableTestPooledParentHitObject : DrawableTestParentHitObject
{
public DrawableTestPooledParentHitObject ( )
: base ( null )
{
InternalChildren [ 0 ] . Colour = Color4 . LightSeaGreen ;
InternalChildren [ 1 ] . Colour = Color4 . Green ;
}
}
2019-08-26 07:31:46 +00:00
#endregion
}
}