2022-08-15 19:11:06 +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.
2022-08-15 15:19:04 +00:00
using System.Linq ;
using NUnit.Framework ;
2022-08-27 11:53:50 +00:00
using osu.Framework.Testing ;
2022-08-15 15:19:04 +00:00
using osu.Framework.Utils ;
using osu.Game.Rulesets.Objects ;
using osu.Game.Rulesets.Objects.Types ;
2022-08-27 11:53:50 +00:00
using osu.Game.Rulesets.Osu.Edit ;
2022-08-15 15:19:04 +00:00
using osu.Game.Rulesets.Osu.Objects ;
using osuTK ;
using osuTK.Input ;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneObjectMerging : TestSceneOsuEditor
{
2022-08-27 11:53:50 +00:00
private OsuSelectionHandler selectionHandler = > Editor . ChildrenOfType < OsuSelectionHandler > ( ) . First ( ) ;
2022-08-15 15:19:04 +00:00
[Test]
public void TestSimpleMerge ( )
{
2022-08-15 19:15:23 +00:00
HitCircle ? circle1 = null ;
HitCircle ? circle2 = null ;
2022-08-15 15:19:04 +00:00
AddStep ( "select first two circles" , ( ) = >
{
circle1 = ( HitCircle ) EditorBeatmap . HitObjects . First ( h = > h is HitCircle ) ;
circle2 = ( HitCircle ) EditorBeatmap . HitObjects . First ( h = > h is HitCircle & & h ! = circle1 ) ;
EditorClock . Seek ( circle1 . StartTime ) ;
EditorBeatmap . SelectedHitObjects . Add ( circle1 ) ;
EditorBeatmap . SelectedHitObjects . Add ( circle2 ) ;
} ) ;
2022-08-27 11:53:50 +00:00
moveMouseToHitObject ( 1 ) ;
2024-08-06 08:34:36 +00:00
AddAssert ( "merge option available" , ( ) = > selectionHandler . ContextMenuItems . Any ( o = > o . Text . Value = = "Merge selection" ) ) ;
2022-08-27 11:53:50 +00:00
2022-08-15 15:19:04 +00:00
mergeSelection ( ) ;
2022-08-15 19:15:23 +00:00
AddAssert ( "slider created" , ( ) = > circle1 is not null & & circle2 is not null & & sliderCreatedFor (
2023-11-08 10:43:54 +00:00
( pos : circle1 . Position , pathType : PathType . LINEAR ) ,
2022-08-15 15:19:04 +00:00
( pos : circle2 . Position , pathType : null ) ) ) ;
AddStep ( "undo" , ( ) = > Editor . Undo ( ) ) ;
2022-08-15 19:15:23 +00:00
AddAssert ( "merged objects restored" , ( ) = > circle1 is not null & & circle2 is not null & & objectsRestored ( circle1 , circle2 ) ) ;
2022-08-15 15:19:04 +00:00
}
[Test]
public void TestMergeCircleSlider ( )
{
2022-08-15 19:15:23 +00:00
HitCircle ? circle1 = null ;
Slider ? slider = null ;
HitCircle ? circle2 = null ;
2022-08-15 15:19:04 +00:00
AddStep ( "select a circle, slider, circle" , ( ) = >
{
circle1 = ( HitCircle ) EditorBeatmap . HitObjects . First ( h = > h is HitCircle ) ;
slider = ( Slider ) EditorBeatmap . HitObjects . First ( h = > h is Slider & & h . StartTime > circle1 . StartTime ) ;
circle2 = ( HitCircle ) EditorBeatmap . HitObjects . First ( h = > h is HitCircle & & h . StartTime > slider . StartTime ) ;
EditorClock . Seek ( circle1 . StartTime ) ;
EditorBeatmap . SelectedHitObjects . Add ( circle1 ) ;
EditorBeatmap . SelectedHitObjects . Add ( slider ) ;
EditorBeatmap . SelectedHitObjects . Add ( circle2 ) ;
} ) ;
mergeSelection ( ) ;
AddAssert ( "slider created" , ( ) = >
{
2024-04-02 17:50:39 +00:00
if ( circle1 = = null | | circle2 = = null | | slider = = null )
2022-08-15 19:15:23 +00:00
return false ;
2022-08-15 15:19:04 +00:00
var controlPoints = slider . Path . ControlPoints ;
( Vector2 , PathType ? ) [ ] args = new ( Vector2 , PathType ? ) [ controlPoints . Count + 2 ] ;
2023-11-08 10:43:54 +00:00
args [ 0 ] = ( circle1 . Position , PathType . LINEAR ) ;
2022-08-15 15:19:04 +00:00
for ( int i = 0 ; i < controlPoints . Count ; i + + )
{
2023-11-08 10:43:54 +00:00
args [ i + 1 ] = ( controlPoints [ i ] . Position + slider . Position , i = = controlPoints . Count - 1 ? PathType . LINEAR : controlPoints [ i ] . Type ) ;
2022-08-15 15:19:04 +00:00
}
args [ ^ 1 ] = ( circle2 . Position , null ) ;
return sliderCreatedFor ( args ) ;
} ) ;
2022-08-26 10:16:12 +00:00
AddAssert ( "samples exist" , sliderSampleExist ) ;
2022-08-15 15:19:04 +00:00
AddStep ( "undo" , ( ) = > Editor . Undo ( ) ) ;
2022-08-15 19:15:23 +00:00
AddAssert ( "merged objects restored" , ( ) = > circle1 is not null & & circle2 is not null & & slider is not null & & objectsRestored ( circle1 , slider , circle2 ) ) ;
2022-08-15 15:19:04 +00:00
}
[Test]
public void TestMergeSliderSlider ( )
{
2022-08-15 19:15:23 +00:00
Slider ? slider1 = null ;
SliderPath ? slider1Path = null ;
Slider ? slider2 = null ;
2022-08-15 15:19:04 +00:00
AddStep ( "select two sliders" , ( ) = >
{
slider1 = ( Slider ) EditorBeatmap . HitObjects . First ( h = > h is Slider ) ;
slider1Path = new SliderPath ( slider1 . Path . ControlPoints . Select ( p = > new PathControlPoint ( p . Position , p . Type ) ) . ToArray ( ) , slider1 . Path . ExpectedDistance . Value ) ;
slider2 = ( Slider ) EditorBeatmap . HitObjects . First ( h = > h is Slider & & h . StartTime > slider1 . StartTime ) ;
EditorClock . Seek ( slider1 . StartTime ) ;
EditorBeatmap . SelectedHitObjects . Add ( slider1 ) ;
EditorBeatmap . SelectedHitObjects . Add ( slider2 ) ;
} ) ;
mergeSelection ( ) ;
AddAssert ( "slider created" , ( ) = >
{
2024-04-02 17:50:39 +00:00
if ( slider1 = = null | | slider2 = = null | | slider1Path = = null )
2022-08-15 19:15:23 +00:00
return false ;
2022-08-15 15:19:04 +00:00
var controlPoints1 = slider1Path . ControlPoints ;
var controlPoints2 = slider2 . Path . ControlPoints ;
( Vector2 , PathType ? ) [ ] args = new ( Vector2 , PathType ? ) [ controlPoints1 . Count + controlPoints2 . Count - 1 ] ;
for ( int i = 0 ; i < controlPoints1 . Count - 1 ; i + + )
{
args [ i ] = ( controlPoints1 [ i ] . Position + slider1 . Position , controlPoints1 [ i ] . Type ) ;
}
for ( int i = 0 ; i < controlPoints2 . Count ; i + + )
{
args [ i + controlPoints1 . Count - 1 ] = ( controlPoints2 [ i ] . Position + controlPoints1 [ ^ 1 ] . Position + slider1 . Position , controlPoints2 [ i ] . Type ) ;
}
return sliderCreatedFor ( args ) ;
} ) ;
2022-08-26 10:16:12 +00:00
AddAssert ( "samples exist" , sliderSampleExist ) ;
2022-08-15 15:19:04 +00:00
AddAssert ( "merged slider matches first slider" , ( ) = >
{
var mergedSlider = ( Slider ) EditorBeatmap . SelectedHitObjects . First ( ) ;
2022-08-15 19:15:23 +00:00
return slider1 is not null & & mergedSlider . HeadCircle . Samples . SequenceEqual ( slider1 . HeadCircle . Samples )
& & mergedSlider . TailCircle . Samples . SequenceEqual ( slider1 . TailCircle . Samples )
2023-04-26 12:21:52 +00:00
& & mergedSlider . Samples . SequenceEqual ( slider1 . Samples ) ;
2022-08-15 15:19:04 +00:00
} ) ;
AddAssert ( "slider end is at same completion for last slider" , ( ) = >
{
2024-04-02 17:50:39 +00:00
if ( slider1Path = = null | | slider2 = = null )
2022-08-15 19:15:23 +00:00
return false ;
2022-08-15 15:19:04 +00:00
var mergedSlider = ( Slider ) EditorBeatmap . SelectedHitObjects . First ( ) ;
return Precision . AlmostEquals ( mergedSlider . Path . Distance , slider1Path . CalculatedDistance + slider2 . Path . Distance ) ;
} ) ;
}
2022-08-16 21:05:35 +00:00
[Test]
public void TestNonMerge ( )
{
HitCircle ? circle1 = null ;
HitCircle ? circle2 = null ;
Spinner ? spinner = null ;
AddStep ( "select first two circles and spinner" , ( ) = >
{
circle1 = ( HitCircle ) EditorBeatmap . HitObjects . First ( h = > h is HitCircle ) ;
circle2 = ( HitCircle ) EditorBeatmap . HitObjects . First ( h = > h is HitCircle & & h ! = circle1 ) ;
spinner = ( Spinner ) EditorBeatmap . HitObjects . First ( h = > h is Spinner ) ;
EditorClock . Seek ( spinner . StartTime ) ;
EditorBeatmap . SelectedHitObjects . Add ( circle1 ) ;
EditorBeatmap . SelectedHitObjects . Add ( circle2 ) ;
EditorBeatmap . SelectedHitObjects . Add ( spinner ) ;
} ) ;
mergeSelection ( ) ;
AddAssert ( "slider created" , ( ) = > circle1 is not null & & circle2 is not null & & sliderCreatedFor (
2023-11-08 10:43:54 +00:00
( pos : circle1 . Position , pathType : PathType . LINEAR ) ,
2022-08-16 21:05:35 +00:00
( pos : circle2 . Position , pathType : null ) ) ) ;
2022-08-26 10:16:12 +00:00
AddAssert ( "samples exist" , sliderSampleExist ) ;
2022-08-25 11:31:47 +00:00
2022-08-26 10:16:12 +00:00
AddAssert ( "spinner not merged" , ( ) = > EditorBeatmap . HitObjects . Contains ( spinner ) ) ;
2022-08-25 11:31:47 +00:00
}
2022-08-27 11:53:50 +00:00
[Test]
public void TestIllegalMerge ( )
{
HitCircle ? circle1 = null ;
HitCircle ? circle2 = null ;
AddStep ( "add two circles on the same position" , ( ) = >
{
circle1 = new HitCircle ( ) ;
2022-08-27 15:43:32 +00:00
circle2 = new HitCircle { Position = circle1 . Position + Vector2 . UnitX } ;
2022-08-27 11:53:50 +00:00
EditorClock . Seek ( 0 ) ;
EditorBeatmap . Add ( circle1 ) ;
EditorBeatmap . Add ( circle2 ) ;
EditorBeatmap . SelectedHitObjects . Add ( circle1 ) ;
EditorBeatmap . SelectedHitObjects . Add ( circle2 ) ;
} ) ;
moveMouseToHitObject ( 1 ) ;
2024-08-06 08:34:36 +00:00
AddAssert ( "merge option not available" , ( ) = > selectionHandler . ContextMenuItems . Length > 0 & & selectionHandler . ContextMenuItems . All ( o = > o . Text . Value ! = "Merge selection" ) ) ;
2022-08-27 11:53:50 +00:00
mergeSelection ( ) ;
AddAssert ( "circles not merged" , ( ) = > circle1 is not null & & circle2 is not null
& & EditorBeatmap . HitObjects . Contains ( circle1 ) & & EditorBeatmap . HitObjects . Contains ( circle2 ) ) ;
}
2022-08-27 15:43:32 +00:00
[Test]
public void TestSameStartTimeMerge ( )
{
HitCircle ? circle1 = null ;
HitCircle ? circle2 = null ;
AddStep ( "add two circles at the same time" , ( ) = >
{
circle1 = new HitCircle ( ) ;
circle2 = new HitCircle { Position = circle1 . Position + 100 * Vector2 . UnitX } ;
EditorClock . Seek ( 0 ) ;
EditorBeatmap . Add ( circle1 ) ;
EditorBeatmap . Add ( circle2 ) ;
EditorBeatmap . SelectedHitObjects . Add ( circle1 ) ;
EditorBeatmap . SelectedHitObjects . Add ( circle2 ) ;
} ) ;
moveMouseToHitObject ( 1 ) ;
2024-08-06 08:34:36 +00:00
AddAssert ( "merge option available" , ( ) = > selectionHandler . ContextMenuItems . Any ( o = > o . Text . Value = = "Merge selection" ) ) ;
2022-08-27 15:43:32 +00:00
mergeSelection ( ) ;
AddAssert ( "slider created" , ( ) = > circle1 is not null & & circle2 is not null & & sliderCreatedFor (
2023-11-08 10:43:54 +00:00
( pos : circle1 . Position , pathType : PathType . LINEAR ) ,
2022-08-27 15:43:32 +00:00
( pos : circle2 . Position , pathType : null ) ) ) ;
}
2024-04-02 10:45:27 +00:00
[Test]
public void TestMergeSliderSliderSameStartTime ( )
{
Slider ? slider1 = null ;
SliderPath ? slider1Path = null ;
Slider ? slider2 = null ;
AddStep ( "select two sliders" , ( ) = >
{
slider1 = ( Slider ) EditorBeatmap . HitObjects . First ( h = > h is Slider ) ;
slider1Path = new SliderPath ( slider1 . Path . ControlPoints . Select ( p = > new PathControlPoint ( p . Position , p . Type ) ) . ToArray ( ) , slider1 . Path . ExpectedDistance . Value ) ;
slider2 = ( Slider ) EditorBeatmap . HitObjects . First ( h = > h is Slider & & h . StartTime > slider1 . StartTime ) ;
EditorClock . Seek ( slider1 . StartTime ) ;
EditorBeatmap . SelectedHitObjects . AddRange ( [ slider1 , slider2 ] ) ;
} ) ;
AddStep ( "move sliders to the same start time" , ( ) = >
{
slider2 ! . StartTime = slider1 ! . StartTime ;
} ) ;
mergeSelection ( ) ;
AddAssert ( "slider created" , ( ) = >
{
2024-04-02 17:50:39 +00:00
if ( slider1 = = null | | slider2 = = null | | slider1Path = = null )
2024-04-02 10:45:27 +00:00
return false ;
var controlPoints1 = slider1Path . ControlPoints ;
var controlPoints2 = slider2 . Path . ControlPoints ;
( Vector2 , PathType ? ) [ ] args = new ( Vector2 , PathType ? ) [ controlPoints1 . Count + controlPoints2 . Count - 1 ] ;
for ( int i = 0 ; i < controlPoints1 . Count - 1 ; i + + )
{
args [ i ] = ( controlPoints1 [ i ] . Position + slider1 . Position , controlPoints1 [ i ] . Type ) ;
}
for ( int i = 0 ; i < controlPoints2 . Count ; i + + )
{
args [ i + controlPoints1 . Count - 1 ] = ( controlPoints2 [ i ] . Position + controlPoints1 [ ^ 1 ] . Position + slider1 . Position , controlPoints2 [ i ] . Type ) ;
}
return sliderCreatedFor ( args ) ;
} ) ;
AddAssert ( "samples exist" , sliderSampleExist ) ;
AddAssert ( "merged slider matches first slider" , ( ) = >
{
var mergedSlider = ( Slider ) EditorBeatmap . SelectedHitObjects . First ( ) ;
return slider1 is not null & & mergedSlider . HeadCircle . Samples . SequenceEqual ( slider1 . HeadCircle . Samples )
& & mergedSlider . TailCircle . Samples . SequenceEqual ( slider1 . TailCircle . Samples )
& & mergedSlider . Samples . SequenceEqual ( slider1 . Samples ) ;
} ) ;
AddAssert ( "slider end is at same completion for last slider" , ( ) = >
{
2024-04-02 17:50:39 +00:00
if ( slider1Path = = null | | slider2 = = null )
2024-04-02 10:45:27 +00:00
return false ;
var mergedSlider = ( Slider ) EditorBeatmap . SelectedHitObjects . First ( ) ;
return Precision . AlmostEquals ( mergedSlider . Path . Distance , slider1Path . CalculatedDistance + slider2 . Path . Distance ) ;
} ) ;
}
[Test]
public void TestMergeSliderSliderSameStartAndEndTime ( )
{
Slider ? slider1 = null ;
SliderPath ? slider1Path = null ;
Slider ? slider2 = null ;
AddStep ( "select two sliders" , ( ) = >
{
slider1 = ( Slider ) EditorBeatmap . HitObjects . First ( h = > h is Slider ) ;
slider1Path = new SliderPath ( slider1 . Path . ControlPoints . Select ( p = > new PathControlPoint ( p . Position , p . Type ) ) . ToArray ( ) , slider1 . Path . ExpectedDistance . Value ) ;
slider2 = ( Slider ) EditorBeatmap . HitObjects . First ( h = > h is Slider & & h . StartTime > slider1 . StartTime ) ;
EditorClock . Seek ( slider1 . StartTime ) ;
EditorBeatmap . SelectedHitObjects . AddRange ( [ slider1 , slider2 ] ) ;
} ) ;
AddStep ( "move sliders to the same start & end time" , ( ) = >
{
slider2 ! . StartTime = slider1 ! . StartTime ;
slider2 . Path = slider1 . Path ;
} ) ;
mergeSelection ( ) ;
AddAssert ( "slider created" , ( ) = >
{
2024-04-02 17:50:39 +00:00
if ( slider1 = = null | | slider2 = = null | | slider1Path = = null )
2024-04-02 10:45:27 +00:00
return false ;
var controlPoints1 = slider1Path . ControlPoints ;
var controlPoints2 = slider2 . Path . ControlPoints ;
( Vector2 , PathType ? ) [ ] args = new ( Vector2 , PathType ? ) [ controlPoints1 . Count + controlPoints2 . Count - 1 ] ;
for ( int i = 0 ; i < controlPoints1 . Count - 1 ; i + + )
{
args [ i ] = ( controlPoints1 [ i ] . Position + slider1 . Position , controlPoints1 [ i ] . Type ) ;
}
for ( int i = 0 ; i < controlPoints2 . Count ; i + + )
{
args [ i + controlPoints1 . Count - 1 ] = ( controlPoints2 [ i ] . Position + controlPoints1 [ ^ 1 ] . Position + slider1 . Position , controlPoints2 [ i ] . Type ) ;
}
return sliderCreatedFor ( args ) ;
} ) ;
AddAssert ( "samples exist" , sliderSampleExist ) ;
AddAssert ( "merged slider matches first slider" , ( ) = >
{
var mergedSlider = ( Slider ) EditorBeatmap . SelectedHitObjects . First ( ) ;
return slider1 is not null & & mergedSlider . HeadCircle . Samples . SequenceEqual ( slider1 . HeadCircle . Samples )
& & mergedSlider . TailCircle . Samples . SequenceEqual ( slider1 . TailCircle . Samples )
& & mergedSlider . Samples . SequenceEqual ( slider1 . Samples ) ;
} ) ;
AddAssert ( "slider end is at same completion for last slider" , ( ) = >
{
2024-04-02 17:50:39 +00:00
if ( slider1Path = = null | | slider2 = = null )
2024-04-02 10:45:27 +00:00
return false ;
var mergedSlider = ( Slider ) EditorBeatmap . SelectedHitObjects . First ( ) ;
return Precision . AlmostEquals ( mergedSlider . Path . Distance , slider1Path . CalculatedDistance + slider2 . Path . Distance ) ;
} ) ;
}
2022-08-15 15:19:04 +00:00
private void mergeSelection ( )
{
AddStep ( "merge selection" , ( ) = >
{
InputManager . PressKey ( Key . LControl ) ;
InputManager . PressKey ( Key . LShift ) ;
InputManager . Key ( Key . M ) ;
InputManager . ReleaseKey ( Key . LShift ) ;
InputManager . ReleaseKey ( Key . LControl ) ;
} ) ;
}
private bool sliderCreatedFor ( params ( Vector2 pos , PathType ? pathType ) [ ] expectedControlPoints )
{
if ( EditorBeatmap . SelectedHitObjects . Count ! = 1 )
return false ;
var mergedSlider = ( Slider ) EditorBeatmap . SelectedHitObjects . First ( ) ;
int i = 0 ;
foreach ( ( Vector2 pos , PathType ? pathType ) in expectedControlPoints )
{
var controlPoint = mergedSlider . Path . ControlPoints [ i + + ] ;
if ( ! Precision . AlmostEquals ( controlPoint . Position + mergedSlider . Position , pos ) | | controlPoint . Type ! = pathType )
return false ;
}
return true ;
}
private bool objectsRestored ( params HitObject [ ] objects )
{
foreach ( var hitObject in objects )
{
if ( EditorBeatmap . HitObjects . Contains ( hitObject ) )
return false ;
}
return true ;
}
2022-08-26 08:55:18 +00:00
private bool sliderSampleExist ( )
{
if ( EditorBeatmap . SelectedHitObjects . Count ! = 1 )
return false ;
var mergedSlider = ( Slider ) EditorBeatmap . SelectedHitObjects . First ( ) ;
2022-08-26 10:16:12 +00:00
return mergedSlider . Samples [ 0 ] is not null ;
2022-08-26 08:55:18 +00:00
}
2022-08-27 11:53:50 +00:00
private void moveMouseToHitObject ( int index )
{
AddStep ( $"hover mouse over hit object {index}" , ( ) = >
{
if ( EditorBeatmap . HitObjects . Count < = index )
return ;
Vector2 position = ( ( OsuHitObject ) EditorBeatmap . HitObjects [ index ] ) . Position ;
InputManager . MoveMouseTo ( selectionHandler . ToScreenSpace ( position ) ) ;
} ) ;
}
2022-08-15 15:19:04 +00:00
}
}