From 191604340f8cbaf4d42c31cbc2ef2665fdfbb318 Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 21 Feb 2023 19:05:10 +0100 Subject: [PATCH 01/23] Added a way for mod settings to be kept when changing ruleset + test --- .../TestSceneModSelectOverlay.cs | 48 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 17 ++++++- osu.Game/Rulesets/Mods/Mod.cs | 31 ++++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 5ccaebd721..25edcd4e0a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Tests.Mods; using osuTK; using osuTK.Input; @@ -371,6 +372,53 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("no mod selected", () => SelectedMods.Value.Count == 0); } + [Test] + public void TestKeepSharedSettingsFromSimilarMods() + { + const float setting_change = 1.2f; + + createScreen(); + changeRuleset(0); + + AddStep("select difficulty adjust mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod()! }); + + changeRuleset(0); + AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModDifficultyAdjust); + + AddStep("change mod settings", () => + { + var osuMod = (getMod() as OsuModDifficultyAdjust)!; + osuMod.ExtendedLimits.Value = true; + osuMod.CircleSize.Value = setting_change; + osuMod.DrainRate.Value = setting_change; + osuMod.OverallDifficulty.Value = setting_change; + osuMod.ApproachRate.Value = setting_change; + }); + + changeRuleset(1); + AddAssert("taiko variant selected", () => SelectedMods.Value.SingleOrDefault() is TaikoModDifficultyAdjust); + + AddAssert("shared settings didn't change", () => + { + var taikoMod = getMod() as TaikoModDifficultyAdjust; + return + taikoMod?.ExtendedLimits.Value == true && + taikoMod?.DrainRate.Value == setting_change && + taikoMod?.OverallDifficulty.Value == setting_change; + }); + + AddAssert("non-shared settings at default", () => + { + var taikoMod = getMod() as TaikoModDifficultyAdjust; + if (taikoMod == null) + return false; + + return taikoMod.ScrollSpeed.Value == taikoMod.ScrollSpeed.Default; + }); + + ModDifficultyAdjust getMod() => (SelectedMods.Value.Single() as ModDifficultyAdjust)!; + } + [Test] public void TestExternallySetCustomizedMod() { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index cf58d07b9e..9644e3fc75 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -393,8 +393,11 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); Beatmap.BindValueChanged(onBeatmapChanged); + SelectedMods.BindValueChanged(change => Logger.Log($"{modSetting(change.OldValue)} => {modSetting(change.NewValue)}")); } + private object modSetting(IReadOnlyList mods) => mods.OfType().FirstOrDefault()?.GetSettingsSourceProperties().First().Item2.GetValue(mods.OfType().First())!; + private void addFilesWarning() { var realmStore = new RealmFileStore(realm, Storage); @@ -626,7 +629,8 @@ namespace osu.Game return; } - var previouslySelectedMods = SelectedMods.Value.ToArray(); + //for some reason emptying SelectedMods resets all SettingSources Bindables to default value + var previouslySelectedMods = SelectedMods.Value.Select(mod => mod.DeepClone()).ToArray(); if (!SelectedMods.Disabled) SelectedMods.Value = Array.Empty(); @@ -634,7 +638,16 @@ namespace osu.Game AvailableMods.Value = dict; if (!SelectedMods.Disabled) - SelectedMods.Value = previouslySelectedMods.Select(m => instance.CreateModFromAcronym(m.Acronym)).Where(m => m != null).ToArray(); + { + SelectedMods.Value = previouslySelectedMods.Select(oldMod => + { + Mod newMod = instance.CreateModFromAcronym(oldMod.Acronym); + + newMod?.CopyFromSimilar(oldMod); + + return newMod; + }).Where(m => m != null).ToArray(); + } void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First(); } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 04d55bc5fe..7cb647d223 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Rulesets.UI; using osu.Game.Utils; @@ -139,6 +140,30 @@ namespace osu.Game.Rulesets.Mods return result; } + /// + /// Copies all shared mod setting values from into this instance. + /// + /// The mod to copy properties from. + public void CopyFromSimilar(Mod source) + { + Dictionary oldSettings = new Dictionary(); + + foreach (var (_, property) in source.GetSettingsSourceProperties()) + { + oldSettings.Add(property.Name.ToSnakeCase(), property.GetValue(source)!); + } + + foreach (var (_, property) in this.GetSettingsSourceProperties()) + { + var targetBindable = (IBindable)property.GetValue(this)!; + + if (!oldSettings.TryGetValue(property.Name.ToSnakeCase(), out object? sourceSetting)) + continue; + + CopyAdjustedSetting(targetBindable, sourceSetting); + } + } + /// /// Copies mod setting values from into this instance, overwriting all existing settings. /// @@ -148,10 +173,10 @@ namespace osu.Game.Rulesets.Mods if (source.GetType() != GetType()) throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source)); - foreach (var (_, prop) in this.GetSettingsSourceProperties()) + foreach (var (_, property) in this.GetSettingsSourceProperties()) { - var targetBindable = (IBindable)prop.GetValue(this)!; - var sourceBindable = (IBindable)prop.GetValue(source)!; + var targetBindable = (IBindable)property.GetValue(this)!; + var sourceBindable = (IBindable)property.GetValue(source)!; CopyAdjustedSetting(targetBindable, sourceBindable); } From dd53a700711dd79fac8466fca94ea881db5fa537 Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 21 Feb 2023 20:59:36 +0100 Subject: [PATCH 02/23] Addressed change requests --- .../TestSceneModSelectOverlay.cs | 4 +- osu.Game/OsuGameBase.cs | 8 +--- osu.Game/Rulesets/Mods/Mod.cs | 40 +++++++++---------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 25edcd4e0a..7a46876737 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -403,8 +403,8 @@ namespace osu.Game.Tests.Visual.UserInterface var taikoMod = getMod() as TaikoModDifficultyAdjust; return taikoMod?.ExtendedLimits.Value == true && - taikoMod?.DrainRate.Value == setting_change && - taikoMod?.OverallDifficulty.Value == setting_change; + taikoMod.DrainRate.Value == setting_change && + taikoMod.OverallDifficulty.Value == setting_change; }); AddAssert("non-shared settings at default", () => diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 9644e3fc75..af2fb123d3 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -58,7 +58,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Utils; -using File = System.IO.File; using RuntimeInfo = osu.Framework.RuntimeInfo; namespace osu.Game @@ -393,11 +392,8 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); Beatmap.BindValueChanged(onBeatmapChanged); - SelectedMods.BindValueChanged(change => Logger.Log($"{modSetting(change.OldValue)} => {modSetting(change.NewValue)}")); } - private object modSetting(IReadOnlyList mods) => mods.OfType().FirstOrDefault()?.GetSettingsSourceProperties().First().Item2.GetValue(mods.OfType().First())!; - private void addFilesWarning() { var realmStore = new RealmFileStore(realm, Storage); @@ -629,7 +625,7 @@ namespace osu.Game return; } - //for some reason emptying SelectedMods resets all SettingSources Bindables to default value + //DeepCloning collection, because emptying SelectedMods resets all SettingSources Bindables to their default value var previouslySelectedMods = SelectedMods.Value.Select(mod => mod.DeepClone()).ToArray(); if (!SelectedMods.Disabled) @@ -643,7 +639,7 @@ namespace osu.Game { Mod newMod = instance.CreateModFromAcronym(oldMod.Acronym); - newMod?.CopyFromSimilar(oldMod); + newMod?.CopySharedSettings(oldMod); return newMod; }).Where(m => m != null).ToArray(); diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 7cb647d223..bc588a5cd8 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -141,10 +141,28 @@ namespace osu.Game.Rulesets.Mods } /// - /// Copies all shared mod setting values from into this instance. + /// Copies mod setting values from into this instance, overwriting all existing settings. /// /// The mod to copy properties from. - public void CopyFromSimilar(Mod source) + public void CopyFrom(Mod source) + { + if (source.GetType() != GetType()) + throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source)); + + foreach (var (_, property) in this.GetSettingsSourceProperties()) + { + var targetBindable = (IBindable)property.GetValue(this)!; + var sourceBindable = (IBindable)property.GetValue(source)!; + + CopyAdjustedSetting(targetBindable, sourceBindable); + } + } + + /// + /// Copies all mod setting values sharing same from into this instance. + /// + /// The mod to copy properties from. + internal void CopySharedSettings(Mod source) { Dictionary oldSettings = new Dictionary(); @@ -164,24 +182,6 @@ namespace osu.Game.Rulesets.Mods } } - /// - /// Copies mod setting values from into this instance, overwriting all existing settings. - /// - /// The mod to copy properties from. - public void CopyFrom(Mod source) - { - if (source.GetType() != GetType()) - throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source)); - - foreach (var (_, property) in this.GetSettingsSourceProperties()) - { - var targetBindable = (IBindable)property.GetValue(this)!; - var sourceBindable = (IBindable)property.GetValue(source)!; - - CopyAdjustedSetting(targetBindable, sourceBindable); - } - } - /// /// When creating copies or clones of a Mod, this method will be called /// to copy explicitly adjusted user settings from . From 82b07d19f86374a24128ee4e8ff7918bf1aa7acf Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 21 Feb 2023 21:48:11 +0100 Subject: [PATCH 03/23] Fix of incorrect `using` optimization --- osu.Game/OsuGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index af2fb123d3..e16aaf39ee 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -58,6 +58,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Utils; +using File = System.IO.File; using RuntimeInfo = osu.Framework.RuntimeInfo; namespace osu.Game From e321536acc2299c19b131b457838c84f15c1aed3 Mon Sep 17 00:00:00 2001 From: Terochi Date: Wed, 22 Feb 2023 07:48:43 +0100 Subject: [PATCH 04/23] Small clean up --- osu.Game/OsuGameBase.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e16aaf39ee..de1f2e810c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -626,17 +626,12 @@ namespace osu.Game return; } - //DeepCloning collection, because emptying SelectedMods resets all SettingSources Bindables to their default value - var previouslySelectedMods = SelectedMods.Value.Select(mod => mod.DeepClone()).ToArray(); - - if (!SelectedMods.Disabled) - SelectedMods.Value = Array.Empty(); - AvailableMods.Value = dict; if (!SelectedMods.Disabled) { - SelectedMods.Value = previouslySelectedMods.Select(oldMod => + //converting mods from one ruleset to the other, while also keeping their shared settings unchanged + SelectedMods.Value = SelectedMods.Value.Select(oldMod => { Mod newMod = instance.CreateModFromAcronym(oldMod.Acronym); From 09e7c21b23a71c8df297c4037af11f1d45046052 Mon Sep 17 00:00:00 2001 From: Terochi Date: Fri, 24 Feb 2023 15:11:22 +0100 Subject: [PATCH 05/23] Implemented a more complex setting conversion logic + tests --- osu.Game.Tests/Mods/ModSettingsTest.cs | 42 ++++++++++++ .../TestSceneModSelectOverlay.cs | 67 +++++++++++++++---- osu.Game/Rulesets/Mods/Mod.cs | 38 ++++++++++- 3 files changed, 133 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index b9ea1f2567..99198f6bae 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -2,6 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -32,5 +35,44 @@ namespace osu.Game.Tests.Mods Assert.That(((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value, Is.EqualTo(2.0)); Assert.That(((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value, Is.EqualTo(1.5)); } + + [Test] + public void TestCopySharedSettingsOfDifferentType() + { + const double setting_change = 2.5; + + var osuMod = new TestNonMatchinSettingTypeOsuMod(); + var maniaMod = new TestNonMatchinSettingTypeManiaMod(); + + osuMod.TestSetting.Value = setting_change; + maniaMod.CopySharedSettings(osuMod); + osuMod.CopySharedSettings(maniaMod); + + Assert.That(maniaMod.TestSetting.IsDefault, "Value has been changed"); + Assert.That(osuMod.TestSetting.Value == setting_change); + } + + private class TestNonMatchinSettingTypeOsuMod : TestNonMatchinSettingTypeMod + { + public override string Acronym => "NMO"; + public override BindableNumber TestSetting { get; } = new BindableDouble(3.5); + } + + private class TestNonMatchinSettingTypeManiaMod : TestNonMatchinSettingTypeMod + { + public override string Acronym => "NMM"; + public override Bindable TestSetting { get; } = new BindableBool(true); + } + + private abstract class TestNonMatchinSettingTypeMod : Mod + { + public override string Name => "Non-matching setting type mod"; + public override LocalisableString Description => "Description"; + public override double ScoreMultiplier => 1; + public override ModType Type => ModType.Conversion; + + [SettingSource("Test setting")] + public abstract IBindable TestSetting { get; } + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 7a46876737..a354a22fb7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -18,6 +18,7 @@ using osu.Game.Overlays.Mods; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -387,7 +388,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("change mod settings", () => { - var osuMod = (getMod() as OsuModDifficultyAdjust)!; + var osuMod = getMod(); + osuMod.ExtendedLimits.Value = true; osuMod.CircleSize.Value = setting_change; osuMod.DrainRate.Value = setting_change; @@ -400,23 +402,60 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("shared settings didn't change", () => { - var taikoMod = getMod() as TaikoModDifficultyAdjust; - return - taikoMod?.ExtendedLimits.Value == true && - taikoMod.DrainRate.Value == setting_change && - taikoMod.OverallDifficulty.Value == setting_change; + var taikoMod = getMod(); + + return taikoMod.ExtendedLimits.Value && + taikoMod.DrainRate.Value == setting_change && + taikoMod.OverallDifficulty.Value == setting_change; }); - AddAssert("non-shared settings at default", () => + AddAssert("non-shared settings unchanged", () => { - var taikoMod = getMod() as TaikoModDifficultyAdjust; - if (taikoMod == null) - return false; + var taikoMod = getMod(); - return taikoMod.ScrollSpeed.Value == taikoMod.ScrollSpeed.Default; + return taikoMod.ScrollSpeed.IsDefault; + }); + } + + [Test] + public void TestKeepSharedSettingsRatio() + { + const float setting_change = 1.8f; + + createScreen(); + changeRuleset(0); + + AddStep("select flashlight mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod()! }); + + changeRuleset(0); + AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModFlashlight); + + AddStep("change mod settings", () => + { + var osuMod = getMod(); + + // range <0.5 - 2> + osuMod.SizeMultiplier.Value = setting_change; }); - ModDifficultyAdjust getMod() => (SelectedMods.Value.Single() as ModDifficultyAdjust)!; + changeRuleset(3); + AddAssert("mania variant selected", () => SelectedMods.Value.SingleOrDefault() is ManiaModFlashlight); + + AddAssert("shared settings changed to closest ratio", () => + { + var maniaMod = getMod(); + + // range <0.5 - 3> + // converted value based on ratio = (setting_change - 0.5) / (2 - 0.5) * (3 - 0.5) + 0.5 = 2.66 + return maniaMod.SizeMultiplier.Value == 2.7f; // taking precision into account + }); + + AddAssert("other settings unchanged", () => + { + var maniaMod = getMod(); + + return maniaMod.ComboBasedSize.IsDefault; + }); } [Test] @@ -514,6 +553,8 @@ namespace osu.Game.Tests.Visual.UserInterface waitForColumnLoad(); AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); + + AddStep("disable panel filtering", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value = false); } [Test] @@ -629,6 +670,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Value == active); } + private T getMod() where T : Mod => (T)SelectedMods.Value.Single(); + private ModPanel getPanelForMod(Type modType) => modSelectOverlay.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType); diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index bc588a5cd8..f6c0e851fd 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using AutoMapper.Internal; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; @@ -173,13 +174,46 @@ namespace osu.Game.Rulesets.Mods foreach (var (_, property) in this.GetSettingsSourceProperties()) { - var targetBindable = (IBindable)property.GetValue(this)!; + object targetSetting = property.GetValue(this)!; if (!oldSettings.TryGetValue(property.Name.ToSnakeCase(), out object? sourceSetting)) continue; - CopyAdjustedSetting(targetBindable, sourceSetting); + if (((IBindable)sourceSetting).IsDefault) + // keep at default value if the source is default + continue; + + Type? targetType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); + Type? sourceType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); + + if (targetType == null || sourceType == null) + { + if (getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == + getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single()) + // change settings only if the type is the same + CopyAdjustedSetting((IBindable)targetSetting, sourceSetting); + + continue; + } + + double targetMin = getValue(targetSetting, nameof(IBindableNumber.MinValue)); + double targetMax = getValue(targetSetting, nameof(IBindableNumber.MaxValue)); + double sourceMin = getValue(sourceSetting, nameof(IBindableNumber.MinValue)); + double sourceMax = getValue(sourceSetting, nameof(IBindableNumber.MaxValue)); + double sourceValue = getValue(sourceSetting, nameof(IBindableNumber.Value)); + + // convert value to same ratio + double targetValue = (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; + + targetType.GetProperty(nameof(IBindableNumber.Value))!.SetValue(targetSetting, + Convert.ChangeType(targetValue, targetType.GenericTypeArguments.Single())); } + + double getValue(object target, string name) => + Convert.ToDouble(target.GetType().GetProperty(name)!.GetValue(target)!); + + Type? getGenericBaseType(object target, Type genericType) => + target.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); } /// From bedf4cc2596f9202a8760cbbe731ca2f1f6bf8ed Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 7 Mar 2023 16:03:11 +0100 Subject: [PATCH 06/23] Remove extra code --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index a354a22fb7..75efc07d43 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -553,8 +553,6 @@ namespace osu.Game.Tests.Visual.UserInterface waitForColumnLoad(); AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); - - AddStep("disable panel filtering", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value = false); } [Test] From 9ea93e0a9fb7b5a9d629519cd6e257e80acd1510 Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 7 Mar 2023 20:38:33 +0100 Subject: [PATCH 07/23] Add more tests --- osu.Game.Tests/Mods/ModSettingsTest.cs | 91 +++++++++++++++++++++----- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index 99198f6bae..45e162df1a 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Localisation; @@ -37,34 +38,92 @@ namespace osu.Game.Tests.Mods } [Test] - public void TestCopySharedSettingsOfDifferentType() + public void TestDifferentTypeSettingsKeptWhenCopied() { - const double setting_change = 2.5; + const double setting_change = 50.4; - var osuMod = new TestNonMatchinSettingTypeOsuMod(); - var maniaMod = new TestNonMatchinSettingTypeManiaMod(); + var modDouble = new TestNonMatchingSettingTypeModDouble(); + var modBool = new TestNonMatchingSettingTypeModBool(); - osuMod.TestSetting.Value = setting_change; - maniaMod.CopySharedSettings(osuMod); - osuMod.CopySharedSettings(maniaMod); + modDouble.TestSetting.Value = setting_change; + modBool.TestSetting.Value = !modBool.TestSetting.Default; + modDouble.CopySharedSettings(modBool); + modBool.CopySharedSettings(modDouble); - Assert.That(maniaMod.TestSetting.IsDefault, "Value has been changed"); - Assert.That(osuMod.TestSetting.Value == setting_change); + Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); + Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); } - private class TestNonMatchinSettingTypeOsuMod : TestNonMatchinSettingTypeMod + [Test] + public void TestCopyDoubleToIntWithDefaultRange() { - public override string Acronym => "NMO"; - public override BindableNumber TestSetting { get; } = new BindableDouble(3.5); + const double setting_change = 50.4; + + var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } }; + var modInt = new TestNonMatchingSettingTypeModInt(); + + modInt.CopySharedSettings(modDouble); + + Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change)); } - private class TestNonMatchinSettingTypeManiaMod : TestNonMatchinSettingTypeMod + [Test] + public void TestCopyIntToDoubleWithDefaultRange() { - public override string Acronym => "NMM"; - public override Bindable TestSetting { get; } = new BindableBool(true); + const int setting_change = 50; + + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = setting_change } }; + var modDouble = new TestNonMatchingSettingTypeModDouble(); + + modDouble.CopySharedSettings(modInt); + + Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); } - private abstract class TestNonMatchinSettingTypeMod : Mod + [Test] + public void TestCopyDoubleToIntWithClampedRange() + { + const double setting_change = 50.4; + + var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 100, MinValue = 0, Value = setting_change } }; + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MaxValue = 200, MinValue = 0 } }; + + modInt.CopySharedSettings(modDouble); + + Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change * 2))); + } + + [Test] + public void TestDefaultValueKeptWhenCopied() + { + var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } }; + var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; + + modBoolFalse.CopySharedSettings(modBoolTrue); + + Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false)); + Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value)); + } + + private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod + { + public override string Acronym => "NMD"; + public override BindableNumber TestSetting { get; } = new BindableDouble(); + } + + private class TestNonMatchingSettingTypeModInt : TestNonMatchingSettingTypeMod + { + public override string Acronym => "NMI"; + public override BindableNumber TestSetting { get; } = new BindableInt(); + } + + private class TestNonMatchingSettingTypeModBool : TestNonMatchingSettingTypeMod + { + public override string Acronym => "NMB"; + public override Bindable TestSetting { get; } = new BindableBool(); + } + + private abstract class TestNonMatchingSettingTypeMod : Mod { public override string Name => "Non-matching setting type mod"; public override LocalisableString Description => "Description"; From 8bf84869a5334c14aec47ebeb43470c8a0d808a1 Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 7 Mar 2023 20:39:50 +0100 Subject: [PATCH 08/23] Fixed errors covered in new tests --- osu.Game/Rulesets/Mods/Mod.cs | 44 +++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f6c0e851fd..e630a53045 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -165,6 +165,10 @@ namespace osu.Game.Rulesets.Mods /// The mod to copy properties from. internal void CopySharedSettings(Mod source) { + const string min_value = nameof(BindableNumber.MinValue); + const string max_value = nameof(BindableNumber.MaxValue); + const string value = nameof(Bindable.Value); + Dictionary oldSettings = new Dictionary(); foreach (var (_, property) in source.GetSettingsSourceProperties()) @@ -190,27 +194,47 @@ namespace osu.Game.Rulesets.Mods { if (getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single()) + { // change settings only if the type is the same - CopyAdjustedSetting((IBindable)targetSetting, sourceSetting); + setValue(targetSetting, value, getValue(sourceSetting, value)); + } continue; } - double targetMin = getValue(targetSetting, nameof(IBindableNumber.MinValue)); - double targetMax = getValue(targetSetting, nameof(IBindableNumber.MaxValue)); - double sourceMin = getValue(sourceSetting, nameof(IBindableNumber.MinValue)); - double sourceMax = getValue(sourceSetting, nameof(IBindableNumber.MaxValue)); - double sourceValue = getValue(sourceSetting, nameof(IBindableNumber.Value)); + Type targetGenericType = targetType.GenericTypeArguments.Single(); + Type sourceGenericType = sourceType.GenericTypeArguments.Single(); + + double allowedMin = Math.Max( + Convert.ToDouble(targetGenericType.GetField("MinValue")!.GetValue(null)), + Convert.ToDouble(sourceGenericType.GetField("MinValue")!.GetValue(null)) + ); + + double allowedMax = Math.Min( + Convert.ToDouble(targetGenericType.GetField("MaxValue")!.GetValue(null)), + Convert.ToDouble(sourceGenericType.GetField("MaxValue")!.GetValue(null)) + ); + + double targetMin = getValueDouble(targetSetting, min_value); + double targetMax = getValueDouble(targetSetting, max_value); + double sourceMin = getValueDouble(sourceSetting, min_value); + double sourceMax = getValueDouble(sourceSetting, max_value); + double sourceValue = getValueDouble(sourceSetting, value); // convert value to same ratio double targetValue = (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; - targetType.GetProperty(nameof(IBindableNumber.Value))!.SetValue(targetSetting, - Convert.ChangeType(targetValue, targetType.GenericTypeArguments.Single())); + setValue(targetSetting, value, Convert.ChangeType(targetValue, targetType.GenericTypeArguments.Single())); + + double getValueDouble(object target, string name) => + Math.Clamp(Convert.ToDouble(getValue(target, name)!), allowedMin, allowedMax); } - double getValue(object target, string name) => - Convert.ToDouble(target.GetType().GetProperty(name)!.GetValue(target)!); + object? getValue(object target, string name) => + target.GetType().GetProperty(name)!.GetValue(target); + + void setValue(object target, string name, object? newValue) => + target.GetType().GetProperty(name)!.SetValue(target, newValue); Type? getGenericBaseType(object target, Type genericType) => target.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); From b51c41a8043ae04fdc57aad461beb9f74b2320b5 Mon Sep 17 00:00:00 2001 From: Terochi Date: Thu, 9 Mar 2023 20:14:58 +0100 Subject: [PATCH 09/23] Addressed changes --- osu.Game.Tests/Mods/ModSettingsTest.cs | 92 ++++++++++++++++++-------- osu.Game/Rulesets/Mods/Mod.cs | 69 ++++++++++++------- 2 files changed, 109 insertions(+), 52 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index 45e162df1a..e2e050870e 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Mods public class ModSettingsTest { [Test] - public void TestModSettingsUnboundWhenCopied() + public void TestModSettingsUnboundWhenCloned() { var original = new OsuModDoubleTime(); var copy = (OsuModDoubleTime)original.DeepClone(); @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Mods } [Test] - public void TestMultiModSettingsUnboundWhenCopied() + public void TestMultiModSettingsUnboundWhenCloned() { var original = new MultiMod(new OsuModDoubleTime()); var copy = (MultiMod)original.DeepClone(); @@ -54,6 +54,42 @@ namespace osu.Game.Tests.Mods Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); } + [Test] + public void TestDefaultValueKeptWhenCopied() + { + var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } }; + var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; + + modBoolFalse.CopySharedSettings(modBoolTrue); + + Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false)); + Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value)); + } + + [Test] + public void TestValueResetsToDefaultWhenCopied() + { + var modDouble = new TestNonMatchingSettingTypeModDouble(); + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = 1 } }; + + modInt.CopySharedSettings(modDouble); + + Assert.That(modInt.TestSetting.Value, Is.EqualTo(modInt.TestSetting.Default)); + } + + [Test] + public void TestRelativelyScaleWithClampedRangeWhenCopied() + { + const double setting_change = 50.4; + + var modDouble100 = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 100, MinValue = 0, Value = setting_change } }; + var modDouble200 = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 200, MinValue = 0 } }; + + modDouble200.CopySharedSettings(modDouble100); + + Assert.That(modDouble200.TestSetting.Value, Is.EqualTo(setting_change * 2)); + } + [Test] public void TestCopyDoubleToIntWithDefaultRange() { @@ -64,7 +100,32 @@ namespace osu.Game.Tests.Mods modInt.CopySharedSettings(modDouble); - Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change)); + Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change))); + } + + [Test] + public void TestCopyDoubleToIntWithOutOfBoundsRange() + { + const double setting_change = 50.4; + + var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MinValue = int.MinValue - 1d, Value = setting_change } }; + // make RangeConstrainedBindable.HasDefinedRange return true + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MinValue = int.MinValue + 1 } }; + + modInt.CopySharedSettings(modDouble); + + Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change))); + } + + [Test] + public void TestCopyDoubleToIntWithOutOfBoundsValue() + { + var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MinValue = int.MinValue + 1, Value = int.MaxValue + 1d } }; + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MinValue = int.MinValue + 1 } }; + + modInt.CopySharedSettings(modDouble); + + Assert.That(modInt.TestSetting.Value, Is.EqualTo(int.MaxValue)); } [Test] @@ -80,31 +141,6 @@ namespace osu.Game.Tests.Mods Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); } - [Test] - public void TestCopyDoubleToIntWithClampedRange() - { - const double setting_change = 50.4; - - var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 100, MinValue = 0, Value = setting_change } }; - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MaxValue = 200, MinValue = 0 } }; - - modInt.CopySharedSettings(modDouble); - - Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change * 2))); - } - - [Test] - public void TestDefaultValueKeptWhenCopied() - { - var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } }; - var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - - modBoolFalse.CopySharedSettings(modBoolTrue); - - Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false)); - Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value)); - } - private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod { public override string Acronym => "NMD"; diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index e630a53045..85014d555d 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -169,28 +169,31 @@ namespace osu.Game.Rulesets.Mods const string max_value = nameof(BindableNumber.MaxValue); const string value = nameof(Bindable.Value); - Dictionary oldSettings = new Dictionary(); + Dictionary sourceSettings = new Dictionary(); - foreach (var (_, property) in source.GetSettingsSourceProperties()) + foreach (var (_, sourceProperty) in source.GetSettingsSourceProperties()) { - oldSettings.Add(property.Name.ToSnakeCase(), property.GetValue(source)!); + sourceSettings.Add(sourceProperty.Name.ToSnakeCase(), sourceProperty.GetValue(source)!); } - foreach (var (_, property) in this.GetSettingsSourceProperties()) + foreach (var (_, targetProperty) in this.GetSettingsSourceProperties()) { - object targetSetting = property.GetValue(this)!; + object targetSetting = targetProperty.GetValue(this)!; - if (!oldSettings.TryGetValue(property.Name.ToSnakeCase(), out object? sourceSetting)) + if (!sourceSettings.TryGetValue(targetProperty.Name.ToSnakeCase(), out object? sourceSetting)) continue; if (((IBindable)sourceSetting).IsDefault) - // keep at default value if the source is default + { + // reset to default value if the source is default + targetSetting.GetType().GetMethod(nameof(Bindable.SetDefault))!.Invoke(targetSetting, null); continue; + } - Type? targetType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); - Type? sourceType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); + Type? targetBindableNumberType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); + Type? sourceBindableNumberType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); - if (targetType == null || sourceType == null) + if (targetBindableNumberType == null || sourceBindableNumberType == null) { if (getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single()) @@ -202,8 +205,16 @@ namespace osu.Game.Rulesets.Mods continue; } - Type targetGenericType = targetType.GenericTypeArguments.Single(); - Type sourceGenericType = sourceType.GenericTypeArguments.Single(); + bool rangeOutOfBounds = false; + + Type targetGenericType = targetBindableNumberType.GenericTypeArguments.Single(); + Type sourceGenericType = sourceBindableNumberType.GenericTypeArguments.Single(); + + if (!Convert.ToBoolean(getValue(targetSetting, nameof(RangeConstrainedBindable.HasDefinedRange))) || + !Convert.ToBoolean(getValue(sourceSetting, nameof(RangeConstrainedBindable.HasDefinedRange)))) + // check if we have a range to rescale from and a range to rescale to + // if not, copy the raw value + rangeOutOfBounds = true; double allowedMin = Math.Max( Convert.ToDouble(targetGenericType.GetField("MinValue")!.GetValue(null)), @@ -219,25 +230,35 @@ namespace osu.Game.Rulesets.Mods double targetMax = getValueDouble(targetSetting, max_value); double sourceMin = getValueDouble(sourceSetting, min_value); double sourceMax = getValueDouble(sourceSetting, max_value); - double sourceValue = getValueDouble(sourceSetting, value); + double sourceValue = Math.Clamp(getValueDouble(sourceSetting, value), allowedMin, allowedMax); - // convert value to same ratio - double targetValue = (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; + double targetValue = rangeOutOfBounds + // keep raw value + ? sourceValue + // convert value to same ratio + : (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; - setValue(targetSetting, value, Convert.ChangeType(targetValue, targetType.GenericTypeArguments.Single())); + setValue(targetSetting, value, Convert.ChangeType(targetValue, targetBindableNumberType.GenericTypeArguments.Single())); - double getValueDouble(object target, string name) => - Math.Clamp(Convert.ToDouble(getValue(target, name)!), allowedMin, allowedMax); + double getValueDouble(object setting, string name) + { + double settingValue = Convert.ToDouble(getValue(setting, name)!); + + if (settingValue < allowedMin || settingValue > allowedMax) + rangeOutOfBounds = true; + + return settingValue; + } } - object? getValue(object target, string name) => - target.GetType().GetProperty(name)!.GetValue(target); + object? getValue(object setting, string name) => + setting.GetType().GetProperty(name)!.GetValue(setting); - void setValue(object target, string name, object? newValue) => - target.GetType().GetProperty(name)!.SetValue(target, newValue); + void setValue(object setting, string name, object? newValue) => + setting.GetType().GetProperty(name)!.SetValue(setting, newValue); - Type? getGenericBaseType(object target, Type genericType) => - target.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); + Type? getGenericBaseType(object setting, Type genericType) => + setting.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); } /// From 8e8dda3ac0ad1d49043198681fcac2b092c9cdda Mon Sep 17 00:00:00 2001 From: Terochi Date: Sat, 11 Mar 2023 23:29:36 +0100 Subject: [PATCH 10/23] Big simplifying --- osu.Game.Tests/Mods/ModSettingsTest.cs | 83 +++---------------- .../TestSceneModSelectOverlay.cs | 42 ---------- osu.Game/Rulesets/Mods/Mod.cs | 61 ++------------ 3 files changed, 19 insertions(+), 167 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index e2e050870e..d943f0ffb1 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Localisation; @@ -42,16 +41,20 @@ namespace osu.Game.Tests.Mods { const double setting_change = 50.4; - var modDouble = new TestNonMatchingSettingTypeModDouble(); - var modBool = new TestNonMatchingSettingTypeModBool(); + var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } }; + var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change } }; - modDouble.TestSetting.Value = setting_change; - modBool.TestSetting.Value = !modBool.TestSetting.Default; modDouble.CopySharedSettings(modBool); + modDouble.CopySharedSettings(modInt); modBool.CopySharedSettings(modDouble); + modBool.CopySharedSettings(modInt); + modInt.CopySharedSettings(modDouble); + modInt.CopySharedSettings(modBool); Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); + Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change)); } [Test] @@ -70,75 +73,11 @@ namespace osu.Game.Tests.Mods public void TestValueResetsToDefaultWhenCopied() { var modDouble = new TestNonMatchingSettingTypeModDouble(); - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = 1 } }; + var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - modInt.CopySharedSettings(modDouble); + modBool.CopySharedSettings(modDouble); - Assert.That(modInt.TestSetting.Value, Is.EqualTo(modInt.TestSetting.Default)); - } - - [Test] - public void TestRelativelyScaleWithClampedRangeWhenCopied() - { - const double setting_change = 50.4; - - var modDouble100 = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 100, MinValue = 0, Value = setting_change } }; - var modDouble200 = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 200, MinValue = 0 } }; - - modDouble200.CopySharedSettings(modDouble100); - - Assert.That(modDouble200.TestSetting.Value, Is.EqualTo(setting_change * 2)); - } - - [Test] - public void TestCopyDoubleToIntWithDefaultRange() - { - const double setting_change = 50.4; - - var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } }; - var modInt = new TestNonMatchingSettingTypeModInt(); - - modInt.CopySharedSettings(modDouble); - - Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change))); - } - - [Test] - public void TestCopyDoubleToIntWithOutOfBoundsRange() - { - const double setting_change = 50.4; - - var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MinValue = int.MinValue - 1d, Value = setting_change } }; - // make RangeConstrainedBindable.HasDefinedRange return true - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MinValue = int.MinValue + 1 } }; - - modInt.CopySharedSettings(modDouble); - - Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change))); - } - - [Test] - public void TestCopyDoubleToIntWithOutOfBoundsValue() - { - var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MinValue = int.MinValue + 1, Value = int.MaxValue + 1d } }; - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MinValue = int.MinValue + 1 } }; - - modInt.CopySharedSettings(modDouble); - - Assert.That(modInt.TestSetting.Value, Is.EqualTo(int.MaxValue)); - } - - [Test] - public void TestCopyIntToDoubleWithDefaultRange() - { - const int setting_change = 50; - - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = setting_change } }; - var modDouble = new TestNonMatchingSettingTypeModDouble(); - - modDouble.CopySharedSettings(modInt); - - Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); + Assert.That(modBool.TestSetting.Value, Is.EqualTo(modBool.TestSetting.Default)); } private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 75efc07d43..b4b5052f23 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -18,7 +18,6 @@ using osu.Game.Overlays.Mods; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch.Mods; -using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -417,47 +416,6 @@ namespace osu.Game.Tests.Visual.UserInterface }); } - [Test] - public void TestKeepSharedSettingsRatio() - { - const float setting_change = 1.8f; - - createScreen(); - changeRuleset(0); - - AddStep("select flashlight mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod()! }); - - changeRuleset(0); - AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModFlashlight); - - AddStep("change mod settings", () => - { - var osuMod = getMod(); - - // range <0.5 - 2> - osuMod.SizeMultiplier.Value = setting_change; - }); - - changeRuleset(3); - AddAssert("mania variant selected", () => SelectedMods.Value.SingleOrDefault() is ManiaModFlashlight); - - AddAssert("shared settings changed to closest ratio", () => - { - var maniaMod = getMod(); - - // range <0.5 - 3> - // converted value based on ratio = (setting_change - 0.5) / (2 - 0.5) * (3 - 0.5) + 0.5 = 2.66 - return maniaMod.SizeMultiplier.Value == 2.7f; // taking precision into account - }); - - AddAssert("other settings unchanged", () => - { - var maniaMod = getMod(); - - return maniaMod.ComboBasedSize.IsDefault; - }); - } - [Test] public void TestExternallySetCustomizedMod() { diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 85014d555d..623a734b82 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -165,8 +165,6 @@ namespace osu.Game.Rulesets.Mods /// The mod to copy properties from. internal void CopySharedSettings(Mod source) { - const string min_value = nameof(BindableNumber.MinValue); - const string max_value = nameof(BindableNumber.MaxValue); const string value = nameof(Bindable.Value); Dictionary sourceSettings = new Dictionary(); @@ -190,65 +188,22 @@ namespace osu.Game.Rulesets.Mods continue; } + bool hasSameGenericArgument = getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == + getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single(); + + if (!hasSameGenericArgument) + continue; + Type? targetBindableNumberType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); Type? sourceBindableNumberType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); if (targetBindableNumberType == null || sourceBindableNumberType == null) { - if (getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == - getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single()) - { - // change settings only if the type is the same - setValue(targetSetting, value, getValue(sourceSetting, value)); - } - + setValue(targetSetting, value, getValue(sourceSetting, value)); continue; } - bool rangeOutOfBounds = false; - - Type targetGenericType = targetBindableNumberType.GenericTypeArguments.Single(); - Type sourceGenericType = sourceBindableNumberType.GenericTypeArguments.Single(); - - if (!Convert.ToBoolean(getValue(targetSetting, nameof(RangeConstrainedBindable.HasDefinedRange))) || - !Convert.ToBoolean(getValue(sourceSetting, nameof(RangeConstrainedBindable.HasDefinedRange)))) - // check if we have a range to rescale from and a range to rescale to - // if not, copy the raw value - rangeOutOfBounds = true; - - double allowedMin = Math.Max( - Convert.ToDouble(targetGenericType.GetField("MinValue")!.GetValue(null)), - Convert.ToDouble(sourceGenericType.GetField("MinValue")!.GetValue(null)) - ); - - double allowedMax = Math.Min( - Convert.ToDouble(targetGenericType.GetField("MaxValue")!.GetValue(null)), - Convert.ToDouble(sourceGenericType.GetField("MaxValue")!.GetValue(null)) - ); - - double targetMin = getValueDouble(targetSetting, min_value); - double targetMax = getValueDouble(targetSetting, max_value); - double sourceMin = getValueDouble(sourceSetting, min_value); - double sourceMax = getValueDouble(sourceSetting, max_value); - double sourceValue = Math.Clamp(getValueDouble(sourceSetting, value), allowedMin, allowedMax); - - double targetValue = rangeOutOfBounds - // keep raw value - ? sourceValue - // convert value to same ratio - : (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; - - setValue(targetSetting, value, Convert.ChangeType(targetValue, targetBindableNumberType.GenericTypeArguments.Single())); - - double getValueDouble(object setting, string name) - { - double settingValue = Convert.ToDouble(getValue(setting, name)!); - - if (settingValue < allowedMin || settingValue > allowedMax) - rangeOutOfBounds = true; - - return settingValue; - } + setValue(targetSetting, value, Convert.ChangeType(getValue(sourceSetting, value), targetBindableNumberType.GenericTypeArguments.Single())); } object? getValue(object setting, string name) => From 0841e73a39ba4c383a62e8897ed55b653f66be1a Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 25 Apr 2023 21:05:40 +0200 Subject: [PATCH 11/23] Improved readability and sounds --- osu.Game.Tests/Mods/ModSettingsTest.cs | 29 +++----- osu.Game/OsuGameBase.cs | 35 ++++++---- osu.Game/Rulesets/Mods/Mod.cs | 95 +++++++++++--------------- 3 files changed, 69 insertions(+), 90 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index d943f0ffb1..b3f6f8da7d 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -43,18 +43,18 @@ namespace osu.Game.Tests.Mods var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } }; var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change } }; + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change / 2 } }; - modDouble.CopySharedSettings(modBool); - modDouble.CopySharedSettings(modInt); - modBool.CopySharedSettings(modDouble); - modBool.CopySharedSettings(modInt); - modInt.CopySharedSettings(modDouble); - modInt.CopySharedSettings(modBool); + modDouble.CopyCommonSettings(modBool); + modDouble.CopyCommonSettings(modInt); + modBool.CopyCommonSettings(modDouble); + modBool.CopyCommonSettings(modInt); + modInt.CopyCommonSettings(modDouble); + modInt.CopyCommonSettings(modBool); Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); - Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change)); + Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change / 2)); } [Test] @@ -63,23 +63,12 @@ namespace osu.Game.Tests.Mods var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } }; var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - modBoolFalse.CopySharedSettings(modBoolTrue); + modBoolFalse.CopyCommonSettings(modBoolTrue); Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false)); Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value)); } - [Test] - public void TestValueResetsToDefaultWhenCopied() - { - var modDouble = new TestNonMatchingSettingTypeModDouble(); - var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - - modBool.CopySharedSettings(modDouble); - - Assert.That(modBool.TestSetting.Value, Is.EqualTo(modBool.TestSetting.Default)); - } - private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod { public override string Acronym => "NMD"; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index de1f2e810c..f6cda62cc3 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -58,7 +58,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Utils; -using File = System.IO.File; using RuntimeInfo = osu.Framework.RuntimeInfo; namespace osu.Game @@ -71,7 +70,7 @@ namespace osu.Game [Cached(typeof(OsuGameBase))] public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider { - public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" }; + public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" }; public const string OSU_PROTOCOL = "osu://"; @@ -626,20 +625,30 @@ namespace osu.Game return; } + var previouslySelectedCommonMods = new List(SelectedMods.Value.Count); + var convertedCommonMods = new List(SelectedMods.Value.Count); + + foreach (var mod in SelectedMods.Value) + { + var convertedMod = instance.CreateModFromAcronym(mod.Acronym); + + if (convertedMod == null) + continue; + + previouslySelectedCommonMods.Add(mod); + + convertedMod.CopyCommonSettings(mod); + convertedCommonMods.Add(convertedMod); + } + + if (!SelectedMods.Disabled) + // Select common mods to play the deselect samples for other mods + SelectedMods.Value = previouslySelectedCommonMods; + AvailableMods.Value = dict; if (!SelectedMods.Disabled) - { - //converting mods from one ruleset to the other, while also keeping their shared settings unchanged - SelectedMods.Value = SelectedMods.Value.Select(oldMod => - { - Mod newMod = instance.CreateModFromAcronym(oldMod.Acronym); - - newMod?.CopySharedSettings(oldMod); - - return newMod; - }).Where(m => m != null).ToArray(); - } + SelectedMods.Value = convertedCommonMods; void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First(); } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 623a734b82..97434ce493 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -115,21 +115,25 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual Type[] IncompatibleMods => Array.Empty(); - private IReadOnlyList? settingsBacking; + private IReadOnlyDictionary? settingsBacking; /// - /// A list of the all settings within this mod. + /// All settings within this mod. /// - internal IReadOnlyList Settings => + internal IEnumerable SettingsBindables => Settings.Values; + + /// + /// Provides mapping of names to s of all settings within this mod. + /// + internal IReadOnlyDictionary Settings => settingsBacking ??= this.GetSettingsSourceProperties() - .Select(p => p.Item2.GetValue(this)) - .Cast() - .ToList(); + .Select(p => p.Item2) + .ToDictionary(property => property.Name.ToSnakeCase(), property => (IBindable)property.GetValue(this)!); /// /// Whether all settings in this mod are set to their default state. /// - protected virtual bool UsesDefaultConfiguration => Settings.All(s => s.IsDefault); + protected virtual bool UsesDefaultConfiguration => SettingsBindables.All(s => s.IsDefault); /// /// Creates a copy of this initialised to a default state. @@ -160,60 +164,37 @@ namespace osu.Game.Rulesets.Mods } /// - /// Copies all mod setting values sharing same from into this instance. + /// When converting mods from one ruleset to the other, this method makes sure + /// to also copy the values of all settings sharing same between the two instances. /// - /// The mod to copy properties from. - internal void CopySharedSettings(Mod source) + /// Copied values are unchanged, even if they have different clamping ranges. + /// The mod to extract settings from. + public void CopyCommonSettings(Mod source) { - const string value = nameof(Bindable.Value); + if (source.UsesDefaultConfiguration) + return; - Dictionary sourceSettings = new Dictionary(); - - foreach (var (_, sourceProperty) in source.GetSettingsSourceProperties()) + foreach (var (name, targetSetting) in Settings) { - sourceSettings.Add(sourceProperty.Name.ToSnakeCase(), sourceProperty.GetValue(source)!); + if (!source.Settings.TryGetValue(name, out IBindable? sourceSetting)) + continue; + + if (sourceSetting.IsDefault) + continue; + + if (getBindableGenericType(targetSetting) != getBindableGenericType(sourceSetting)) + continue; + + // TODO: special case for handling number types + + PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable.Value))!; + property.SetValue(targetSetting, property.GetValue(sourceSetting)); } - foreach (var (_, targetProperty) in this.GetSettingsSourceProperties()) - { - object targetSetting = targetProperty.GetValue(this)!; - - if (!sourceSettings.TryGetValue(targetProperty.Name.ToSnakeCase(), out object? sourceSetting)) - continue; - - if (((IBindable)sourceSetting).IsDefault) - { - // reset to default value if the source is default - targetSetting.GetType().GetMethod(nameof(Bindable.SetDefault))!.Invoke(targetSetting, null); - continue; - } - - bool hasSameGenericArgument = getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == - getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single(); - - if (!hasSameGenericArgument) - continue; - - Type? targetBindableNumberType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); - Type? sourceBindableNumberType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); - - if (targetBindableNumberType == null || sourceBindableNumberType == null) - { - setValue(targetSetting, value, getValue(sourceSetting, value)); - continue; - } - - setValue(targetSetting, value, Convert.ChangeType(getValue(sourceSetting, value), targetBindableNumberType.GenericTypeArguments.Single())); - } - - object? getValue(object setting, string name) => - setting.GetType().GetProperty(name)!.GetValue(setting); - - void setValue(object setting, string name, object? newValue) => - setting.GetType().GetProperty(name)!.SetValue(setting, newValue); - - Type? getGenericBaseType(object setting, Type genericType) => - setting.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); + Type getBindableGenericType(IBindable setting) => + setting.GetType().GetTypeInheritance() + .First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Bindable<>)) + .GenericTypeArguments.Single(); } /// @@ -250,7 +231,7 @@ namespace osu.Game.Rulesets.Mods if (ReferenceEquals(this, other)) return true; return GetType() == other.GetType() && - Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default); + SettingsBindables.SequenceEqual(other.SettingsBindables, ModSettingsEqualityComparer.Default); } public override int GetHashCode() @@ -259,7 +240,7 @@ namespace osu.Game.Rulesets.Mods hashCode.Add(GetType()); - foreach (var setting in Settings) + foreach (var setting in SettingsBindables) hashCode.Add(setting.GetUnderlyingSettingValue()); return hashCode.ToHashCode(); From 8e297dc60a18a9d7057d4b7525ec8cd6189bb504 Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 25 Apr 2023 21:28:03 +0200 Subject: [PATCH 12/23] Added safety measures for invalid mod combinations --- osu.Game/OsuGameBase.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index f6cda62cc3..0473f2665b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -641,6 +641,16 @@ namespace osu.Game convertedCommonMods.Add(convertedMod); } + if (!ModUtils.CheckValidForGameplay(convertedCommonMods, out var invalid)) + { + invalid.ForEach(mod => + { + int index = convertedCommonMods.IndexOf(mod); + convertedCommonMods.RemoveAt(index); + previouslySelectedCommonMods.RemoveAt(index); + }); + } + if (!SelectedMods.Disabled) // Select common mods to play the deselect samples for other mods SelectedMods.Value = previouslySelectedCommonMods; From 332c199dc2b0cdabe76e56dc8696d6ee52f58fa9 Mon Sep 17 00:00:00 2001 From: Terochi Date: Wed, 26 Apr 2023 10:46:29 +0200 Subject: [PATCH 13/23] further simplification --- osu.Game/Rulesets/Mods/Mod.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 97434ce493..0af8717264 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using AutoMapper.Internal; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; @@ -182,7 +181,10 @@ namespace osu.Game.Rulesets.Mods if (sourceSetting.IsDefault) continue; - if (getBindableGenericType(targetSetting) != getBindableGenericType(sourceSetting)) + var targetType = targetSetting.GetType(); + var sourceType = sourceSetting.GetType(); + + if (!targetType.IsAssignableFrom(sourceType) && !sourceType.IsAssignableFrom(targetType)) continue; // TODO: special case for handling number types @@ -190,11 +192,6 @@ namespace osu.Game.Rulesets.Mods PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable.Value))!; property.SetValue(targetSetting, property.GetValue(sourceSetting)); } - - Type getBindableGenericType(IBindable setting) => - setting.GetType().GetTypeInheritance() - .First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Bindable<>)) - .GenericTypeArguments.Single(); } /// From d1d4b54c640df278987ff8ba8ccad28519e59e44 Mon Sep 17 00:00:00 2001 From: Terochi Date: Wed, 3 May 2023 18:31:35 +0200 Subject: [PATCH 14/23] Simplified --- osu.Game/OsuGameBase.cs | 46 +++++++++++++---------------------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e6d6deabe9..076c1213ad 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -625,40 +625,22 @@ namespace osu.Game return; } - var previouslySelectedCommonMods = new List(SelectedMods.Value.Count); - var convertedCommonMods = new List(SelectedMods.Value.Count); - - foreach (var mod in SelectedMods.Value) - { - var convertedMod = instance.CreateModFromAcronym(mod.Acronym); - - if (convertedMod == null) - continue; - - previouslySelectedCommonMods.Add(mod); - - convertedMod.CopyCommonSettings(mod); - convertedCommonMods.Add(convertedMod); - } - - if (!ModUtils.CheckValidForGameplay(convertedCommonMods, out var invalid)) - { - invalid.ForEach(mod => - { - int index = convertedCommonMods.IndexOf(mod); - convertedCommonMods.RemoveAt(index); - previouslySelectedCommonMods.RemoveAt(index); - }); - } - - if (!SelectedMods.Disabled) - // Select common mods to play the deselect samples for other mods - SelectedMods.Value = previouslySelectedCommonMods; - AvailableMods.Value = dict; - if (!SelectedMods.Disabled) - SelectedMods.Value = convertedCommonMods; + if (SelectedMods.Disabled) + return; + + var convertedMods = SelectedMods.Value.Select(mod => + { + var newMod = instance.CreateModFromAcronym(mod.Acronym); + newMod?.CopyCommonSettings(mod); + return newMod; + }).Where(newMod => newMod != null).ToList(); + + if (!ModUtils.CheckValidForGameplay(convertedMods, out var invalid)) + invalid.ForEach(newMod => convertedMods.Remove(newMod)); + + SelectedMods.Value = convertedMods; void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First(); } From 0d2396c5575cd34864e42974b7c99218e4074c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 18:15:12 +0200 Subject: [PATCH 15/23] Rename method to better indicate directionality --- osu.Game.Tests/Mods/ModSettingsTest.cs | 14 +++++++------- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index b3f6f8da7d..4dcc4511db 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -45,12 +45,12 @@ namespace osu.Game.Tests.Mods var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change / 2 } }; - modDouble.CopyCommonSettings(modBool); - modDouble.CopyCommonSettings(modInt); - modBool.CopyCommonSettings(modDouble); - modBool.CopyCommonSettings(modInt); - modInt.CopyCommonSettings(modDouble); - modInt.CopyCommonSettings(modBool); + modDouble.CopyCommonSettingsFrom(modBool); + modDouble.CopyCommonSettingsFrom(modInt); + modBool.CopyCommonSettingsFrom(modDouble); + modBool.CopyCommonSettingsFrom(modInt); + modInt.CopyCommonSettingsFrom(modDouble); + modInt.CopyCommonSettingsFrom(modBool); Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Mods var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } }; var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - modBoolFalse.CopyCommonSettings(modBoolTrue); + modBoolFalse.CopyCommonSettingsFrom(modBoolTrue); Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false)); Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value)); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 076c1213ad..c55b6c249f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -633,7 +633,7 @@ namespace osu.Game var convertedMods = SelectedMods.Value.Select(mod => { var newMod = instance.CreateModFromAcronym(mod.Acronym); - newMod?.CopyCommonSettings(mod); + newMod?.CopyCommonSettingsFrom(mod); return newMod; }).Where(newMod => newMod != null).ToList(); diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 0af8717264..cf5a1c6fcc 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Mods /// /// Copied values are unchanged, even if they have different clamping ranges. /// The mod to extract settings from. - public void CopyCommonSettings(Mod source) + public void CopyCommonSettingsFrom(Mod source) { if (source.UsesDefaultConfiguration) return; From 26337dbcd8c7ca314cf0a8034ee2a6cdad9058b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 18:26:45 +0200 Subject: [PATCH 16/23] Do not rely on unspecified `Dictionary.Values` ordering --- osu.Game/Rulesets/Mods/Mod.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index cf5a1c6fcc..db7e7ce8e3 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -119,7 +119,11 @@ namespace osu.Game.Rulesets.Mods /// /// All settings within this mod. /// - internal IEnumerable SettingsBindables => Settings.Values; + /// + /// The settings are returned in ascending key order as per . + /// The ordering is intentionally enforced manually, as ordering of is unspecified. + /// + internal IEnumerable SettingsBindables => Settings.OrderBy(pair => pair.Key).Select(pair => pair.Value); /// /// Provides mapping of names to s of all settings within this mod. From 88e77ad390fad33752395905260f97ed95f86def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 18:30:41 +0200 Subject: [PATCH 17/23] Rename `Settings{-> Map}` --- osu.Game/Rulesets/Mods/Mod.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index db7e7ce8e3..cd89b45a5e 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -120,15 +120,15 @@ namespace osu.Game.Rulesets.Mods /// All settings within this mod. /// /// - /// The settings are returned in ascending key order as per . + /// The settings are returned in ascending key order as per . /// The ordering is intentionally enforced manually, as ordering of is unspecified. /// - internal IEnumerable SettingsBindables => Settings.OrderBy(pair => pair.Key).Select(pair => pair.Value); + internal IEnumerable SettingsBindables => SettingsMap.OrderBy(pair => pair.Key).Select(pair => pair.Value); /// /// Provides mapping of names to s of all settings within this mod. /// - internal IReadOnlyDictionary Settings => + internal IReadOnlyDictionary SettingsMap => settingsBacking ??= this.GetSettingsSourceProperties() .Select(p => p.Item2) .ToDictionary(property => property.Name.ToSnakeCase(), property => (IBindable)property.GetValue(this)!); @@ -177,9 +177,9 @@ namespace osu.Game.Rulesets.Mods if (source.UsesDefaultConfiguration) return; - foreach (var (name, targetSetting) in Settings) + foreach (var (name, targetSetting) in SettingsMap) { - if (!source.Settings.TryGetValue(name, out IBindable? sourceSetting)) + if (!source.SettingsMap.TryGetValue(name, out IBindable? sourceSetting)) continue; if (sourceSetting.IsDefault) From 1f1342b099752b99de109a2fc6a6fcbaa9b37bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 18:33:49 +0200 Subject: [PATCH 18/23] Reword xmldoc --- osu.Game/Rulesets/Mods/Mod.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index cd89b45a5e..f5ccde01cf 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -167,10 +167,13 @@ namespace osu.Game.Rulesets.Mods } /// - /// When converting mods from one ruleset to the other, this method makes sure - /// to also copy the values of all settings sharing same between the two instances. + /// This method copies the values of all settings from that share the same names with this mod instance. + /// The most frequent use of this is when switching rulesets, in order to preserve values of common settings during the switch. /// - /// Copied values are unchanged, even if they have different clamping ranges. + /// + /// The values are copied directly, without adjusting for possibly different allowed ranges of values. + /// If the value of a setting is not valid for this instance due to not falling inside of the allowed range, it will be clamped accordingly. + /// /// The mod to extract settings from. public void CopyCommonSettingsFrom(Mod source) { From 485b7282dce5a7c220fa534e2f5f9655935270e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 19:06:07 +0200 Subject: [PATCH 19/23] Clarify type check --- osu.Game/Rulesets/Mods/Mod.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f5ccde01cf..787da926ea 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -188,10 +188,13 @@ namespace osu.Game.Rulesets.Mods if (sourceSetting.IsDefault) continue; - var targetType = targetSetting.GetType(); - var sourceType = sourceSetting.GetType(); + var targetBindableType = targetSetting.GetType(); + var sourceBindableType = sourceSetting.GetType(); - if (!targetType.IsAssignableFrom(sourceType) && !sourceType.IsAssignableFrom(targetType)) + // if either the target is assignable to the source or the source is assignable to the target, + // then we presume that the data types contained in both bindables are compatible and we can proceed with the copy. + // this handles cases like `Bindable` and `BindableInt`. + if (!targetBindableType.IsAssignableFrom(sourceBindableType) && !sourceBindableType.IsAssignableFrom(targetBindableType)) continue; // TODO: special case for handling number types From aa7885ab973a258c350ad26c19bc51d89befdf51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 19:11:51 +0200 Subject: [PATCH 20/23] Use better test step names --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index b4b5052f23..f079b65e23 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -399,7 +399,7 @@ namespace osu.Game.Tests.Visual.UserInterface changeRuleset(1); AddAssert("taiko variant selected", () => SelectedMods.Value.SingleOrDefault() is TaikoModDifficultyAdjust); - AddAssert("shared settings didn't change", () => + AddAssert("shared settings preserved", () => { var taikoMod = getMod(); @@ -408,7 +408,7 @@ namespace osu.Game.Tests.Visual.UserInterface taikoMod.OverallDifficulty.Value == setting_change; }); - AddAssert("non-shared settings unchanged", () => + AddAssert("non-shared settings remain default", () => { var taikoMod = getMod(); From 99e8b2ce70276a34d648960556373917b50d0e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 19:12:22 +0200 Subject: [PATCH 21/23] Make `getMod()` method generally better --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index f079b65e23..9993893a99 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -387,7 +387,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("change mod settings", () => { - var osuMod = getMod(); + var osuMod = getSelectedMod(); osuMod.ExtendedLimits.Value = true; osuMod.CircleSize.Value = setting_change; @@ -401,7 +401,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("shared settings preserved", () => { - var taikoMod = getMod(); + var taikoMod = getSelectedMod(); return taikoMod.ExtendedLimits.Value && taikoMod.DrainRate.Value == setting_change && @@ -410,7 +410,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("non-shared settings remain default", () => { - var taikoMod = getMod(); + var taikoMod = getSelectedMod(); return taikoMod.ScrollSpeed.IsDefault; }); @@ -626,7 +626,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Value == active); } - private T getMod() where T : Mod => (T)SelectedMods.Value.Single(); + private T getSelectedMod() where T : Mod => SelectedMods.Value.OfType().Single(); private ModPanel getPanelForMod(Type modType) => modSelectOverlay.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType); From a1106d0a4e0a2522122f945d283f92368714f4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 19:14:20 +0200 Subject: [PATCH 22/23] Be explicit in test --- osu.Game.Tests/Mods/ModSettingsTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index 4dcc4511db..5ec9629dc2 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Mods modInt.CopyCommonSettingsFrom(modBool); Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); - Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); + Assert.That(modBool.TestSetting.Value, Is.EqualTo(true)); Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change / 2)); } From dcd2abae6d5f7a196fae601854f08a794999c470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 20:46:27 +0200 Subject: [PATCH 23/23] Add test coverage for mod equality with multiple settings --- .../Mods/ModSettingsEqualityComparison.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs index cd6879cf01..a03b29f7bc 100644 --- a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs +++ b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs @@ -49,5 +49,31 @@ namespace osu.Game.Tests.Mods Assert.That(mod3, Is.EqualTo(mod2)); Assert.That(doubleConvertedMod3, Is.EqualTo(doubleConvertedMod2)); } + + [Test] + public void TestModWithMultipleSettings() + { + var ruleset = new OsuRuleset(); + + var mod1 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 0 } }; + var mod2 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 6 } }; + var mod3 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 6 } }; + + var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset); + var doubleConvertedMod2 = new APIMod(mod2).ToMod(ruleset); + var doubleConvertedMod3 = new APIMod(mod3).ToMod(ruleset); + + Assert.That(mod1, Is.Not.EqualTo(mod2)); + Assert.That(doubleConvertedMod1, Is.Not.EqualTo(doubleConvertedMod2)); + + Assert.That(mod2, Is.EqualTo(mod2)); + Assert.That(doubleConvertedMod2, Is.EqualTo(doubleConvertedMod2)); + + Assert.That(mod2, Is.EqualTo(mod3)); + Assert.That(doubleConvertedMod2, Is.EqualTo(doubleConvertedMod3)); + + Assert.That(mod3, Is.EqualTo(mod2)); + Assert.That(doubleConvertedMod3, Is.EqualTo(doubleConvertedMod2)); + } } }