diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj index c44ed69c4d..19e36a63f1 100644 --- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj +++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj @@ -69,5 +69,9 @@ osu.Game + + + + \ No newline at end of file diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj index ca68369ebb..67b2298f4c 100644 --- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj +++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj @@ -45,6 +45,7 @@ + \ No newline at end of file diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs new file mode 100644 index 0000000000..fdb441343a --- /dev/null +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Moq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; + +namespace osu.Game.Tests.Mods +{ + [TestFixture] + public class ModUtilsTest + { + [Test] + public void TestModIsCompatibleByItself() + { + var mod = new Mock(); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object })); + } + + [Test] + public void TestIncompatibleThroughTopLevel() + { + var mod1 = new Mock(); + var mod2 = new Mock(); + + mod1.Setup(m => m.IncompatibleMods).Returns(new[] { mod2.Object.GetType() }); + + // Test both orderings. + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod2.Object, mod1.Object }), Is.False); + } + + [Test] + public void TestIncompatibleThroughMultiMod() + { + var mod1 = new Mock(); + + // The nested mod. + var mod2 = new Mock(); + mod2.Setup(m => m.IncompatibleMods).Returns(new[] { mod1.Object.GetType() }); + + var multiMod = new MultiMod(new MultiMod(mod2.Object)); + + // Test both orderings. + Assert.That(ModUtils.CheckCompatibleSet(new[] { multiMod, mod1.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, multiMod }), Is.False); + } + + [Test] + public void TestAllowedThroughMostDerivedType() + { + var mod = new Mock(); + Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { mod.Object.GetType() })); + } + + [Test] + public void TestNotAllowedThroughBaseType() + { + var mod = new Mock(); + Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index c0c0578391..d29ed94b5f 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -7,6 +7,7 @@ + WinExe diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs new file mode 100644 index 0000000000..808dba2900 --- /dev/null +++ b/osu.Game/Utils/ModUtils.cs @@ -0,0 +1,109 @@ +// 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 osu.Framework.Extensions.TypeExtensions; +using osu.Game.Rulesets.Mods; + +#nullable enable + +namespace osu.Game.Utils +{ + /// + /// A set of utilities to handle combinations. + /// + public static class ModUtils + { + /// + /// Checks that all s are compatible with each-other, and that all appear within a set of allowed types. + /// + /// + /// The allowed types must contain exact types for the respective s to be allowed. + /// + /// The s to check. + /// The set of allowed types. + /// Whether all s are compatible with each-other and appear in the set of allowed types. + public static bool CheckCompatibleSetAndAllowed(IEnumerable combination, IEnumerable allowedTypes) + { + // Prevent multiple-enumeration. + var combinationList = combination as ICollection ?? combination.ToArray(); + return CheckCompatibleSet(combinationList) && CheckAllowed(combinationList, allowedTypes); + } + + /// + /// Checks that all s in a combination are compatible with each-other. + /// + /// The combination to check. + /// Whether all s in the combination are compatible with each-other. + public static bool CheckCompatibleSet(IEnumerable combination) + { + var incompatibleTypes = new HashSet(); + var incomingTypes = new HashSet(); + + foreach (var mod in combination.SelectMany(FlattenMod)) + { + // Add the new mod incompatibilities, checking whether any match the existing mod types. + foreach (var t in mod.IncompatibleMods) + { + if (incomingTypes.Contains(t)) + return false; + + incompatibleTypes.Add(t); + } + + // Add the new mod types, checking whether any match the incompatible types. + foreach (var t in mod.GetType().EnumerateBaseTypes()) + { + if (incomingTypes.Contains(t)) + return false; + + incomingTypes.Add(t); + } + } + + return true; + } + + /// + /// Checks that all s in a combination appear within a set of allowed types. + /// + /// + /// The set of allowed types must contain exact types for the respective s to be allowed. + /// + /// The combination to check. + /// The set of allowed types. + /// Whether all s in the combination are allowed. + public static bool CheckAllowed(IEnumerable combination, IEnumerable allowedTypes) + { + var allowedSet = new HashSet(allowedTypes); + + return combination.SelectMany(FlattenMod) + .All(m => allowedSet.Contains(m.GetType())); + } + + /// + /// Flattens a set of s, returning a new set with all s removed. + /// + /// The set of s to flatten. + /// The new set, containing all s in recursively with all s removed. + public static IEnumerable FlattenMods(IEnumerable mods) => mods.SelectMany(FlattenMod); + + /// + /// Flattens a , returning a set of s in-place of any s. + /// + /// The to flatten. + /// A set of singular "flattened" s + public static IEnumerable FlattenMod(Mod mod) + { + if (mod is MultiMod multi) + { + foreach (var m in multi.Mods.SelectMany(FlattenMod)) + yield return m; + } + else + yield return mod; + } + } +}