diff --git a/osu.Game.Tests/Utils/BindableValueAccessorTest.cs b/osu.Game.Tests/Utils/BindableValueAccessorTest.cs new file mode 100644 index 000000000000..f09623dbfc4c --- /dev/null +++ b/osu.Game.Tests/Utils/BindableValueAccessorTest.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Utils; + +namespace osu.Game.Tests.Utils +{ + [TestFixture] + public class BindableValueAccessorTest + { + [Test] + public void GetValue() + { + const int value = 1337; + + BindableInt bindable = new BindableInt(value); + Assert.That(BindableValueAccessor.GetValue(bindable), Is.EqualTo(value)); + } + + [Test] + public void SetValue() + { + const int value = 1337; + + BindableInt bindable = new BindableInt(); + BindableValueAccessor.SetValue(bindable, value); + + Assert.That(bindable.Value, Is.EqualTo(value)); + } + + [Test] + public void GetInvalidBindable() + { + BindableList list = new BindableList(); + Assert.That(BindableValueAccessor.GetValue(list), Is.EqualTo(list)); + } + + [Test] + public void SetInvalidBindable() + { + const int value = 1337; + + BindableList list = new BindableList { value }; + BindableValueAccessor.SetValue(list, 2); + + Assert.That(list, Has.Exactly(1).Items); + Assert.That(list[0], Is.EqualTo(value)); + } + } +} diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 1e425c88a6c3..580366a75a55 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reflection; using JetBrains.Annotations; @@ -15,6 +14,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; +using osu.Game.Utils; namespace osu.Game.Configuration { @@ -228,10 +228,7 @@ public static object GetUnderlyingSettingValue(this object setting) return b.Value; case IBindable u: - // An unknown (e.g. enum) generic type. - var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); - Debug.Assert(valueMethod != null); - return valueMethod.GetValue(u)!; + return BindableValueAccessor.GetValue(u); default: // fall back for non-bindable cases. diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b9a937b1a2fb..1b21216235d8 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -266,8 +266,7 @@ public void CopyCommonSettingsFrom(Mod source) // TODO: special case for handling number types - PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable.Value))!; - property.SetValue(targetSetting, property.GetValue(sourceSetting)); + BindableValueAccessor.SetValue(targetSetting, BindableValueAccessor.GetValue(sourceSetting)); } } diff --git a/osu.Game/Utils/BindableValueAccessor.cs b/osu.Game/Utils/BindableValueAccessor.cs new file mode 100644 index 000000000000..dd097ada362e --- /dev/null +++ b/osu.Game/Utils/BindableValueAccessor.cs @@ -0,0 +1,44 @@ +// 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 System.Linq; +using System.Reflection; +using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; + +namespace osu.Game.Utils +{ + internal static class BindableValueAccessor + { + private static readonly MethodInfo get_method = typeof(BindableValueAccessor).GetMethod(nameof(getValue), BindingFlags.Static | BindingFlags.NonPublic)!; + private static readonly MethodInfo set_method = typeof(BindableValueAccessor).GetMethod(nameof(setValue), BindingFlags.Static | BindingFlags.NonPublic)!; + + public static object GetValue(IBindable bindable) + { + Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(isBindableT); + if (bindableWithValueType == null) + return bindable; + + return get_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable])!; + } + + public static void SetValue(IBindable bindable, object value) + { + Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().FirstOrDefault(isBindableT); + if (bindableWithValueType == null) + return; + + set_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable, value]); + } + + private static bool isBindableT(Type type) + => type.IsGenericType + && (type.GetGenericTypeDefinition() == typeof(Bindable<>) + || type.GetGenericTypeDefinition() == typeof(IBindable<>)); + + private static object getValue(object bindable) => ((IBindable)bindable).Value!; + + private static object setValue(object bindable, object value) => ((Bindable)bindable).Value = (T)value; + } +}