From ead8262257ed0749b9b81670aa76b534aa73874f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Feb 2021 20:20:10 +0900 Subject: [PATCH 1/7] Add function to check for (and return) invalid mods --- osu.Game/Utils/ModUtils.cs | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 808dba2900..9e638d4f2f 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -83,6 +83,48 @@ namespace osu.Game.Utils .All(m => allowedSet.Contains(m.GetType())); } + /// + /// Check the provided combination of mods are valid for a local gameplay session. + /// + /// The mods to check. + /// Invalid mods, if any where found. Can be null if all mods were valid. + /// Whether the input mods were all valid. If false, will contain all invalid entries. + public static bool CheckValidForGameplay(IEnumerable mods, out Mod[]? invalidMods) + { + mods = mods.ToArray(); + + List? foundInvalid = null; + + void addInvalid(Mod mod) + { + foundInvalid ??= new List(); + foundInvalid.Add(mod); + } + + foreach (var mod in mods) + { + bool valid = mod.Type != ModType.System + && mod.HasImplementation + && !(mod is MultiMod); + + if (!valid) + { + // if this mod was found as invalid, we can exclude it before potentially excluding more incompatible types. + addInvalid(mod); + continue; + } + + foreach (var type in mod.IncompatibleMods) + { + foreach (var invalid in mods.Where(m => type.IsInstanceOfType(m))) + addInvalid(invalid); + } + } + + invalidMods = foundInvalid?.ToArray(); + return foundInvalid == null; + } + /// /// Flattens a set of s, returning a new set with all s removed. /// From 425dc8a210d69c853c58f4d2a10ce0347914261f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Feb 2021 20:20:19 +0900 Subject: [PATCH 2/7] Ensure mods are always in a valid state at a game level --- osu.Game/OsuGame.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5acd6bc73d..a00cd5e6a0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -468,6 +468,12 @@ namespace osu.Game private void modsChanged(ValueChangedEvent> mods) { updateModDefaults(); + + if (!ModUtils.CheckValidForGameplay(mods.NewValue, out var invalid)) + { + // ensure we always have a valid set of mods. + SelectedMods.Value = mods.NewValue.Except(invalid).ToArray(); + } } private void updateModDefaults() From 80d88024d6bbca6db1f9ca2765340bb765f5ae07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 14:13:50 +0900 Subject: [PATCH 3/7] Add basic test coverage of CheckValidForGameplay function --- osu.Game.Tests/Mods/ModUtilsTest.cs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index fdb441343a..88eee5449c 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -1,9 +1,13 @@ // 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.Collections.Generic; +using System.Linq; using Moq; using NUnit.Framework; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Utils; namespace osu.Game.Tests.Mods @@ -60,5 +64,29 @@ namespace osu.Game.Tests.Mods var mod = new Mock(); Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); } + + // test incompatible pair. + [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) }, new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) })] + // test incompatible pair with derived class. + [TestCase(new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) }, new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) })] + // test system mod. + [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModTouchDevice) }, new[] { typeof(OsuModTouchDevice) })] + // test valid. + [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModHardRock) }, null)] + public void TestInvalidModScenarios(Type[] input, Type[] expectedInvalid) + { + List inputMods = new List(); + foreach (var t in input) + inputMods.Add((Mod)Activator.CreateInstance(t)); + + bool isValid = ModUtils.CheckValidForGameplay(inputMods, out var invalid); + + Assert.That(isValid, Is.EqualTo(expectedInvalid == null)); + + if (isValid) + Assert.IsNull(invalid); + else + Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); + } } } From 1c645601d41491004c96903231c61ce5a163c7b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 14:14:31 +0900 Subject: [PATCH 4/7] Fix typo in xmldoc --- osu.Game/Utils/ModUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 9e638d4f2f..2146abacb6 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -87,7 +87,7 @@ namespace osu.Game.Utils /// Check the provided combination of mods are valid for a local gameplay session. /// /// The mods to check. - /// Invalid mods, if any where found. Can be null if all mods were valid. + /// Invalid mods, if any were found. Can be null if all mods were valid. /// Whether the input mods were all valid. If false, will contain all invalid entries. public static bool CheckValidForGameplay(IEnumerable mods, out Mod[]? invalidMods) { From 052cf1abaedec23cf44022aedc3324dacd5a8168 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:42:02 +0900 Subject: [PATCH 5/7] Reuse existing method --- osu.Game/Utils/ModUtils.cs | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 05a07f0459..0eb30cbe36 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -88,40 +88,22 @@ namespace osu.Game.Utils /// The mods to check. /// Invalid mods, if any were found. Can be null if all mods were valid. /// Whether the input mods were all valid. If false, will contain all invalid entries. - public static bool CheckValidForGameplay(IEnumerable mods, out Mod[]? invalidMods) + public static bool CheckValidForGameplay(IEnumerable mods, out List? invalidMods) { mods = mods.ToArray(); - List? foundInvalid = null; - - void addInvalid(Mod mod) - { - foundInvalid ??= new List(); - foundInvalid.Add(mod); - } + CheckCompatibleSet(mods, out invalidMods); foreach (var mod in mods) { - bool valid = mod.Type != ModType.System - && mod.HasImplementation - && !(mod is MultiMod); - - if (!valid) + if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod) { - // if this mod was found as invalid, we can exclude it before potentially excluding more incompatible types. - addInvalid(mod); - continue; - } - - foreach (var type in mod.IncompatibleMods) - { - foreach (var invalid in mods.Where(m => type.IsInstanceOfType(m))) - addInvalid(invalid); + invalidMods ??= new List(); + invalidMods.Add(mod); } } - invalidMods = foundInvalid?.ToArray(); - return foundInvalid == null; + return invalidMods == null; } /// From 0a9861d0abe19879a3d9772c248df7ed2b0c2e74 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:51:13 +0900 Subject: [PATCH 6/7] Use TestCaseSource and add multi-mod test --- osu.Game.Tests/Mods/ModUtilsTest.cs | 47 +++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index ff1af88bac..fbdb1e2f3d 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -103,20 +103,43 @@ namespace osu.Game.Tests.Mods Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); } - // test incompatible pair. - [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) }, new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) })] - // test incompatible pair with derived class. - [TestCase(new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) }, new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) })] - // test system mod. - [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModTouchDevice) }, new[] { typeof(OsuModTouchDevice) })] - // test valid. - [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModHardRock) }, null)] - public void TestInvalidModScenarios(Type[] input, Type[] expectedInvalid) + private static readonly object[] invalid_mod_test_scenarios = { - List inputMods = new List(); - foreach (var t in input) - inputMods.Add((Mod)Activator.CreateInstance(t)); + // incompatible pair. + new object[] + { + new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() }, + new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) } + }, + // incompatible pair with derived class. + new object[] + { + new Mod[] { new OsuModNightcore(), new OsuModHalfTime() }, + new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) } + }, + // system mod. + new object[] + { + new Mod[] { new OsuModDoubleTime(), new OsuModTouchDevice() }, + new[] { typeof(OsuModTouchDevice) } + }, + // multi mod. + new object[] + { + new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModHalfTime() }, + new[] { typeof(MultiMod) } + }, + // valid pair. + new object[] + { + new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() }, + null + } + }; + [TestCaseSource(nameof(invalid_mod_test_scenarios))] + public void TestInvalidModScenarios(Mod[] inputMods, Type[] expectedInvalid) + { bool isValid = ModUtils.CheckValidForGameplay(inputMods, out var invalid); Assert.That(isValid, Is.EqualTo(expectedInvalid == null)); From 8e70a50af0836af6fb821c69a7239c7cad9e1eec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 19:22:13 +0900 Subject: [PATCH 7/7] Remove unused using statement --- osu.Game.Tests/Mods/ModUtilsTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index fbdb1e2f3d..7dcaabca3d 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using Moq; using NUnit.Framework;