2021-08-25 15:55:03 +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.
2023-02-16 16:58:32 +08:00
using System.Diagnostics ;
2021-08-25 15:55:03 +08:00
using System.Linq ;
using NUnit.Framework ;
2023-02-16 16:58:32 +08:00
using osu.Framework.Allocation ;
using osu.Framework.Audio ;
using osu.Framework.Timing ;
2023-02-21 13:04:19 +08:00
using osu.Framework.Utils ;
2021-08-25 15:55:03 +08:00
using osu.Game.Audio ;
using osu.Game.Beatmaps ;
using osu.Game.Beatmaps.ControlPoints ;
2023-02-16 17:13:11 +08:00
using osu.Game.Beatmaps.Legacy ;
2021-08-25 15:55:03 +08:00
using osu.Game.Rulesets ;
using osu.Game.Rulesets.Objects ;
using osu.Game.Rulesets.Objects.Drawables ;
2023-02-16 17:13:11 +08:00
using osu.Game.Rulesets.Objects.Types ;
2021-08-25 15:55:03 +08:00
using osu.Game.Rulesets.Osu ;
using osu.Game.Rulesets.Osu.Objects ;
2023-06-20 20:02:34 +08:00
using osu.Game.Rulesets.Scoring ;
2021-08-25 15:55:03 +08:00
using osu.Game.Rulesets.UI ;
2023-02-16 16:58:32 +08:00
using osu.Game.Storyboards ;
2023-02-16 17:13:11 +08:00
using osuTK ;
2021-08-25 15:55:03 +08:00
using osuTK.Input ;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
{
2023-02-16 16:37:46 +08:00
private TestGameplaySampleTriggerSource sampleTriggerSource = null ! ;
2021-08-25 15:55:03 +08:00
protected override Ruleset CreatePlayerRuleset ( ) = > new OsuRuleset ( ) ;
2023-02-16 16:37:46 +08:00
private Beatmap beatmap = null ! ;
2021-08-25 15:55:03 +08:00
2023-02-16 16:58:32 +08:00
[Resolved]
private AudioManager audio { get ; set ; } = null ! ;
protected override WorkingBeatmap CreateWorkingBeatmap ( IBeatmap beatmap , Storyboard ? storyboard = null )
= > new ClockBackedTestWorkingBeatmap ( beatmap , storyboard , new FramedClock ( new ManualClock { Rate = 1 } ) , audio ) ;
2021-08-25 15:55:03 +08:00
protected override IBeatmap CreateBeatmap ( RulesetInfo ruleset )
{
2023-02-16 17:13:11 +08:00
ControlPointInfo controlPointInfo = new LegacyControlPointInfo ( ) ;
2021-08-25 15:55:03 +08:00
beatmap = new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
2022-01-18 21:57:39 +08:00
Difficulty = new BeatmapDifficulty { CircleSize = 6 , SliderMultiplier = 3 } ,
2021-08-25 15:55:03 +08:00
Ruleset = ruleset
2023-02-16 17:13:11 +08:00
} ,
ControlPointInfo = controlPointInfo
2021-08-25 15:55:03 +08:00
} ;
const double start_offset = 8000 ;
const double spacing = 2000 ;
2023-02-16 16:58:32 +08:00
// intentionally start objects a bit late so we can test the case of no alive objects.
2021-08-25 15:55:03 +08:00
double t = start_offset ;
2023-02-16 16:58:32 +08:00
2023-02-16 17:13:11 +08:00
beatmap . HitObjects . AddRange ( new HitObject [ ]
2021-08-25 15:55:03 +08:00
{
new HitCircle
{
2023-06-20 20:02:34 +08:00
HitWindows = new HitWindows ( ) ,
2021-08-25 15:55:03 +08:00
StartTime = t + = spacing ,
Samples = new [ ] { new HitSampleInfo ( HitSampleInfo . HIT_NORMAL ) }
} ,
new HitCircle
{
2023-06-20 20:02:34 +08:00
HitWindows = new HitWindows ( ) ,
2021-08-25 15:55:03 +08:00
StartTime = t + = spacing ,
Samples = new [ ] { new HitSampleInfo ( HitSampleInfo . HIT_WHISTLE ) }
} ,
new HitCircle
{
2023-06-20 20:02:34 +08:00
HitWindows = new HitWindows ( ) ,
2021-08-25 15:55:03 +08:00
StartTime = t + = spacing ,
2023-05-24 13:03:04 +08:00
Samples = new [ ] { new HitSampleInfo ( HitSampleInfo . HIT_NORMAL , HitSampleInfo . BANK_SOFT ) } ,
2021-08-25 15:55:03 +08:00
} ,
new HitCircle
{
2023-06-20 20:02:34 +08:00
HitWindows = new HitWindows ( ) ,
2023-02-16 17:13:11 +08:00
StartTime = t + = spacing ,
} ,
new Slider
{
2023-06-20 20:02:34 +08:00
HitWindows = new HitWindows ( ) ,
2023-02-16 17:13:11 +08:00
StartTime = t + = spacing ,
Path = new SliderPath ( PathType . Linear , new [ ] { Vector2 . Zero , Vector2 . UnitY * 200 } ) ,
2023-05-24 13:04:10 +08:00
Samples = new [ ] { new HitSampleInfo ( HitSampleInfo . HIT_WHISTLE , HitSampleInfo . BANK_SOFT ) } ,
2021-08-25 15:55:03 +08:00
} ,
} ) ;
2023-02-16 17:13:11 +08:00
// Add a change in volume halfway through final slider.
controlPointInfo . Add ( t , new SampleControlPoint
{
SampleBank = "normal" ,
SampleVolume = 20 ,
} ) ;
2021-08-25 15:55:03 +08:00
return beatmap ;
}
public override void SetUpSteps ( )
{
base . SetUpSteps ( ) ;
2023-07-06 16:33:47 +08:00
AddStep ( "Add trigger source" , ( ) = > Player . DrawableRuleset . FrameStableComponents . Add ( sampleTriggerSource = new TestGameplaySampleTriggerSource ( Player . DrawableRuleset . Playfield . HitObjectContainer ) ) ) ;
2021-08-25 15:55:03 +08:00
}
[Test]
public void TestCorrectHitObject ( )
{
2023-02-16 16:58:32 +08:00
waitForAliveObjectIndex ( null ) ;
2023-02-17 02:02:51 +08:00
checkValidObjectIndex ( 0 ) ;
2021-08-25 15:55:03 +08:00
2023-02-16 16:58:32 +08:00
seekBeforeIndex ( 0 ) ;
waitForAliveObjectIndex ( 0 ) ;
checkValidObjectIndex ( 0 ) ;
2021-08-25 15:55:03 +08:00
2023-02-16 16:58:32 +08:00
AddAssert ( "first object not hit" , ( ) = > getNextAliveObject ( ) ? . Entry ? . Result ? . HasResult ! = true ) ;
2021-08-25 15:55:03 +08:00
2023-02-16 16:58:32 +08:00
AddStep ( "hit first object" , ( ) = >
2021-08-25 15:55:03 +08:00
{
2023-02-16 16:58:32 +08:00
var next = getNextAliveObject ( ) ;
2021-08-25 15:55:03 +08:00
2023-02-16 16:58:32 +08:00
if ( next ! = null )
2021-08-25 15:55:03 +08:00
{
2023-02-16 16:58:32 +08:00
Debug . Assert ( next . Entry ? . Result ? . HasResult ! = true ) ;
2021-08-25 15:55:03 +08:00
2023-02-16 16:58:32 +08:00
InputManager . MoveMouseTo ( next . ScreenSpaceDrawQuad . Centre ) ;
InputManager . Click ( MouseButton . Left ) ;
}
2021-08-25 15:55:03 +08:00
} ) ;
2023-02-16 16:58:32 +08:00
AddAssert ( "first object hit" , ( ) = > getNextAliveObject ( ) ? . Entry ? . Result ? . HasResult = = true ) ;
2023-06-20 20:02:34 +08:00
// next object is too far away, so we still use the already hit object.
checkValidObjectIndex ( 0 ) ;
// still too far away.
seekBeforeIndex ( 1 , 400 ) ;
checkValidObjectIndex ( 0 ) ;
2021-08-25 15:55:03 +08:00
2023-02-16 16:58:32 +08:00
// Still object 1 as it's not hit yet.
seekBeforeIndex ( 1 ) ;
waitForAliveObjectIndex ( 1 ) ;
checkValidObjectIndex ( 1 ) ;
2021-08-25 15:55:03 +08:00
2023-02-16 16:58:32 +08:00
seekBeforeIndex ( 2 ) ;
waitForAliveObjectIndex ( 2 ) ;
checkValidObjectIndex ( 2 ) ;
2021-08-25 15:55:03 +08:00
2023-07-06 16:33:47 +08:00
// test rewinding
seekBeforeIndex ( 1 ) ;
waitForAliveObjectIndex ( 1 ) ;
checkValidObjectIndex ( 1 ) ;
seekBeforeIndex ( 1 , 400 ) ;
checkValidObjectIndex ( 0 ) ;
2023-02-16 16:58:32 +08:00
seekBeforeIndex ( 3 ) ;
waitForAliveObjectIndex ( 3 ) ;
checkValidObjectIndex ( 3 ) ;
2023-02-16 17:13:11 +08:00
seekBeforeIndex ( 4 ) ;
waitForAliveObjectIndex ( 4 ) ;
// Even before the object, we should prefer the first nested object's sample.
// This is because the (parent) object will only play its sample at the final EndTime.
AddAssert ( "check valid object is slider's first nested" , ( ) = > sampleTriggerSource . GetMostValidObject ( ) , ( ) = > Is . EqualTo ( beatmap . HitObjects [ 4 ] . NestedHitObjects . First ( ) ) ) ;
2021-08-25 15:55:03 +08:00
2023-02-22 13:41:20 +08:00
AddStep ( "seek to just before slider ends" , ( ) = > Player . GameplayClockContainer . Seek ( beatmap . HitObjects [ 4 ] . GetEndTime ( ) - 100 ) ) ;
2023-02-21 13:04:19 +08:00
waitForCatchUp ( ) ;
2023-02-16 17:13:11 +08:00
AddUntilStep ( "wait until valid object is slider's last nested" , ( ) = > sampleTriggerSource . GetMostValidObject ( ) , ( ) = > Is . EqualTo ( beatmap . HitObjects [ 4 ] . NestedHitObjects . Last ( ) ) ) ;
2021-08-25 15:55:03 +08:00
2023-02-16 17:13:11 +08:00
// After we get far enough away, the samples of the object itself should be used, not any nested object.
2023-02-22 13:41:20 +08:00
AddStep ( "seek to further after slider" , ( ) = > Player . GameplayClockContainer . Seek ( beatmap . HitObjects [ 4 ] . GetEndTime ( ) + 1000 ) ) ;
2023-02-21 13:04:19 +08:00
waitForCatchUp ( ) ;
2023-02-16 17:13:11 +08:00
AddUntilStep ( "wait until valid object is slider itself" , ( ) = > sampleTriggerSource . GetMostValidObject ( ) , ( ) = > Is . EqualTo ( beatmap . HitObjects [ 4 ] ) ) ;
2021-08-25 15:55:03 +08:00
2023-02-22 13:41:20 +08:00
AddStep ( "Seek into future" , ( ) = > Player . GameplayClockContainer . Seek ( beatmap . HitObjects . Last ( ) . GetEndTime ( ) + 10000 ) ) ;
2023-02-21 13:04:19 +08:00
waitForCatchUp ( ) ;
2023-02-16 16:58:32 +08:00
waitForAliveObjectIndex ( null ) ;
2023-02-16 17:13:11 +08:00
checkValidObjectIndex ( 4 ) ;
2021-08-25 15:55:03 +08:00
}
2023-06-20 20:02:34 +08:00
private void seekBeforeIndex ( int index , double amount = 100 )
2023-02-21 13:04:19 +08:00
{
2023-06-20 20:02:34 +08:00
AddStep ( $"seek to {amount} ms before object {index}" , ( ) = > Player . GameplayClockContainer . Seek ( beatmap . HitObjects [ index ] . StartTime - amount ) ) ;
2023-02-21 13:04:19 +08:00
waitForCatchUp ( ) ;
}
private void waitForCatchUp ( ) = >
2023-02-22 14:26:09 +08:00
AddUntilStep ( "wait for frame stable clock to catch up" , ( ) = > Precision . AlmostEquals ( Player . GameplayClockContainer . CurrentTime , Player . DrawableRuleset . FrameStableClock . CurrentTime ) ) ;
2023-02-16 16:58:32 +08:00
private void waitForAliveObjectIndex ( int? index )
{
if ( index = = null )
AddUntilStep ( "wait for no alive objects" , getNextAliveObject , ( ) = > Is . Null ) ;
else
AddUntilStep ( $"wait for next alive to be {index}" , ( ) = > getNextAliveObject ( ) ? . HitObject , ( ) = > Is . EqualTo ( beatmap . HitObjects [ index . Value ] ) ) ;
}
private void checkValidObjectIndex ( int index ) = >
2023-07-05 16:41:51 +08:00
AddAssert ( $"check object at index {index} is correct" , ( ) = > sampleTriggerSource . GetMostValidObject ( ) , ( ) = > Is . EqualTo ( beatmap . HitObjects [ index ] ) ) ;
2023-02-16 16:58:32 +08:00
2023-02-16 16:37:46 +08:00
private DrawableHitObject ? getNextAliveObject ( ) = >
2021-08-25 15:55:03 +08:00
Player . DrawableRuleset . Playfield . HitObjectContainer . AliveObjects . FirstOrDefault ( ) ;
[Test]
public void TestSampleTriggering ( )
{
AddRepeatStep ( "trigger sample" , ( ) = > sampleTriggerSource . Play ( ) , 10 ) ;
}
public partial class TestGameplaySampleTriggerSource : GameplaySampleTriggerSource
{
public TestGameplaySampleTriggerSource ( HitObjectContainer hitObjectContainer )
: base ( hitObjectContainer )
{
}
2023-06-21 03:57:32 +08:00
public new HitObject ? GetMostValidObject ( ) = > base . GetMostValidObject ( ) ;
2021-08-25 15:55:03 +08:00
}
}
}