diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
index 7ff463361a..c5ce3751ef 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
@@ -8,6 +8,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@@ -15,6 +16,7 @@
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Visual.UserInterface
@@ -75,6 +77,24 @@ public void TestCustomisationMenuVisibility()
AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.Alpha == 0);
}
+ [Test]
+ public void TestModSettingsUnboundWhenCopied()
+ {
+ OsuModDoubleTime original = null;
+ OsuModDoubleTime copy = null;
+
+ AddStep("create mods", () =>
+ {
+ original = new OsuModDoubleTime();
+ copy = (OsuModDoubleTime)original.CreateCopy();
+ });
+
+ AddStep("change property", () => original.SpeedChange.Value = 2);
+
+ AddAssert("original has new value", () => Precision.AlmostEquals(2.0, original.SpeedChange.Value));
+ AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
+ }
+
private void createModSelect()
{
AddStep("create mod select", () =>
diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index 0e5fe3fc9c..52ffa0ad2a 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
@@ -126,7 +127,25 @@ public virtual string SettingDescription
///
/// Creates a copy of this initialised to a default state.
///
- public virtual Mod CreateCopy() => (Mod)MemberwiseClone();
+ public virtual Mod CreateCopy()
+ {
+ var copy = (Mod)Activator.CreateInstance(GetType());
+
+ // Copy bindable values across
+ foreach (var (_, prop) in this.GetSettingsSourceProperties())
+ {
+ var origBindable = prop.GetValue(this);
+ var copyBindable = prop.GetValue(copy);
+
+ // The bindables themselves are readonly, so the value must be transferred through the Bindable.Value property.
+ var valueProperty = origBindable.GetType().GetProperty(nameof(Bindable