From f04aec538fa6286743147eb2e6f7c83c1b6c4a6b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 18:12:19 +0900 Subject: [PATCH 1/7] Fix MultiMod throwing exceptions when creating copies --- .../UserInterface/TestSceneModSettings.cs | 18 ++++++++++++++++++ osu.Game/Rulesets/Mods/MultiMod.cs | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index c5ce3751ef..0d43be3f65 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -95,6 +95,24 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value)); } + [Test] + public void TestMultiModSettingsUnboundWhenCopied() + { + MultiMod original = null; + MultiMod copy = null; + + AddStep("create mods", () => + { + original = new MultiMod(new OsuModDoubleTime()); + copy = (MultiMod)original.CreateCopy(); + }); + + AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2); + + AddAssert("original has new value", () => Precision.AlmostEquals(2.0, ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value)); + AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value)); + } + private void createModSelect() { AddStep("create mod select", () => diff --git a/osu.Game/Rulesets/Mods/MultiMod.cs b/osu.Game/Rulesets/Mods/MultiMod.cs index f7d574d3c7..2107009dbb 100644 --- a/osu.Game/Rulesets/Mods/MultiMod.cs +++ b/osu.Game/Rulesets/Mods/MultiMod.cs @@ -6,7 +6,7 @@ using System.Linq; namespace osu.Game.Rulesets.Mods { - public class MultiMod : Mod + public sealed class MultiMod : Mod { public override string Name => string.Empty; public override string Acronym => string.Empty; @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Mods Mods = mods; } + public override Mod CreateCopy() => new MultiMod(Mods.Select(m => m.CreateCopy()).ToArray()); + public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray(); } } From da8565c0fa653c7b8dee30ec71d8a51d0cdf97f1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 18:28:19 +0900 Subject: [PATCH 2/7] Add 10K mod to incompatibility list --- osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs index 13fdd74113..8fd5950dfb 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs @@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods typeof(ManiaModKey7), typeof(ManiaModKey8), typeof(ManiaModKey9), + typeof(ManiaModKey10), }.Except(new[] { GetType() }).ToArray(); } } From d7a52e97fffd13fd780f6d9a8f283c68f3c637c3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 19:03:11 +0900 Subject: [PATCH 3/7] Fix multimod difficulty combinations not generating correctly --- ...DifficultyAdjustmentModCombinationsTest.cs | 39 +++++++++++++++++++ .../Difficulty/DifficultyCalculator.cs | 31 +++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 760a033aff..de0397dc84 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -94,6 +94,38 @@ namespace osu.Game.Tests.NonVisual Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA); } + [Test] + public void TestMultiMod1() + { + var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(4, combinations.Length); + Assert.IsTrue(combinations[0] is ModNoMod); + Assert.IsTrue(combinations[1] is ModA); + Assert.IsTrue(combinations[2] is MultiMod); + Assert.IsTrue(combinations[3] is MultiMod); + + Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA); + Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB); + Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC); + Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB); + Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC); + } + + [Test] + public void TestMultiMod2() + { + var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(3, combinations.Length); + Assert.IsTrue(combinations[0] is ModNoMod); + Assert.IsTrue(combinations[1] is ModA); + Assert.IsTrue(combinations[2] is MultiMod); + + Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB); + Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA); + } + private class ModA : Mod { public override string Name => nameof(ModA); @@ -112,6 +144,13 @@ namespace osu.Game.Tests.NonVisual public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) }; } + private class ModC : Mod + { + public override string Name => nameof(ModC); + public override string Acronym => nameof(ModC); + public override double ScoreMultiplier => 1; + } + private class ModIncompatibleWithA : Mod { public override string Name => $"Incompatible With {nameof(ModA)}"; diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 1902de5bda..9989c750ee 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Difficulty { return createDifficultyAdjustmentModCombinations(Array.Empty(), DifficultyAdjustmentMods).ToArray(); - IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) + static IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) { switch (currentSetCount) { @@ -133,13 +133,36 @@ namespace osu.Game.Rulesets.Difficulty for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++) { var adjustmentMod = adjustmentSet[i]; - if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)))) - continue; - foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(adjustmentMod), adjustmentSet, currentSetCount + 1, i + 1)) + if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)) + || adjustmentMod.IncompatibleMods.Any(m => m.IsInstanceOfType(c)))) + { + continue; + } + + // Append the new mod. + int newSetCount = currentSetCount; + var newSet = append(currentSet, adjustmentMod, ref newSetCount); + + foreach (var combo in createDifficultyAdjustmentModCombinations(newSet, adjustmentSet, newSetCount, i + 1)) yield return combo; } } + + // Appends a mod to an existing enumerable, returning the result. Recurses for MultiMod. + static IEnumerable append(IEnumerable existing, Mod mod, ref int count) + { + if (mod is MultiMod multi) + { + foreach (var nested in multi.Mods) + existing = append(existing, nested, ref count); + + return existing; + } + + count++; + return existing.Append(mod); + } } /// From 7d2eeb979532fc643eb6df12fed2691a2c417d66 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 19:18:04 +0900 Subject: [PATCH 4/7] Fix test names --- .../NonVisual/DifficultyAdjustmentModCombinationsTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index de0397dc84..917f245f4f 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.NonVisual } [Test] - public void TestMultiMod1() + public void TestMultiModFlattening() { var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations(); @@ -113,7 +113,7 @@ namespace osu.Game.Tests.NonVisual } [Test] - public void TestMultiMod2() + public void TestIncompatibleThroughMultiMod() { var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations(); From e9ebeedbe2edd7b1d4e62f9d01d8940b4cf5255a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 19:31:31 +0900 Subject: [PATCH 5/7] Refactor generation --- .../Difficulty/DifficultyCalculator.cs | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 9989c750ee..70f248e072 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -105,10 +105,11 @@ namespace osu.Game.Rulesets.Difficulty /// public Mod[] CreateDifficultyAdjustmentModCombinations() { - return createDifficultyAdjustmentModCombinations(Array.Empty(), DifficultyAdjustmentMods).ToArray(); + return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty()).ToArray(); - static IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) + static IEnumerable createDifficultyAdjustmentModCombinations(ReadOnlyMemory remainingMods, IEnumerable currentSet, int currentSetCount = 0) { + // Return the current set. switch (currentSetCount) { case 0: @@ -128,11 +129,10 @@ namespace osu.Game.Rulesets.Difficulty break; } - // Apply mods in the adjustment set recursively. Using the entire adjustment set would result in duplicate multi-mod mod - // combinations in further recursions, so a moving subset is used to eliminate this effect - for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++) + // Apply the rest of the remaining mods recursively. + for (int i = 0; i < remainingMods.Length; i++) { - var adjustmentMod = adjustmentSet[i]; + var adjustmentMod = remainingMods.Span[i]; if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)) || adjustmentMod.IncompatibleMods.Any(m => m.IsInstanceOfType(c)))) @@ -141,27 +141,30 @@ namespace osu.Game.Rulesets.Difficulty } // Append the new mod. - int newSetCount = currentSetCount; - var newSet = append(currentSet, adjustmentMod, ref newSetCount); + var (newSet, newSetCount) = flatten(adjustmentMod); - foreach (var combo in createDifficultyAdjustmentModCombinations(newSet, adjustmentSet, newSetCount, i + 1)) + foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(newSet), currentSetCount + newSetCount)) yield return combo; } } - // Appends a mod to an existing enumerable, returning the result. Recurses for MultiMod. - static IEnumerable append(IEnumerable existing, Mod mod, ref int count) + // Flattens a mod hierarchy (through MultiMod) as an IEnumerable + static (IEnumerable set, int count) flatten(Mod mod) { - if (mod is MultiMod multi) - { - foreach (var nested in multi.Mods) - existing = append(existing, nested, ref count); + if (!(mod is MultiMod multi)) + return (mod.Yield(), 1); - return existing; + IEnumerable set = Enumerable.Empty(); + int count = 0; + + foreach (var nested in multi.Mods) + { + var (nestedSet, nestedCount) = flatten(nested); + set = set.Concat(nestedSet); + count += nestedCount; } - count++; - return existing.Append(mod); + return (set, count); } } From c4fdd35223e85e383b26b82eec0e8c1212eb11f9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 19:53:37 +0900 Subject: [PATCH 6/7] Fix same-type incompatibility through multimod --- .../DifficultyAdjustmentModCombinationsTest.cs | 14 ++++++++++++++ .../Difficulty/DifficultyCalculator.cs | 18 ++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 917f245f4f..5c7adb3f49 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -126,6 +126,20 @@ namespace osu.Game.Tests.NonVisual Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA); } + [Test] + public void TestIncompatibleWithSameInstanceViaMultiMod() + { + var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(3, combinations.Length); + Assert.IsTrue(combinations[0] is ModNoMod); + Assert.IsTrue(combinations[1] is ModA); + Assert.IsTrue(combinations[2] is MultiMod); + + Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA); + Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB); + } + private class ModA : Mod { public override string Name => nameof(ModA); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 70f248e072..55b3d6607c 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; @@ -11,6 +12,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using Sentry; namespace osu.Game.Rulesets.Difficulty { @@ -132,18 +134,18 @@ namespace osu.Game.Rulesets.Difficulty // Apply the rest of the remaining mods recursively. for (int i = 0; i < remainingMods.Length; i++) { - var adjustmentMod = remainingMods.Span[i]; + var (nextSet, nextCount) = flatten(remainingMods.Span[i]); - if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)) - || adjustmentMod.IncompatibleMods.Any(m => m.IsInstanceOfType(c)))) - { + // Check if any mods in the next set are incompatible with any of the current set. + if (currentSet.SelectMany(m => m.IncompatibleMods).Any(c => nextSet.Any(c.IsInstanceOfType))) continue; - } - // Append the new mod. - var (newSet, newSetCount) = flatten(adjustmentMod); + // Check if any mods in the next set are the same type as the current set. Mods of the exact same type are not incompatible with themselves. + if (currentSet.Any(c => nextSet.Any(n => c.GetType() == n.GetType()))) + continue; - foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(newSet), currentSetCount + newSetCount)) + // If all's good, attach the next set to the current set and recurse further. + foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(nextSet), currentSetCount + nextCount)) yield return combo; } } From ed57b1363fdef33c350d1c65404739aca92750bd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 20:08:46 +0900 Subject: [PATCH 7/7] Remove unused usings --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 55b3d6607c..7616c48150 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; @@ -12,7 +11,6 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using Sentry; namespace osu.Game.Rulesets.Difficulty {