mirror of
https://github.com/ppy/osu.git
synced 2025-03-15 01:27:20 +08:00
Merge pull request #11640 from smoogipoo/add-mod-utils
Add ModUtils class for validating mod usages
This commit is contained in:
commit
4ecbe058f7
@ -69,5 +69,9 @@
|
|||||||
<Name>osu.Game</Name>
|
<Name>osu.Game</Name>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</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" />
|
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
@ -45,6 +45,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.16.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
|
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
110
osu.Game.Tests/Mods/ModUtilsTest.cs
Normal file
110
osu.Game.Tests/Mods/ModUtilsTest.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// 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<CustomMod1>();
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestIncompatibleThroughTopLevel()
|
||||||
|
{
|
||||||
|
var mod1 = new Mock<CustomMod1>();
|
||||||
|
var mod2 = new Mock<CustomMod2>();
|
||||||
|
|
||||||
|
mod1.Setup(m => m.IncompatibleMods).Returns(new[] { mod2.Object.GetType() });
|
||||||
|
|
||||||
|
// Test both orderings.
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False);
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiModIncompatibleWithTopLevel()
|
||||||
|
{
|
||||||
|
var mod1 = new Mock<CustomMod1>();
|
||||||
|
|
||||||
|
// The nested mod.
|
||||||
|
var mod2 = new Mock<CustomMod2>();
|
||||||
|
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 Mod[] { multiMod, mod1.Object }), Is.False);
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, multiMod }), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTopLevelIncompatibleWithMultiMod()
|
||||||
|
{
|
||||||
|
// The nested mod.
|
||||||
|
var mod1 = new Mock<CustomMod1>();
|
||||||
|
var multiMod = new MultiMod(new MultiMod(mod1.Object));
|
||||||
|
|
||||||
|
var mod2 = new Mock<CustomMod2>();
|
||||||
|
mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(CustomMod1) });
|
||||||
|
|
||||||
|
// Test both orderings.
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { multiMod, mod2.Object }), Is.False);
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, multiMod }), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCompatibleMods()
|
||||||
|
{
|
||||||
|
var mod1 = new Mock<CustomMod1>();
|
||||||
|
var mod2 = new Mock<CustomMod2>();
|
||||||
|
|
||||||
|
// Test both orderings.
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.True);
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestIncompatibleThroughBaseType()
|
||||||
|
{
|
||||||
|
var mod1 = new Mock<CustomMod1>();
|
||||||
|
var mod2 = new Mock<CustomMod2>();
|
||||||
|
mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(Mod) });
|
||||||
|
|
||||||
|
// Test both orderings.
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False);
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAllowedThroughMostDerivedType()
|
||||||
|
{
|
||||||
|
var mod = new Mock<CustomMod1>();
|
||||||
|
Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { mod.Object.GetType() }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNotAllowedThroughBaseType()
|
||||||
|
{
|
||||||
|
var mod = new Mock<CustomMod1>();
|
||||||
|
Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class CustomMod1 : Mod
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class CustomMod2 : Mod
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@
|
|||||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||||
|
<PackageReference Include="Moq" Version="4.16.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
109
osu.Game/Utils/ModUtils.cs
Normal file
109
osu.Game/Utils/ModUtils.cs
Normal 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.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
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, out _) && 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)
|
||||||
|
=> CheckCompatibleSet(combination, out _);
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// <param name="invalidMods">Any invalid mods in the set.</param>
|
||||||
|
/// <returns>Whether all <see cref="Mod"/>s in the combination are compatible with each-other.</returns>
|
||||||
|
public static bool CheckCompatibleSet(IEnumerable<Mod> combination, [NotNullWhen(false)] out List<Mod>? invalidMods)
|
||||||
|
{
|
||||||
|
combination = FlattenMods(combination).ToArray();
|
||||||
|
invalidMods = null;
|
||||||
|
|
||||||
|
foreach (var mod in combination)
|
||||||
|
{
|
||||||
|
foreach (var type in mod.IncompatibleMods)
|
||||||
|
{
|
||||||
|
foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m)))
|
||||||
|
{
|
||||||
|
invalidMods ??= new List<Mod>();
|
||||||
|
invalidMods.Add(invalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return invalidMods == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user