From b584178e851fa26da60a3027ec3b52708b8bd39a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 6 Dec 2017 00:37:37 +0900 Subject: [PATCH] Make Beatmap ISerializable and add more JsonIgnores --- osu.Game.Rulesets.Mania/Objects/Note.cs | 2 + .../osu.Game.Rulesets.Mania.csproj | 4 + .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 153 ++++++++++++++++++ osu.Game.Tests/osu.Game.Tests.csproj | 1 + osu.Game/Beatmaps/Beatmap.cs | 8 +- osu.Game/Beatmaps/BeatmapDifficulty.cs | 2 + osu.Game/Beatmaps/BeatmapInfo.cs | 7 + osu.Game/Beatmaps/BeatmapMetadata.cs | 5 + .../ControlPoints/ControlPointInfo.cs | 6 +- .../Converters/TypedListConverter.cs | 83 ++++++++++ .../Converters/Vector2Converter.cs | 35 ++++ .../IO/Serialization/IJsonSerializable.cs | 26 +-- osu.Game/Rulesets/RulesetInfo.cs | 3 + osu.Game/osu.Game.csproj | 2 + 14 files changed, 322 insertions(+), 15 deletions(-) create mode 100644 osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs create mode 100644 osu.Game/IO/Serialization/Converters/TypedListConverter.cs create mode 100644 osu.Game/IO/Serialization/Converters/Vector2Converter.cs diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 3c4ff4216f..c4d5a13352 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // 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 /// /// The key-press hit window for this note. /// + [JsonIgnore] public HitWindows HitWindows { get; protected set; } = new HitWindows(); public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index ec6f59b5be..1e11e2e694 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -40,6 +40,10 @@ $(SolutionDir)\packages\OpenTK.3.0.0-git00009\lib\net20\OpenTK.dll True + + $(SolutionDir)\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + True + diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs new file mode 100644 index 0000000000..1531deb265 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -0,0 +1,153 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// 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)); + } + + /// + /// Reads a .osu file first with a , serializes the resulting to JSON + /// and then deserializes the result back into a through an . + /// + /// The .osu file to decode. + /// The after being decoded by an . + 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); + } + } + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 092ccfb9b5..60ce67c7f6 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -83,6 +83,7 @@ + diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 35b6cc2b02..c331872dda 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -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 { /// /// A Beatmap containing converted HitObjects. /// - public class Beatmap + public class Beatmap : IJsonSerializable where T : HitObject { public BeatmapInfo BeatmapInfo = new BeatmapInfo(); public ControlPointInfo ControlPointInfo = new ControlPointInfo(); public List Breaks = new List(); - public readonly List ComboColors = new List + public List ComboColors = new List { 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))] /// /// The HitObjects this Beatmap contains. /// diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 0b0fca8292..03fbf9a0a7 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -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; diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 022d64db03..6b5f7cb150 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -15,6 +15,7 @@ namespace osu.Game.Beatmaps public class BeatmapInfo : IEquatable, 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; } /// @@ -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) { diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 89f9ebf47a..2efbcd4f15 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -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 Beatmaps { get; set; } + + [JsonIgnore] public List BeatmapSets { get; set; } /// @@ -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, diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index e46eb8e20a..f24e25f112 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -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 /// The timing control point. public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.FirstOrDefault()); + [JsonIgnore] /// /// Finds the maximum BPM represented by any timing control point. /// public double BPMMaximum => 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength; + [JsonIgnore] /// /// Finds the minimum BPM represented by any timing control point. /// public double BPMMinimum => 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength; + [JsonIgnore] /// /// Finds the mode BPM (most common BPM) represented by the control points. /// @@ -108,4 +112,4 @@ namespace osu.Game.Beatmaps.ControlPoints return list[index - 1]; } } -} \ No newline at end of file +} diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs new file mode 100644 index 0000000000..1d617422e7 --- /dev/null +++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs @@ -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 : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(List); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var list = new List(); + + var localSerializer = createLocalSerializer(); + + var obj = JObject.Load(reader); + + var lookupTable = new List(); + 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)value; + + var localSerializer = createLocalSerializer(); + + var lookupTable = new List(); + var objects = new List(); + 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)).ToArray(); + return JsonSerializer.Create(localSettings); + } + } +} diff --git a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs b/osu.Game/IO/Serialization/Converters/Vector2Converter.cs new file mode 100644 index 0000000000..4ec27311bc --- /dev/null +++ b/osu.Game/IO/Serialization/Converters/Vector2Converter.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// 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(); + } + } +} diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs index e725742726..8d10f0b291 100644 --- a/osu.Game/IO/Serialization/IJsonSerializable.cs +++ b/osu.Game/IO/Serialization/IJsonSerializable.cs @@ -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(this string objString) - { - return JsonConvert.DeserializeObject(objString); - } + public static T Deserialize(this string objString) => JsonConvert.DeserializeObject(objString, CreateGlobalSettings()); - public static T DeepClone(this T obj) - where T : IJsonSerializable + public static void DeserializeInto(this string objString, T target) => JsonConvert.PopulateObject(objString, target, CreateGlobalSettings()); + + public static T DeepClone(this T obj) where T : IJsonSerializable => Deserialize(Serialize(obj)); + + public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings { - return Deserialize(Serialize(obj)); - } + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + Formatting = Formatting.Indented, + ObjectCreationHandling = ObjectCreationHandling.Replace, + DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, + Converters = new JsonConverter[] { new Vector2Converter() } + }; } } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 17f07158df..e6e0b98293 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -3,18 +3,21 @@ using System; using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; namespace osu.Game.Rulesets { public class RulesetInfo : IEquatable { [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); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4ca6123f04..f50e87b074 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -403,6 +403,8 @@ + +