From 2bc7458abf98113f38d43212201193caa01cb54f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Jan 2020 13:27:47 +0900 Subject: [PATCH] Add mod setting (de)serialization support --- .../Online/TestAPIModSerialization.cs | 82 +++++++++++++++++++ .../Configuration/SettingSourceAttribute.cs | 26 ++++-- osu.Game/Online/API/APIMod.cs | 56 +++++++++++++ .../Online/API/Requests/Responses/APIMod.cs | 14 ---- osu.Game/Online/Multiplayer/PlaylistItem.cs | 11 ++- 5 files changed, 162 insertions(+), 27 deletions(-) create mode 100644 osu.Game.Tests/Online/TestAPIModSerialization.cs create mode 100644 osu.Game/Online/API/APIMod.cs delete mode 100644 osu.Game/Online/API/Requests/Responses/APIMod.cs diff --git a/osu.Game.Tests/Online/TestAPIModSerialization.cs b/osu.Game.Tests/Online/TestAPIModSerialization.cs new file mode 100644 index 0000000000..845f57136a --- /dev/null +++ b/osu.Game.Tests/Online/TestAPIModSerialization.cs @@ -0,0 +1,82 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using Newtonsoft.Json; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Online.API; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Tests.Online +{ + [TestFixture] + public class TestAPIModSerialization + { + [Test] + public void TestAcronymIsPreserved() + { + var apiMod = new APIMod(new TestMod()); + + var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); + + Assert.That(deserialized.Acronym, Is.EqualTo(apiMod.Acronym)); + } + + [Test] + public void TestRawSettingIsPreserved() + { + var apiMod = new APIMod(new TestMod { TestSetting = { Value = 2 } }); + + var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); + + Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(2.0)); + } + + [Test] + public void TestConvertedModHasCorrectSetting() + { + var apiMod = new APIMod(new TestMod { TestSetting = { Value = 2 } }); + + var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); + var converted = (TestMod)deserialized.ToMod(new TestRuleset()); + + Assert.That(converted.TestSetting.Value, Is.EqualTo(2)); + } + + private class TestRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) => new[] { new TestMod() }; + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException(); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new System.NotImplementedException(); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new System.NotImplementedException(); + + public override string Description { get; } + public override string ShortName { get; } + } + + private class TestMod : Mod + { + public override string Name => "Test Mod"; + public override string Acronym => "TM"; + public override double ScoreMultiplier => 1; + + [SettingSource("Test")] + public BindableNumber TestSetting { get; } = new BindableDouble + { + MinValue = 0, + MaxValue = 10, + Default = 5, + Precision = 0.01, + }; + } + } +} diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 056fa8bcc0..f859dccc80 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -35,16 +35,11 @@ namespace osu.Game.Configuration { public static IEnumerable CreateSettingsControls(this object obj) { - foreach (var property in obj.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance)) + foreach (var (attr, property) in obj.GetSettingsSourceProperties()) { - var attr = property.GetCustomAttribute(true); + object value = property.GetValue(obj); - if (attr == null) - continue; - - var prop = property.GetValue(obj); - - switch (prop) + switch (value) { case BindableNumber bNumber: yield return new SettingsSlider @@ -102,9 +97,22 @@ namespace osu.Game.Configuration break; default: - throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} was attached to an unsupported type ({prop})"); + throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} was attached to an unsupported type ({value})"); } } } + + public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj) + { + foreach (var property in obj.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance)) + { + var attr = property.GetCustomAttribute(true); + + if (attr == null) + continue; + + yield return (attr, property); + } + } } } diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs new file mode 100644 index 0000000000..30336d16a3 --- /dev/null +++ b/osu.Game/Online/API/APIMod.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using Newtonsoft.Json; +using osu.Framework.Bindables; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Online.API +{ + public class APIMod : IMod + { + [JsonProperty("acronym")] + public string Acronym { get; set; } + + [JsonProperty("settings")] + public Dictionary Settings { get; set; } = new Dictionary(); + + [JsonConstructor] + private APIMod() + { + } + + public APIMod(Mod mod) + { + Acronym = mod.Acronym; + + foreach (var (_, property) in mod.GetSettingsSourceProperties()) + Settings.Add(property.Name.Underscore(), property.GetValue(mod)); + } + + public Mod ToMod(Ruleset ruleset) + { + Mod resultMod = ruleset.GetAllMods().FirstOrDefault(m => m.Acronym == Acronym); + + if (resultMod == null) + return null; // Todo: Maybe throw exception? + + foreach (var (_, property) in resultMod.GetSettingsSourceProperties()) + { + if (!Settings.TryGetValue(property.Name.Underscore(), out object settingValue)) + continue; + + ((IBindable)property.GetValue(resultMod)).Parse(settingValue); + } + + return resultMod; + } + + public bool Equals(IMod other) => Acronym == other?.Acronym; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIMod.cs b/osu.Game/Online/API/Requests/Responses/APIMod.cs deleted file mode 100644 index b9da4f49ee..0000000000 --- a/osu.Game/Online/API/Requests/Responses/APIMod.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Online.API.Requests.Responses -{ - public class APIMod : IMod - { - public string Acronym { get; set; } - - public bool Equals(IMod other) => Acronym == other?.Acronym; - } -} diff --git a/osu.Game/Online/Multiplayer/PlaylistItem.cs b/osu.Game/Online/Multiplayer/PlaylistItem.cs index d13e8b31e6..5f8edc607b 100644 --- a/osu.Game/Online/Multiplayer/PlaylistItem.cs +++ b/osu.Game/Online/Multiplayer/PlaylistItem.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -50,7 +51,7 @@ namespace osu.Game.Online.Multiplayer [JsonProperty("allowed_mods")] private APIMod[] allowedMods { - get => AllowedMods.Select(m => new APIMod { Acronym = m.Acronym }).ToArray(); + get => AllowedMods.Select(m => new APIMod(m)).ToArray(); set => allowedModsBacking = value; } @@ -59,7 +60,7 @@ namespace osu.Game.Online.Multiplayer [JsonProperty("required_mods")] private APIMod[] requiredMods { - get => RequiredMods.Select(m => new APIMod { Acronym = m.Acronym }).ToArray(); + get => RequiredMods.Select(m => new APIMod(m)).ToArray(); set => requiredModsBacking = value; } @@ -72,10 +73,12 @@ namespace osu.Game.Online.Multiplayer Beatmap = apiBeatmap == null ? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == BeatmapID) : apiBeatmap.ToBeatmap(rulesets); Ruleset = rulesets.GetRuleset(RulesetID); + Ruleset rulesetInstance = Ruleset.CreateInstance(); + if (allowedModsBacking != null) { AllowedMods.Clear(); - AllowedMods.AddRange(Ruleset.CreateInstance().GetAllMods().Where(mod => allowedModsBacking.Any(m => m.Acronym == mod.Acronym))); + AllowedMods.AddRange(allowedModsBacking.Select(m => m.ToMod(rulesetInstance))); allowedModsBacking = null; } @@ -83,7 +86,7 @@ namespace osu.Game.Online.Multiplayer if (requiredModsBacking != null) { RequiredMods.Clear(); - RequiredMods.AddRange(Ruleset.CreateInstance().GetAllMods().Where(mod => requiredModsBacking.Any(m => m.Acronym == mod.Acronym))); + RequiredMods.AddRange(requiredModsBacking.Select(m => m.ToMod(rulesetInstance))); requiredModsBacking = null; }