2019-01-24 16:43: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.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
2018-03-01 23:07:02 +08:00
using System ;
using System.Collections.Generic ;
using System.IO ;
2019-07-31 18:48:50 +08:00
using System.Linq ;
2018-03-01 23:07:02 +08:00
using System.Reflection ;
2022-01-03 17:00:28 +08:00
using System.Threading.Tasks ;
2018-03-01 23:07:02 +08:00
using Newtonsoft.Json ;
using NUnit.Framework ;
2019-07-31 18:48:50 +08:00
using osu.Framework.Audio.Track ;
2022-01-03 17:00:28 +08:00
using osu.Framework.Extensions ;
2021-05-15 05:50:25 +08:00
using osu.Framework.Extensions.ObjectExtensions ;
2019-07-31 18:48:50 +08:00
using osu.Framework.Graphics.Textures ;
2018-03-01 23:07:02 +08:00
using osu.Game.Beatmaps ;
using osu.Game.Beatmaps.Formats ;
2019-09-10 06:43:30 +08:00
using osu.Game.IO ;
2021-10-09 21:00:47 +08:00
using osu.Game.IO.Serialization ;
2018-05-07 13:04:37 +08:00
using osu.Game.Rulesets ;
2019-07-31 18:48:50 +08:00
using osu.Game.Rulesets.Mods ;
2018-03-01 23:07:02 +08:00
using osu.Game.Rulesets.Objects ;
2021-05-22 01:21:00 +08:00
using osu.Game.Skinning ;
2018-04-13 17:19:50 +08:00
2018-03-01 23:07:02 +08:00
namespace osu.Game.Tests.Beatmaps
{
[TestFixture]
2018-06-14 19:28:29 +08:00
public abstract class BeatmapConversionTest < TConvertMapping , TConvertValue >
where TConvertMapping : ConvertMapping < TConvertValue > , IEquatable < TConvertMapping > , new ( )
2018-03-01 23:07:02 +08:00
where TConvertValue : IEquatable < TConvertValue >
{
private const string resource_namespace = "Testing.Beatmaps" ;
private const string expected_conversion_suffix = "-expected-conversion" ;
2018-04-13 17:19:50 +08:00
2018-03-01 23:07:02 +08:00
protected abstract string ResourceAssembly { get ; }
2018-04-13 17:19:50 +08:00
2019-07-31 18:49:25 +08:00
protected void Test ( string name , params Type [ ] mods )
2018-03-01 23:07:02 +08:00
{
2019-07-31 18:49:25 +08:00
var ourResult = convert ( name , mods . Select ( m = > ( Mod ) Activator . CreateInstance ( m ) ) . ToArray ( ) ) ;
2018-03-02 17:20:33 +08:00
var expectedResult = read ( name ) ;
2018-04-13 17:19:50 +08:00
2020-10-09 19:12:17 +08:00
foreach ( var m in ourResult . Mappings )
m . PostProcess ( ) ;
foreach ( var m in expectedResult . Mappings )
m . PostProcess ( ) ;
2018-03-01 23:07:02 +08:00
Assert . Multiple ( ( ) = >
{
int mappingCounter = 0 ;
2019-04-01 11:16:05 +08:00
2018-03-01 23:07:02 +08:00
while ( true )
{
if ( mappingCounter > = ourResult . Mappings . Count & & mappingCounter > = expectedResult . Mappings . Count )
break ;
2019-02-28 12:31:40 +08:00
2018-03-01 23:07:02 +08:00
if ( mappingCounter > = ourResult . Mappings . Count )
2018-03-02 00:40:25 +08:00
Assert . Fail ( $"A conversion did not generate any hitobjects, but should have, for hitobject at time: {expectedResult.Mappings[mappingCounter].StartTime}\n" ) ;
2018-03-01 23:07:02 +08:00
else if ( mappingCounter > = expectedResult . Mappings . Count )
2018-03-02 00:40:25 +08:00
Assert . Fail ( $"A conversion generated hitobjects, but should not have, for hitobject at time: {ourResult.Mappings[mappingCounter].StartTime}\n" ) ;
2018-06-15 17:20:28 +08:00
else if ( ! expectedResult . Mappings [ mappingCounter ] . Equals ( ourResult . Mappings [ mappingCounter ] ) )
{
var expectedMapping = expectedResult . Mappings [ mappingCounter ] ;
var ourMapping = ourResult . Mappings [ mappingCounter ] ;
Assert . Fail ( $"The conversion mapping differed for object at time {expectedMapping.StartTime}:\n"
+ $"Expected {JsonConvert.SerializeObject(expectedMapping)}\n"
+ $"Received: {JsonConvert.SerializeObject(ourMapping)}\n" ) ;
}
2018-03-01 23:07:02 +08:00
else
{
2018-06-15 17:20:28 +08:00
var ourMapping = ourResult . Mappings [ mappingCounter ] ;
var expectedMapping = expectedResult . Mappings [ mappingCounter ] ;
2018-03-01 23:07:02 +08:00
Assert . Multiple ( ( ) = >
{
int objectCounter = 0 ;
2019-04-01 11:16:05 +08:00
2018-03-01 23:07:02 +08:00
while ( true )
{
if ( objectCounter > = ourMapping . Objects . Count & & objectCounter > = expectedMapping . Objects . Count )
break ;
2019-02-28 12:31:40 +08:00
2018-03-01 23:07:02 +08:00
if ( objectCounter > = ourMapping . Objects . Count )
2019-11-11 19:53:22 +08:00
{
2018-03-02 00:40:25 +08:00
Assert . Fail ( $"The conversion did not generate a hitobject, but should have, for hitobject at time: {expectedMapping.StartTime}:\n"
+ $"Expected: {JsonConvert.SerializeObject(expectedMapping.Objects[objectCounter])}\n" ) ;
2019-11-11 19:53:22 +08:00
}
2018-03-01 23:07:02 +08:00
else if ( objectCounter > = expectedMapping . Objects . Count )
2019-11-11 19:53:22 +08:00
{
2018-03-02 00:40:25 +08:00
Assert . Fail ( $"The conversion generated a hitobject, but should not have, for hitobject at time: {ourMapping.StartTime}:\n"
+ $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n" ) ;
2019-11-11 19:53:22 +08:00
}
2018-06-14 19:28:29 +08:00
else if ( ! expectedMapping . Objects [ objectCounter ] . Equals ( ourMapping . Objects [ objectCounter ] ) )
2018-03-01 23:07:02 +08:00
{
2018-06-14 19:28:29 +08:00
Assert . Fail ( $"The conversion generated differing hitobjects for object at time: {expectedMapping.StartTime}:\n"
2018-03-01 23:07:02 +08:00
+ $"Expected: {JsonConvert.SerializeObject(expectedMapping.Objects[objectCounter])}\n"
+ $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n" ) ;
}
2018-04-13 17:19:50 +08:00
2018-03-01 23:07:02 +08:00
objectCounter + + ;
}
} ) ;
}
2018-04-13 17:19:50 +08:00
2018-03-01 23:07:02 +08:00
mappingCounter + + ;
}
} ) ;
}
2018-04-13 17:19:50 +08:00
2019-07-31 18:49:25 +08:00
private ConvertResult convert ( string name , Mod [ ] mods )
2018-03-01 23:07:02 +08:00
{
2022-01-03 17:00:28 +08:00
var conversionTask = Task . Factory . StartNew ( ( ) = >
{
var beatmap = GetBeatmap ( name ) ;
2018-04-13 17:19:50 +08:00
2022-01-03 17:00:28 +08:00
string beforeConversion = beatmap . Serialize ( ) ;
2021-10-09 21:00:47 +08:00
2022-01-03 17:00:28 +08:00
var converterResult = new Dictionary < HitObject , IEnumerable < HitObject > > ( ) ;
2018-06-13 17:38:27 +08:00
2022-01-03 17:00:28 +08:00
var working = new ConversionWorkingBeatmap ( beatmap )
2019-07-31 18:48:50 +08:00
{
2022-01-03 17:00:28 +08:00
ConversionGenerated = ( o , r , c ) = >
{
converterResult [ o ] = r ;
OnConversionGenerated ( o , r , c ) ;
}
} ;
2018-06-14 19:26:55 +08:00
2022-01-03 17:00:28 +08:00
working . GetPlayableBeatmap ( CreateRuleset ( ) . RulesetInfo , mods ) ;
2018-06-14 19:28:29 +08:00
2022-01-03 17:00:28 +08:00
string afterConversion = beatmap . Serialize ( ) ;
2021-10-09 21:00:47 +08:00
2022-01-03 17:00:28 +08:00
Assert . AreEqual ( beforeConversion , afterConversion , "Conversion altered original beatmap" ) ;
2021-10-09 21:00:47 +08:00
2022-01-03 17:00:28 +08:00
return new ConvertResult
2019-07-31 18:48:50 +08:00
{
2022-01-03 17:00:28 +08:00
Mappings = converterResult . Select ( r = >
{
var mapping = CreateConvertMapping ( r . Key ) ;
mapping . StartTime = r . Key . StartTime ;
mapping . Objects . AddRange ( r . Value . SelectMany ( CreateConvertValue ) ) ;
return mapping ;
} ) . ToList ( )
} ;
} , TaskCreationOptions . LongRunning ) ;
if ( ! conversionTask . Wait ( 10000 ) )
Assert . Fail ( "Conversion timed out" ) ;
2022-01-06 21:54:43 +08:00
return conversionTask . GetResultSafely ( ) ;
2019-07-31 18:48:50 +08:00
}
2018-06-13 17:38:27 +08:00
2019-07-31 18:48:50 +08:00
protected virtual void OnConversionGenerated ( HitObject original , IEnumerable < HitObject > result , IBeatmapConverter beatmapConverter )
{
2018-03-01 23:07:02 +08:00
}
2018-04-13 17:19:50 +08:00
2018-03-02 17:20:33 +08:00
private ConvertResult read ( string name )
2018-03-01 23:07:02 +08:00
{
2018-03-02 17:20:33 +08:00
using ( var resStream = openResource ( $"{resource_namespace}.{name}{expected_conversion_suffix}.json" ) )
2018-03-01 23:07:02 +08:00
using ( var reader = new StreamReader ( resStream ) )
{
2021-10-27 12:04:41 +08:00
string contents = reader . ReadToEnd ( ) ;
2018-03-01 23:07:02 +08:00
return JsonConvert . DeserializeObject < ConvertResult > ( contents ) ;
}
}
2018-04-13 17:19:50 +08:00
2020-05-19 22:28:13 +08:00
public IBeatmap GetBeatmap ( string name )
2018-03-01 23:07:02 +08:00
{
2018-03-02 17:20:33 +08:00
using ( var resStream = openResource ( $"{resource_namespace}.{name}.osu" ) )
2019-09-10 06:43:30 +08:00
using ( var stream = new LineBufferedReader ( resStream ) )
2018-03-06 00:39:01 +08:00
{
2018-03-09 20:23:03 +08:00
var decoder = Decoder . GetDecoder < Beatmap > ( stream ) ;
2018-03-06 00:39:01 +08:00
( ( LegacyBeatmapDecoder ) decoder ) . ApplyOffsets = false ;
2020-05-19 22:28:13 +08:00
var beatmap = decoder . Decode ( stream ) ;
2020-05-20 09:06:23 +08:00
var rulesetInstance = CreateRuleset ( ) ;
2022-01-27 14:19:48 +08:00
beatmap . BeatmapInfo . Ruleset = beatmap . BeatmapInfo . Ruleset . OnlineID = = rulesetInstance . RulesetInfo . OnlineID ? rulesetInstance . RulesetInfo : new RulesetInfo ( ) ;
2020-05-19 22:28:13 +08:00
return beatmap ;
2018-03-06 00:39:01 +08:00
}
2018-03-01 23:07:02 +08:00
}
2018-04-13 17:19:50 +08:00
2018-03-01 23:07:02 +08:00
private Stream openResource ( string name )
{
2022-12-16 17:16:26 +08:00
string localPath = Path . GetDirectoryName ( Uri . UnescapeDataString ( new UriBuilder ( Assembly . GetExecutingAssembly ( ) . Location ) . Path ) ) . AsNonNull ( ) ;
2018-03-01 23:07:02 +08:00
return Assembly . LoadFrom ( Path . Combine ( localPath , $"{ResourceAssembly}.dll" ) ) . GetManifestResourceStream ( $@"{ResourceAssembly}.Resources.{name}" ) ;
}
2018-04-13 17:19:50 +08:00
2018-06-14 19:28:29 +08:00
/// <summary>
/// Creates the conversion mapping for a <see cref="HitObject"/>. A conversion mapping stores important information about the conversion process.
/// This is generated _after_ the <see cref="HitObject"/> has been converted.
/// <para>
/// This should be used to validate the integrity of the conversion process after a conversion has occurred.
/// </para>
/// </summary>
2019-07-31 18:48:50 +08:00
protected virtual TConvertMapping CreateConvertMapping ( HitObject source ) = > new TConvertMapping ( ) ;
2018-06-14 19:28:29 +08:00
/// <summary>
/// Creates the conversion value for a <see cref="HitObject"/>. A conversion value stores information about the converted <see cref="HitObject"/>.
/// <para>
/// This should be used to validate the integrity of the converted <see cref="HitObject"/>.
/// </para>
/// </summary>
/// <param name="hitObject">The converted <see cref="HitObject"/>.</param>
2018-03-02 01:02:09 +08:00
protected abstract IEnumerable < TConvertValue > CreateConvertValue ( HitObject hitObject ) ;
2018-04-13 17:19:50 +08:00
2018-06-14 19:28:29 +08:00
/// <summary>
/// Creates the <see cref="Ruleset"/> applicable to this <see cref="BeatmapConversionTest{TConvertMapping,TConvertValue}"/>.
/// </summary>
protected abstract Ruleset CreateRuleset ( ) ;
2018-04-13 17:19:50 +08:00
2018-03-01 23:07:02 +08:00
private class ConvertResult
{
[JsonProperty]
2018-06-14 19:28:29 +08:00
public List < TConvertMapping > Mappings = new List < TConvertMapping > ( ) ;
2018-03-01 23:07:02 +08:00
}
2019-07-31 18:48:50 +08:00
private class ConversionWorkingBeatmap : WorkingBeatmap
{
public Action < HitObject , IEnumerable < HitObject > , IBeatmapConverter > ConversionGenerated ;
private readonly IBeatmap beatmap ;
public ConversionWorkingBeatmap ( IBeatmap beatmap )
: base ( beatmap . BeatmapInfo , null )
{
this . beatmap = beatmap ;
}
protected override IBeatmap GetBeatmap ( ) = > beatmap ;
protected override Texture GetBackground ( ) = > throw new NotImplementedException ( ) ;
2020-08-07 21:31:41 +08:00
protected override Track GetBeatmapTrack ( ) = > throw new NotImplementedException ( ) ;
2019-07-31 18:48:50 +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 ( ) ;
2019-07-31 18:48:50 +08:00
protected override IBeatmapConverter CreateBeatmapConverter ( IBeatmap beatmap , Ruleset ruleset )
{
var converter = base . CreateBeatmapConverter ( beatmap , ruleset ) ;
converter . ObjectConverted + = ( orig , converted ) = > ConversionGenerated ? . Invoke ( orig , converted , converter ) ;
return converter ;
}
}
2018-03-01 23:07:02 +08:00
}
2018-06-14 19:28:29 +08:00
public abstract class BeatmapConversionTest < TConvertValue > : BeatmapConversionTest < ConvertMapping < TConvertValue > , TConvertValue >
where TConvertValue : IEquatable < TConvertValue >
{
}
public class ConvertMapping < TConvertValue > : IEquatable < ConvertMapping < TConvertValue > >
where TConvertValue : IEquatable < TConvertValue >
{
[JsonProperty]
public double StartTime ;
[JsonIgnore]
public List < TConvertValue > Objects = new List < TConvertValue > ( ) ;
[JsonProperty("Objects")]
2019-02-28 12:31:40 +08:00
private List < TConvertValue > setObjects
{
set = > Objects = value ;
}
2018-06-14 19:28:29 +08:00
2020-10-09 19:12:17 +08:00
/// <summary>
/// Invoked after this <see cref="ConvertMapping{TConvertValue}"/> is populated to post-process the contained data.
/// </summary>
public virtual void PostProcess ( )
{
}
2019-12-03 20:16:41 +08:00
public virtual bool Equals ( ConvertMapping < TConvertValue > other ) = > StartTime = = other ? . StartTime ;
2018-06-14 19:28:29 +08:00
}
2018-03-01 23:07:02 +08:00
}