2023-06-23 00:37:25 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-12-10 19:44:45 +08:00
// See the LICENCE file in the repository root for full licence text.
2020-04-21 13:47:12 +08:00
using System ;
2020-04-21 13:49:31 +08:00
using System.Collections ;
2019-12-13 18:00:28 +08:00
using System.Collections.Generic ;
2019-12-10 19:44:45 +08:00
using System.IO ;
using System.Linq ;
2020-05-11 15:37:08 +08:00
using System.Text ;
2019-12-10 19:44:45 +08:00
using NUnit.Framework ;
2020-04-21 13:47:12 +08:00
using osu.Framework.Audio.Track ;
using osu.Framework.Graphics.Textures ;
2020-08-12 12:38:05 +08:00
using osu.Framework.IO.Stores ;
2019-12-10 19:44:45 +08:00
using osu.Game.Beatmaps ;
2020-04-21 13:49:31 +08:00
using osu.Game.Beatmaps.ControlPoints ;
2019-12-10 19:44:45 +08:00
using osu.Game.Beatmaps.Formats ;
2021-08-30 15:57:49 +08:00
using osu.Game.Beatmaps.Legacy ;
2019-12-10 19:44:45 +08:00
using osu.Game.IO ;
2019-12-13 18:00:28 +08:00
using osu.Game.IO.Serialization ;
2020-04-21 13:47:12 +08:00
using osu.Game.Rulesets.Catch ;
using osu.Game.Rulesets.Mania ;
2021-04-06 13:33:46 +08:00
using osu.Game.Rulesets.Objects ;
using osu.Game.Rulesets.Objects.Types ;
2020-04-21 13:47:12 +08:00
using osu.Game.Rulesets.Osu ;
2021-04-06 13:33:46 +08:00
using osu.Game.Rulesets.Osu.Objects ;
2020-04-21 13:47:12 +08:00
using osu.Game.Rulesets.Taiko ;
2020-08-12 12:38:05 +08:00
using osu.Game.Skinning ;
2019-12-10 19:44:45 +08:00
using osu.Game.Tests.Resources ;
2021-04-06 13:33:46 +08:00
using osuTK ;
2019-12-10 19:44:45 +08:00
namespace osu.Game.Tests.Beatmaps.Formats
{
[TestFixture]
public class LegacyBeatmapEncoderTest
{
2020-08-23 21:08:02 +08:00
private static readonly DllResourceStore beatmaps_resource_store = TestResources . GetStore ( ) ;
2020-08-16 04:03:24 +08:00
2020-10-16 11:58:34 +08:00
private static IEnumerable < string > allBeatmaps = beatmaps_resource_store . GetAvailableResources ( ) . Where ( res = > res . EndsWith ( ".osu" , StringComparison . Ordinal ) ) ;
2020-08-16 04:03:24 +08:00
2019-12-13 18:00:28 +08:00
[TestCaseSource(nameof(allBeatmaps))]
2020-05-11 15:30:54 +08:00
public void TestEncodeDecodeStability ( string name )
2019-12-10 19:44:45 +08:00
{
2020-08-30 22:08:13 +08:00
var decoded = decodeFromLegacy ( beatmaps_resource_store . GetStream ( name ) , name ) ;
2020-08-23 21:08:02 +08:00
var decodedAfterEncode = decodeFromLegacy ( encodeToLegacy ( decoded ) , name ) ;
2019-12-10 19:44:45 +08:00
2020-09-07 00:44:41 +08:00
sort ( decoded . beatmap ) ;
sort ( decodedAfterEncode . beatmap ) ;
2020-04-21 13:49:31 +08:00
2021-09-10 15:51:24 +08:00
compareBeatmaps ( decoded , decodedAfterEncode ) ;
2020-08-31 23:24:24 +08:00
}
2021-08-30 14:30:04 +08:00
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStabilityDoubleConvert ( string name )
{
var decoded = decodeFromLegacy ( beatmaps_resource_store . GetStream ( name ) , name ) ;
var decodedAfterEncode = decodeFromLegacy ( encodeToLegacy ( decoded ) , name ) ;
// run an extra convert. this is expected to be stable.
decodedAfterEncode . beatmap = convert ( decodedAfterEncode . beatmap ) ;
sort ( decoded . beatmap ) ;
sort ( decodedAfterEncode . beatmap ) ;
2021-09-10 15:51:24 +08:00
compareBeatmaps ( decoded , decodedAfterEncode ) ;
2020-08-31 23:24:24 +08:00
}
2021-08-31 13:51:14 +08:00
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStabilityWithNonLegacyControlPoints ( string name )
{
var decoded = decodeFromLegacy ( beatmaps_resource_store . GetStream ( name ) , name ) ;
// we are testing that the transfer of relevant data to hitobjects (from legacy control points) sticks through encode/decode.
// before the encode step, the legacy information is removed here.
decoded . beatmap . ControlPointInfo = removeLegacyControlPointTypes ( decoded . beatmap . ControlPointInfo ) ;
var decodedAfterEncode = decodeFromLegacy ( encodeToLegacy ( decoded ) , name ) ;
2021-09-10 15:51:24 +08:00
compareBeatmaps ( decoded , decodedAfterEncode ) ;
2021-08-31 13:51:14 +08:00
2023-11-08 18:43:54 +08:00
static ControlPointInfo removeLegacyControlPointTypes ( ControlPointInfo controlPointInfo )
2021-08-31 13:51:14 +08:00
{
// emulate non-legacy control points by cloning the non-legacy portion.
// the assertion is that the encoder can recreate this losslessly from hitobject data.
Assert . IsInstanceOf < LegacyControlPointInfo > ( controlPointInfo ) ;
var newControlPoints = new ControlPointInfo ( ) ;
foreach ( var point in controlPointInfo . AllControlPoints )
{
// completely ignore "legacy" types, which have been moved to HitObjects.
// even though these would mostly be ignored by the Add call, they will still be available in groups,
// which isn't what we want to be testing here.
2021-09-06 21:04:51 +08:00
if ( point is SampleControlPoint | | point is DifficultyControlPoint )
2021-08-31 13:51:14 +08:00
continue ;
newControlPoints . Add ( point . Time , point . DeepClone ( ) ) ;
}
return newControlPoints ;
}
}
2021-09-10 15:51:24 +08:00
private void compareBeatmaps ( ( IBeatmap beatmap , TestLegacySkin skin ) expected , ( IBeatmap beatmap , TestLegacySkin skin ) actual )
{
// Check all control points that are still considered to be at a global level.
Assert . That ( expected . beatmap . ControlPointInfo . TimingPoints . Serialize ( ) , Is . EqualTo ( actual . beatmap . ControlPointInfo . TimingPoints . Serialize ( ) ) ) ;
Assert . That ( expected . beatmap . ControlPointInfo . EffectPoints . Serialize ( ) , Is . EqualTo ( actual . beatmap . ControlPointInfo . EffectPoints . Serialize ( ) ) ) ;
// Check all hitobjects.
Assert . That ( expected . beatmap . HitObjects . Serialize ( ) , Is . EqualTo ( actual . beatmap . HitObjects . Serialize ( ) ) ) ;
// Check skin.
Assert . IsTrue ( areComboColoursEqual ( expected . skin . Configuration , actual . skin . Configuration ) ) ;
}
2023-11-21 13:02:08 +08:00
[Test]
public void TestEncodeBSplineCurveType ( )
{
var beatmap = new Beatmap
{
HitObjects =
{
new Slider
{
Path = new SliderPath ( new [ ]
{
new PathControlPoint ( Vector2 . Zero , PathType . BSpline ( 3 ) ) ,
new PathControlPoint ( new Vector2 ( 50 ) ) ,
new PathControlPoint ( new Vector2 ( 100 ) , PathType . BSpline ( 3 ) ) ,
new PathControlPoint ( new Vector2 ( 150 ) )
} )
} ,
}
} ;
var decodedAfterEncode = decodeFromLegacy ( encodeToLegacy ( ( beatmap , new TestLegacySkin ( beatmaps_resource_store , string . Empty ) ) ) , string . Empty ) ;
var decodedSlider = ( Slider ) decodedAfterEncode . beatmap . HitObjects [ 0 ] ;
Assert . That ( decodedSlider . Path . ControlPoints . Count , Is . EqualTo ( 4 ) ) ;
Assert . That ( decodedSlider . Path . ControlPoints [ 0 ] . Type , Is . EqualTo ( PathType . BSpline ( 3 ) ) ) ;
Assert . That ( decodedSlider . Path . ControlPoints [ 2 ] . Type , Is . EqualTo ( PathType . BSpline ( 3 ) ) ) ;
}
2021-04-06 13:33:46 +08:00
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError ( )
{
var beatmap = new Beatmap
{
HitObjects =
{
new Slider
{
Position = new Vector2 ( 0.6f ) ,
Path = new SliderPath ( new [ ]
{
2023-11-08 18:43:54 +08:00
new PathControlPoint ( Vector2 . Zero , PathType . BEZIER ) ,
2021-04-06 13:33:46 +08:00
new PathControlPoint ( new Vector2 ( 0.5f ) ) ,
new PathControlPoint ( new Vector2 ( 0.51f ) ) , // This is actually on the same position as the previous one in legacy beatmaps (truncated to int).
2023-11-08 18:43:54 +08:00
new PathControlPoint ( new Vector2 ( 1f ) , PathType . BEZIER ) ,
2021-04-06 13:33:46 +08:00
new PathControlPoint ( new Vector2 ( 2f ) )
} )
} ,
}
} ;
var decodedAfterEncode = decodeFromLegacy ( encodeToLegacy ( ( beatmap , new TestLegacySkin ( beatmaps_resource_store , string . Empty ) ) ) , string . Empty ) ;
var decodedSlider = ( Slider ) decodedAfterEncode . beatmap . HitObjects [ 0 ] ;
Assert . That ( decodedSlider . Path . ControlPoints . Count , Is . EqualTo ( 5 ) ) ;
}
2020-08-31 23:24:24 +08:00
private bool areComboColoursEqual ( IHasComboColours a , IHasComboColours b )
{
// equal to null, no need to SequenceEqual
if ( a . ComboColours = = null & & b . ComboColours = = null )
return true ;
if ( a . ComboColours = = null | | b . ComboColours = = null )
return false ;
return a . ComboColours . SequenceEqual ( b . ComboColours ) ;
2019-12-10 19:44:45 +08:00
}
2020-09-07 00:44:41 +08:00
private void sort ( IBeatmap beatmap )
2020-04-21 13:49:31 +08:00
{
// Sort control points to ensure a sane ordering, as they may be parsed in different orders. This works because each group contains only uniquely-typed control points.
2020-09-07 00:44:41 +08:00
foreach ( var g in beatmap . ControlPointInfo . Groups )
2020-04-21 13:49:31 +08:00
{
ArrayList . Adapter ( ( IList ) g . ControlPoints ) . Sort (
Comparer < ControlPoint > . Create ( ( c1 , c2 ) = > string . Compare ( c1 . GetType ( ) . ToString ( ) , c2 . GetType ( ) . ToString ( ) , StringComparison . Ordinal ) ) ) ;
}
}
2021-09-10 15:51:24 +08:00
private ( IBeatmap beatmap , TestLegacySkin skin ) decodeFromLegacy ( Stream stream , string name )
2019-12-10 19:44:45 +08:00
{
2020-05-11 15:30:54 +08:00
using ( var reader = new LineBufferedReader ( stream ) )
2020-08-16 05:41:53 +08:00
{
var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false } . Decode ( reader ) ;
2020-08-31 23:24:24 +08:00
var beatmapSkin = new TestLegacySkin ( beatmaps_resource_store , name ) ;
return ( convert ( beatmap ) , beatmapSkin ) ;
2020-08-31 03:13:06 +08:00
}
}
2020-09-02 00:15:46 +08:00
private class TestLegacySkin : LegacySkin
2020-08-30 22:08:52 +08:00
{
2023-11-16 19:16:23 +08:00
public TestLegacySkin ( IResourceStore < byte [ ] > fallbackStore , string fileName )
: base ( new SkinInfo { Name = "Test Skin" , Creator = "Craftplacer" } , null , fallbackStore , fileName )
2020-08-30 22:08:52 +08:00
{
}
}
2021-09-10 15:51:24 +08:00
private MemoryStream encodeToLegacy ( ( IBeatmap beatmap , ISkin skin ) fullBeatmap )
2020-05-11 15:30:54 +08:00
{
2020-08-16 05:41:53 +08:00
var ( beatmap , beatmapSkin ) = fullBeatmap ;
2020-05-11 15:30:54 +08:00
var stream = new MemoryStream ( ) ;
2019-12-10 19:44:45 +08:00
2020-05-11 16:26:11 +08:00
using ( var writer = new StreamWriter ( stream , Encoding . UTF8 , 1024 , true ) )
2020-08-16 04:06:26 +08:00
new LegacyBeatmapEncoder ( beatmap , beatmapSkin ) . Encode ( writer ) ;
2019-12-10 19:44:45 +08:00
2020-05-11 15:30:54 +08:00
stream . Position = 0 ;
2020-04-21 13:47:12 +08:00
2020-05-11 15:30:54 +08:00
return stream ;
2019-12-10 19:44:45 +08:00
}
2020-04-21 13:47:12 +08:00
private IBeatmap convert ( IBeatmap beatmap )
{
2022-01-27 14:19:48 +08:00
switch ( beatmap . BeatmapInfo . Ruleset . OnlineID )
2020-04-21 13:47:12 +08:00
{
case 0 :
beatmap . BeatmapInfo . Ruleset = new OsuRuleset ( ) . RulesetInfo ;
break ;
case 1 :
beatmap . BeatmapInfo . Ruleset = new TaikoRuleset ( ) . RulesetInfo ;
break ;
case 2 :
beatmap . BeatmapInfo . Ruleset = new CatchRuleset ( ) . RulesetInfo ;
break ;
case 3 :
beatmap . BeatmapInfo . Ruleset = new ManiaRuleset ( ) . RulesetInfo ;
break ;
}
return new TestWorkingBeatmap ( beatmap ) . GetPlayableBeatmap ( beatmap . BeatmapInfo . Ruleset ) ;
}
private class TestWorkingBeatmap : WorkingBeatmap
{
private readonly IBeatmap beatmap ;
public TestWorkingBeatmap ( IBeatmap beatmap )
: base ( beatmap . BeatmapInfo , null )
{
this . beatmap = beatmap ;
}
protected override IBeatmap GetBeatmap ( ) = > beatmap ;
2023-06-08 15:17:44 +08:00
public override Texture GetBackground ( ) = > throw new NotImplementedException ( ) ;
2020-04-21 13:47:12 +08:00
2020-08-07 21:31:41 +08:00
protected override Track GetBeatmapTrack ( ) = > throw new NotImplementedException ( ) ;
2021-04-17 23:47:13 +08:00
2021-08-16 00:38:01 +08:00
protected internal override ISkin GetSkin ( ) = > throw new NotImplementedException ( ) ;
2021-05-22 01:21:00 +08:00
2021-04-17 23:47:13 +08:00
public override Stream GetStream ( string storagePath ) = > throw new NotImplementedException ( ) ;
2020-04-21 13:47:12 +08:00
}
2019-12-10 19:44:45 +08:00
}
}