1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 07:22:54 +08:00

Make Beatmap ISerializable and add more JsonIgnores

This commit is contained in:
smoogipoo 2017-12-06 00:37:37 +09:00
parent 58777a9674
commit b584178e85
14 changed files with 322 additions and 15 deletions

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Judgements;
@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary>
/// The key-press hit window for this note.
/// </summary>
[JsonIgnore]
public HitWindows HitWindows { get; protected set; } = new HitWindows();
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)

View File

@ -40,6 +40,10 @@
<HintPath>$(SolutionDir)\packages\OpenTK.3.0.0-git00009\lib\net20\OpenTK.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
</ItemGroup>

View File

@ -0,0 +1,153 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Resources;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Tests.Beatmaps.Formats
{
[TestFixture]
public class OsuJsonDecoderTest
{
[Test]
public void TestDecodeMetadata()
{
var beatmap = decodeAsJson("Soleily - Renatus (Gamu) [Insane].osu");
var meta = beatmap.BeatmapInfo.Metadata;
Assert.AreEqual(241526, meta.OnlineBeatmapSetID);
Assert.AreEqual("Soleily", meta.Artist);
Assert.AreEqual("Soleily", meta.ArtistUnicode);
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile);
Assert.AreEqual("Gamu", meta.AuthorString);
Assert.AreEqual("machinetop_background.jpg", meta.BackgroundFile);
Assert.AreEqual(164471, meta.PreviewTime);
Assert.AreEqual(string.Empty, meta.Source);
Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", meta.Tags);
Assert.AreEqual("Renatus", meta.Title);
Assert.AreEqual("Renatus", meta.TitleUnicode);
}
[Test]
public void TestDecodeGeneral()
{
var beatmap = decodeAsJson("Soleily - Renatus (Gamu) [Insane].osu");
var beatmapInfo = beatmap.BeatmapInfo;
Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
Assert.AreEqual(false, beatmapInfo.Countdown);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
Assert.AreEqual(false, beatmapInfo.SpecialStyle);
Assert.IsTrue(beatmapInfo.RulesetID == 0);
Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks);
Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard);
}
[Test]
public void TestDecodeEditor()
{
var beatmap = decodeAsJson("Soleily - Renatus (Gamu) [Insane].osu");
var beatmapInfo = beatmap.BeatmapInfo;
int[] expectedBookmarks =
{
11505, 22054, 32604, 43153, 53703, 64252, 74802, 85351,
95901, 106450, 116999, 119637, 130186, 140735, 151285,
161834, 164471, 175020, 185570, 196119, 206669, 209306
};
Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length);
for (int i = 0; i < expectedBookmarks.Length; i++)
Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]);
Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing);
Assert.AreEqual(4, beatmapInfo.BeatDivisor);
Assert.AreEqual(4, beatmapInfo.GridSize);
Assert.AreEqual(2, beatmapInfo.TimelineZoom);
}
[Test]
public void TestDecodeDifficulty()
{
var beatmap = decodeAsJson("Soleily - Renatus (Gamu) [Insane].osu");
var difficulty = beatmap.BeatmapInfo.BaseDifficulty;
Assert.AreEqual(6.5f, difficulty.DrainRate);
Assert.AreEqual(4, difficulty.CircleSize);
Assert.AreEqual(8, difficulty.OverallDifficulty);
Assert.AreEqual(9, difficulty.ApproachRate);
Assert.AreEqual(1.8f, difficulty.SliderMultiplier);
Assert.AreEqual(2, difficulty.SliderTickRate);
}
[Test]
public void TestDecodeColors()
{
var beatmap = decodeAsJson("Soleily - Renatus (Gamu) [Insane].osu");
Color4[] expected =
{
new Color4(142, 199, 255, 255),
new Color4(255, 128, 128, 255),
new Color4(128, 255, 255, 255),
new Color4(128, 255, 128, 255),
new Color4(255, 187, 255, 255),
new Color4(255, 177, 140, 255),
};
Assert.AreEqual(expected.Length, beatmap.ComboColors.Count);
for (int i = 0; i < expected.Length; i++)
Assert.AreEqual(expected[i], beatmap.ComboColors[i]);
}
[Test]
public void TestDecodeHitObjects()
{
var beatmap = decodeAsJson("Soleily - Renatus (Gamu) [Insane].osu");
var curveData = beatmap.HitObjects[0] as IHasCurve;
var positionData = beatmap.HitObjects[0] as IHasPosition;
Assert.IsNotNull(positionData);
Assert.IsNotNull(curveData);
Assert.AreEqual(new Vector2(192, 168), positionData.Position);
Assert.AreEqual(956, beatmap.HitObjects[0].StartTime);
Assert.IsTrue(beatmap.HitObjects[0].Samples.Any(s => s.Name == SampleInfo.HIT_NORMAL));
positionData = beatmap.HitObjects[1] as IHasPosition;
Assert.IsNotNull(positionData);
Assert.AreEqual(new Vector2(304, 56), positionData.Position);
Assert.AreEqual(1285, beatmap.HitObjects[1].StartTime);
Assert.IsTrue(beatmap.HitObjects[1].Samples.Any(s => s.Name == SampleInfo.HIT_CLAP));
}
/// <summary>
/// Reads a .osu file first with a <see cref="OsuLegacyDecoder"/>, serializes the resulting <see cref="Beatmap"/> to JSON
/// and then deserializes the result back into a <see cref="Beatmap"/> through an <see cref="OsuJsonDecoder"/>.
/// </summary>
/// <param name="filename">The .osu file to decode.</param>
/// <returns>The <see cref="Beatmap"/> after being decoded by an <see cref="OsuLegacyDecoder"/>.</returns>
private Beatmap decodeAsJson(string filename)
{
using (var stream = Resource.OpenResource(filename))
using (var sr = new StreamReader(stream))
{
var legacyDecoded = new OsuLegacyDecoder().Decode(sr);
using (var ms = new MemoryStream())
using (var sw = new StreamWriter(ms))
using (var sr2 = new StreamReader(ms))
{
sw.Write(legacyDecoded.Serialize());
sw.Flush();
ms.Position = 0;
return new OsuJsonDecoder().Decode(sr2);
}
}
}
}
}

View File

@ -83,6 +83,7 @@
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<ItemGroup>
<Compile Include="Beatmaps\Formats\OsuJsonDecoderTest.cs" />
<Compile Include="Beatmaps\IO\OszArchiveReaderTest.cs" />
<Compile Include="Beatmaps\IO\ImportBeatmapTest.cs" />
<Compile Include="Resources\Resource.cs" />

View File

@ -9,19 +9,21 @@ using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO.Serialization;
using osu.Game.Storyboards;
using Newtonsoft.Json;
using osu.Game.IO.Serialization.Converters;
namespace osu.Game.Beatmaps
{
/// <summary>
/// A Beatmap containing converted HitObjects.
/// </summary>
public class Beatmap<T>
public class Beatmap<T> : IJsonSerializable
where T : HitObject
{
public BeatmapInfo BeatmapInfo = new BeatmapInfo();
public ControlPointInfo ControlPointInfo = new ControlPointInfo();
public List<BreakPeriod> Breaks = new List<BreakPeriod>();
public readonly List<Color4> ComboColors = new List<Color4>
public List<Color4> ComboColors = new List<Color4>
{
new Color4(17, 136, 170, 255),
new Color4(102, 136, 0, 255),
@ -29,8 +31,10 @@ namespace osu.Game.Beatmaps
new Color4(121, 9, 13, 255)
};
[JsonIgnore]
public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata;
[JsonConverter(typeof(TypedListConverter<HitObject>))]
/// <summary>
/// The HitObjects this Beatmap contains.
/// </summary>

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
namespace osu.Game.Beatmaps
{
@ -13,6 +14,7 @@ namespace osu.Game.Beatmaps
public const float DEFAULT_DIFFICULTY = 5;
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[JsonIgnore]
public int ID { get; set; }
public float DrainRate { get; set; } = DEFAULT_DIFFICULTY;

View File

@ -15,6 +15,7 @@ namespace osu.Game.Beatmaps
public class BeatmapInfo : IEquatable<BeatmapInfo>, IJsonSerializable, IHasPrimaryKey
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[JsonIgnore]
public int ID { get; set; }
//TODO: should be in database
@ -38,13 +39,16 @@ namespace osu.Game.Beatmaps
set { onlineBeatmapSetID = value > 0 ? value : null; }
}
[JsonIgnore]
public int BeatmapSetInfoID { get; set; }
[Required]
[JsonIgnore]
public BeatmapSetInfo BeatmapSet { get; set; }
public BeatmapMetadata Metadata { get; set; }
[JsonIgnore]
public int BaseDifficultyID { get; set; }
public BeatmapDifficulty BaseDifficulty { get; set; }
@ -60,6 +64,7 @@ namespace osu.Game.Beatmaps
[JsonProperty("file_sha2")]
public string Hash { get; set; }
[JsonIgnore]
public bool Hidden { get; set; }
/// <summary>
@ -74,6 +79,7 @@ namespace osu.Game.Beatmaps
public float StackLeniency { get; set; }
public bool SpecialStyle { get; set; }
[JsonIgnore]
public int RulesetID { get; set; }
public RulesetInfo Ruleset { get; set; }
@ -116,6 +122,7 @@ namespace osu.Game.Beatmaps
public string Version { get; set; }
public double StarDifficulty { get; set; }
public bool ShouldSerializeStarDifficulty() => false;
public bool Equals(BeatmapInfo other)
{

View File

@ -12,6 +12,7 @@ namespace osu.Game.Beatmaps
public class BeatmapMetadata
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[JsonIgnore]
public int ID { get; set; }
private int? onlineBeatmapSetID;
@ -29,7 +30,10 @@ namespace osu.Game.Beatmaps
public string Artist { get; set; }
public string ArtistUnicode { get; set; }
[JsonIgnore]
public List<BeatmapInfo> Beatmaps { get; set; }
[JsonIgnore]
public List<BeatmapSetInfo> BeatmapSets { get; set; }
/// <summary>
@ -56,6 +60,7 @@ namespace osu.Game.Beatmaps
public string AudioFile { get; set; }
public string BackgroundFile { get; set; }
[JsonIgnore]
public string[] SearchableTerms => new[]
{
Author?.Username,

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Lists;
namespace osu.Game.Beatmaps.ControlPoints
@ -58,18 +59,21 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <returns>The timing control point.</returns>
public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.FirstOrDefault());
[JsonIgnore]
/// <summary>
/// Finds the maximum BPM represented by any timing control point.
/// </summary>
public double BPMMaximum =>
60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
[JsonIgnore]
/// <summary>
/// Finds the minimum BPM represented by any timing control point.
/// </summary>
public double BPMMinimum =>
60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
[JsonIgnore]
/// <summary>
/// Finds the mode BPM (most common BPM) represented by the control points.
/// </summary>
@ -108,4 +112,4 @@ namespace osu.Game.Beatmaps.ControlPoints
return list[index - 1];
}
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace osu.Game.IO.Serialization.Converters
{
public class TypedListConverter<T> : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(List<T>);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var list = new List<T>();
var localSerializer = createLocalSerializer();
var obj = JObject.Load(reader);
var lookupTable = new List<string>();
localSerializer.Populate(obj["LookupTable"].CreateReader(), lookupTable);
foreach (var tok in obj["Items"])
{
var itemReader = tok.CreateReader();
var typeName = lookupTable[(int)tok["Type"]];
var instance = (T)Activator.CreateInstance(Type.GetType(typeName));
localSerializer.Populate(itemReader, instance);
list.Add(instance);
}
return list;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var list = (List<T>)value;
var localSerializer = createLocalSerializer();
var lookupTable = new List<string>();
var objects = new List<JObject>();
foreach (var item in list)
{
var type = item.GetType().AssemblyQualifiedName;
int typeId = lookupTable.IndexOf(type);
if (typeId == -1)
{
lookupTable.Add(type);
typeId = lookupTable.Count - 1;
}
var itemObject = JObject.FromObject(item, localSerializer);
itemObject.AddFirst(new JProperty("Type", typeId));
objects.Add(itemObject);
}
writer.WriteStartObject();
writer.WritePropertyName("LookupTable");
localSerializer.Serialize(writer, lookupTable);
writer.WritePropertyName("Items");
writer.WriteStartArray();
foreach (var item in objects)
item.WriteTo(writer);
writer.WriteEndArray();
writer.WriteEndObject();
}
private JsonSerializer createLocalSerializer()
{
var localSettings = JsonSerializableExtensions.CreateGlobalSettings();
localSettings.Converters = localSettings.Converters.Where(c => !(c is TypedListConverter<T>)).ToArray();
return JsonSerializer.Create(localSettings);
}
}
}

View File

@ -0,0 +1,35 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenTK;
namespace osu.Game.IO.Serialization.Converters
{
public class Vector2Converter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(Vector2);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var obj = JObject.Load(reader);
return new Vector2((float)obj["X"], (float)obj["Y"]);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var vector = (Vector2)value;
writer.WriteStartObject();
writer.WritePropertyName("X");
writer.WriteValue(vector.X);
writer.WritePropertyName("Y");
writer.WriteValue(vector.Y);
writer.WriteEndObject();
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Newtonsoft.Json;
using osu.Game.IO.Serialization.Converters;
namespace osu.Game.IO.Serialization
{
@ -11,20 +12,21 @@ namespace osu.Game.IO.Serialization
public static class JsonSerializableExtensions
{
public static string Serialize(this IJsonSerializable obj)
{
return JsonConvert.SerializeObject(obj, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore });
}
public static string Serialize(this IJsonSerializable obj) => JsonConvert.SerializeObject(obj, CreateGlobalSettings());
public static T Deserialize<T>(this string objString)
{
return JsonConvert.DeserializeObject<T>(objString);
}
public static T Deserialize<T>(this string objString) => JsonConvert.DeserializeObject<T>(objString, CreateGlobalSettings());
public static T DeepClone<T>(this T obj)
where T : IJsonSerializable
public static void DeserializeInto<T>(this string objString, T target) => JsonConvert.PopulateObject(objString, target, CreateGlobalSettings());
public static T DeepClone<T>(this T obj) where T : IJsonSerializable => Deserialize<T>(Serialize(obj));
public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings
{
return Deserialize<T>(Serialize(obj));
}
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
Formatting = Formatting.Indented,
ObjectCreationHandling = ObjectCreationHandling.Replace,
DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
Converters = new JsonConverter[] { new Vector2Converter() }
};
}
}

View File

@ -3,18 +3,21 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
namespace osu.Game.Rulesets
{
public class RulesetInfo : IEquatable<RulesetInfo>
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[JsonIgnore]
public int? ID { get; set; }
public string Name { get; set; }
public string InstantiationInfo { get; set; }
[JsonIgnore]
public bool Available { get; set; }
public virtual Ruleset CreateInstance() => (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo), this);

View File

@ -403,6 +403,8 @@
<Compile Include="IO\Legacy\ILegacySerializable.cs" />
<Compile Include="IO\Legacy\SerializationReader.cs" />
<Compile Include="IO\Legacy\SerializationWriter.cs" />
<Compile Include="IO\Serialization\Converters\TypedListConverter.cs" />
<Compile Include="IO\Serialization\Converters\Vector2Converter.cs" />
<Compile Include="IO\Serialization\IJsonSerializable.cs" />
<Compile Include="IPC\BeatmapIPCChannel.cs" />
<Compile Include="IPC\ScoreIPCChannel.cs" />