diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
index c62479faa0..3d225aa0a9 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
@@ -16,7 +16,9 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Select;
@@ -145,6 +147,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("new item has id 2", () => Room.Playlist.Last().ID == 2);
}
+ ///
+ /// Tests that the same instances are not shared between two playlist items.
+ ///
+ [Test]
+ public void TestNewItemHasNewModInstances()
+ {
+ AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
+ AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
+ AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2);
+ AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
+
+ AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value));
+ AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value));
+ }
+
private class TestMatchSongSelect : MatchSongSelect
{
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
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 NUnit.Framework;
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.Overlays.Mods;
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 @@ namespace osu.Game.Tests.Visual.UserInterface
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 @@ namespace osu.Game.Rulesets.Mods
///
/// 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