1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 18:47:27 +08:00

Add ModUtils class for validating mod usages

This commit is contained in:
smoogipoo 2021-02-01 19:37:19 +09:00
parent 328bd191d4
commit b9832c1b2d
5 changed files with 179 additions and 0 deletions

View File

@ -69,5 +69,9 @@
<Name>osu.Game</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Moq" Version="4.16.0" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>

View File

@ -45,6 +45,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Moq" Version="4.16.0" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
</Project>

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<Mod>();
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
}
[Test]
public void TestIncompatibleThroughTopLevel()
{
var mod1 = new Mock<Mod>();
var mod2 = new Mock<Mod>();
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<Mod>();
// The nested mod.
var mod2 = new Mock<Mod>();
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<Mod>();
Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { mod.Object.GetType() }));
}
[Test]
public void TestNotAllowedThroughBaseType()
{
var mod = new Mock<Mod>();
Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False);
}
}
}

View File

@ -7,6 +7,7 @@
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Moq" Version="4.16.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

109
osu.Game/Utils/ModUtils.cs Normal file
View File

@ -0,0 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// A set of utilities to handle <see cref="Mod"/> combinations.
/// </summary>
public static class ModUtils
{
/// <summary>
/// Checks that all <see cref="Mod"/>s are compatible with each-other, and that all appear within a set of allowed types.
/// </summary>
/// <remarks>
/// The allowed types must contain exact <see cref="Mod"/> types for the respective <see cref="Mod"/>s to be allowed.
/// </remarks>
/// <param name="combination">The <see cref="Mod"/>s to check.</param>
/// <param name="allowedTypes">The set of allowed <see cref="Mod"/> types.</param>
/// <returns>Whether all <see cref="Mod"/>s are compatible with each-other and appear in the set of allowed types.</returns>
public static bool CheckCompatibleSetAndAllowed(IEnumerable<Mod> combination, IEnumerable<Type> allowedTypes)
{
// Prevent multiple-enumeration.
var combinationList = combination as ICollection<Mod> ?? combination.ToArray();
return CheckCompatibleSet(combinationList) && CheckAllowed(combinationList, allowedTypes);
}
/// <summary>
/// Checks that all <see cref="Mod"/>s in a combination are compatible with each-other.
/// </summary>
/// <param name="combination">The <see cref="Mod"/> combination to check.</param>
/// <returns>Whether all <see cref="Mod"/>s in the combination are compatible with each-other.</returns>
public static bool CheckCompatibleSet(IEnumerable<Mod> combination)
{
var incompatibleTypes = new HashSet<Type>();
var incomingTypes = new HashSet<Type>();
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;
}
/// <summary>
/// Checks that all <see cref="Mod"/>s in a combination appear within a set of allowed types.
/// </summary>
/// <remarks>
/// The set of allowed types must contain exact <see cref="Mod"/> types for the respective <see cref="Mod"/>s to be allowed.
/// </remarks>
/// <param name="combination">The <see cref="Mod"/> combination to check.</param>
/// <param name="allowedTypes">The set of allowed <see cref="Mod"/> types.</param>
/// <returns>Whether all <see cref="Mod"/>s in the combination are allowed.</returns>
public static bool CheckAllowed(IEnumerable<Mod> combination, IEnumerable<Type> allowedTypes)
{
var allowedSet = new HashSet<Type>(allowedTypes);
return combination.SelectMany(FlattenMod)
.All(m => allowedSet.Contains(m.GetType()));
}
/// <summary>
/// Flattens a set of <see cref="Mod"/>s, returning a new set with all <see cref="MultiMod"/>s removed.
/// </summary>
/// <param name="mods">The set of <see cref="Mod"/>s to flatten.</param>
/// <returns>The new set, containing all <see cref="Mod"/>s in <paramref name="mods"/> recursively with all <see cref="MultiMod"/>s removed.</returns>
public static IEnumerable<Mod> FlattenMods(IEnumerable<Mod> mods) => mods.SelectMany(FlattenMod);
/// <summary>
/// Flattens a <see cref="Mod"/>, returning a set of <see cref="Mod"/>s in-place of any <see cref="MultiMod"/>s.
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to flatten.</param>
/// <returns>A set of singular "flattened" <see cref="Mod"/>s</returns>
public static IEnumerable<Mod> FlattenMod(Mod mod)
{
if (mod is MultiMod multi)
{
foreach (var m in multi.Mods.SelectMany(FlattenMod))
yield return m;
}
else
yield return mod;
}
}
}