Merge pull request #14696 from peppy/cached-all-mods

Add new methods to ruleset for quicker mod lookups
This commit is contained in:
Dan Balasescu 2021-09-10 13:14:22 +09:00 committed by GitHub
commit d19da36fb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 139 additions and 54 deletions

View File

@ -4,6 +4,7 @@
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines; using BenchmarkDotNet.Engines;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
namespace osu.Game.Benchmarks namespace osu.Game.Benchmarks
@ -37,7 +38,25 @@ namespace osu.Game.Benchmarks
[Benchmark] [Benchmark]
public void BenchmarkGetAllMods() public void BenchmarkGetAllMods()
{ {
ruleset.GetAllMods().Consume(new Consumer()); ruleset.CreateAllMods().Consume(new Consumer());
}
[Benchmark]
public void BenchmarkGetAllModsForReference()
{
ruleset.AllMods.Consume(new Consumer());
}
[Benchmark]
public void BenchmarkGetForAcronym()
{
ruleset.CreateModFromAcronym("DT");
}
[Benchmark]
public void BenchmarkGetForType()
{
ruleset.CreateMod<ModDoubleTime>();
} }
} }
} }

View File

@ -3,6 +3,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Mods namespace osu.Game.Tests.Mods
@ -11,26 +12,42 @@ namespace osu.Game.Tests.Mods
public class ModSettingsEqualityComparison public class ModSettingsEqualityComparison
{ {
[Test] [Test]
public void Test() public void TestAPIMod()
{ {
var apiMod1 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.25 } });
var apiMod2 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.26 } });
var apiMod3 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.26 } });
Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
Assert.That(apiMod2, Is.EqualTo(apiMod2));
Assert.That(apiMod2, Is.EqualTo(apiMod3));
Assert.That(apiMod3, Is.EqualTo(apiMod2));
}
[Test]
public void TestMod()
{
var ruleset = new OsuRuleset();
var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } }; var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } };
var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } }; var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } }; var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
var apiMod1 = new APIMod(mod1);
var apiMod2 = new APIMod(mod2); var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset);
var apiMod3 = new APIMod(mod3); var doulbeConvertedMod2 = new APIMod(mod2).ToMod(ruleset);
var doulbeConvertedMod3 = new APIMod(mod3).ToMod(ruleset);
Assert.That(mod1, Is.Not.EqualTo(mod2)); Assert.That(mod1, Is.Not.EqualTo(mod2));
Assert.That(apiMod1, Is.Not.EqualTo(apiMod2)); Assert.That(doubleConvertedMod1, Is.Not.EqualTo(doulbeConvertedMod2));
Assert.That(mod2, Is.EqualTo(mod2)); Assert.That(mod2, Is.EqualTo(mod2));
Assert.That(apiMod2, Is.EqualTo(apiMod2)); Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod2));
Assert.That(mod2, Is.EqualTo(mod3)); Assert.That(mod2, Is.EqualTo(mod3));
Assert.That(apiMod2, Is.EqualTo(apiMod3)); Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod3));
Assert.That(mod3, Is.EqualTo(mod2)); Assert.That(mod3, Is.EqualTo(mod2));
Assert.That(apiMod3, Is.EqualTo(apiMod2)); Assert.That(doulbeConvertedMod3, Is.EqualTo(doulbeConvertedMod2));
} }
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -71,7 +70,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var working = CreateWorkingBeatmap(rulesetInfo); var working = CreateWorkingBeatmap(rulesetInfo);
Beatmap.Value = working; Beatmap.Value = working;
SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; SelectedMods.Value = new[] { ruleset.CreateMod<ModNoFail>() };
Player = CreatePlayer(ruleset); Player = CreatePlayer(ruleset);

View File

@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select EZ mod", () => AddStep("select EZ mod", () =>
{ {
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
SelectedMods.Value = new[] { ruleset.GetAllMods().OfType<ModEasy>().Single() }; SelectedMods.Value = new[] { ruleset.CreateMod<ModEasy>() };
}); });
AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue)); AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue));
@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select HR mod", () => AddStep("select HR mod", () =>
{ {
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
SelectedMods.Value = new[] { ruleset.GetAllMods().OfType<ModHardRock>().Single() }; SelectedMods.Value = new[] { ruleset.CreateMod<ModHardRock>() };
}); });
AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue)); AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue));
@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select unchanged Difficulty Adjust mod", () => AddStep("select unchanged Difficulty Adjust mod", () =>
{ {
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
var difficultyAdjustMod = ruleset.GetAllMods().OfType<ModDifficultyAdjust>().Single(); var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>();
difficultyAdjustMod.ReadFromDifficulty(advancedStats.Beatmap.BaseDifficulty); difficultyAdjustMod.ReadFromDifficulty(advancedStats.Beatmap.BaseDifficulty);
SelectedMods.Value = new[] { difficultyAdjustMod }; SelectedMods.Value = new[] { difficultyAdjustMod };
}); });
@ -142,7 +142,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select changed Difficulty Adjust mod", () => AddStep("select changed Difficulty Adjust mod", () =>
{ {
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
var difficultyAdjustMod = ruleset.GetAllMods().OfType<OsuModDifficultyAdjust>().Single(); var difficultyAdjustMod = ruleset.CreateMod<OsuModDifficultyAdjust>();
var originalDifficulty = advancedStats.Beatmap.BaseDifficulty; var originalDifficulty = advancedStats.Beatmap.BaseDifficulty;
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty); difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);

View File

@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
AddStep("setup display", () => AddStep("setup display", () =>
{ {
var randomMods = Ruleset.Value.CreateInstance().GetAllMods().OrderBy(_ => RNG.Next()).Take(5).ToList(); var randomMods = Ruleset.Value.CreateInstance().CreateAllMods().OrderBy(_ => RNG.Next()).Take(5).ToList();
OsuLogo logo = new OsuLogo { Scale = new Vector2(0.15f) }; OsuLogo logo = new OsuLogo { Scale = new Vector2(0.15f) };

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Width = 200, Width = 200,
Current = Current =
{ {
Value = new OsuRuleset().GetAllMods().ToArray(), Value = new OsuRuleset().CreateAllMods().ToArray(),
} }
}; };
}); });

View File

@ -158,8 +158,8 @@ namespace osu.Game.Tests.Visual.UserInterface
var mania = new ManiaRuleset(); var mania = new ManiaRuleset();
testModsWithSameBaseType( testModsWithSameBaseType(
mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModFadeIn)), mania.CreateMod<ManiaModFadeIn>(),
mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModHidden))); mania.CreateMod<ManiaModHidden>());
} }
[Test] [Test]

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Tests.Components
private void success(APIBeatmap apiBeatmap) private void success(APIBeatmap apiBeatmap)
{ {
beatmap = apiBeatmap.ToBeatmap(rulesets); beatmap = apiBeatmap.ToBeatmap(rulesets);
var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods(); var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().AllMods;
foreach (var mod in mods) foreach (var mod in mods)
{ {

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -49,7 +48,7 @@ namespace osu.Game.Tournament.Components
} }
var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0); var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0);
var modIcon = ruleset?.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == modAcronym); var modIcon = ruleset?.CreateInstance().CreateModFromAcronym(modAcronym);
if (modIcon == null) if (modIcon == null)
return; return;

View File

@ -16,7 +16,7 @@ using osu.Game.Utils;
namespace osu.Game.Online.API namespace osu.Game.Online.API
{ {
[MessagePackObject] [MessagePackObject]
public class APIMod : IMod, IEquatable<APIMod> public class APIMod : IEquatable<APIMod>
{ {
[JsonProperty("acronym")] [JsonProperty("acronym")]
[Key(0)] [Key(0)]
@ -48,7 +48,7 @@ namespace osu.Game.Online.API
public Mod ToMod(Ruleset ruleset) public Mod ToMod(Ruleset ruleset)
{ {
Mod resultMod = ruleset.GetAllMods().FirstOrDefault(m => m.Acronym == Acronym); Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
if (resultMod == null) if (resultMod == null)
throw new InvalidOperationException($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}."); throw new InvalidOperationException($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}.");
@ -67,15 +67,12 @@ namespace osu.Game.Online.API
return resultMod; return resultMod;
} }
public bool Equals(IMod other) => other is APIMod them && Equals(them);
public bool Equals(APIMod other) public bool Equals(APIMod other)
{ {
if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;
return Acronym == other.Acronym && return Acronym == other.Acronym && Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
} }
public override string ToString() public override string ToString()

View File

@ -23,10 +23,10 @@ namespace osu.Game.Online.API.Requests.Responses
var rulesetInstance = ruleset.CreateInstance(); var rulesetInstance = ruleset.CreateInstance();
var mods = Mods != null ? rulesetInstance.GetAllMods().Where(mod => Mods.Contains(mod.Acronym)).ToArray() : Array.Empty<Mod>(); var mods = Mods != null ? Mods.Select(acronym => rulesetInstance.CreateModFromAcronym(acronym)).Where(m => m != null).ToArray() : Array.Empty<Mod>();
// all API scores provided by this class are considered to be legacy. // all API scores provided by this class are considered to be legacy.
mods = mods.Append(rulesetInstance.GetAllMods().OfType<ModClassic>().Single()).ToArray(); mods = mods.Append(rulesetInstance.CreateMod<ModClassic>()).ToArray();
var scoreInfo = new ScoreInfo var scoreInfo = new ScoreInfo
{ {

View File

@ -54,7 +54,7 @@ namespace osu.Game.Overlays.BeatmapSet
return; return;
modsContainer.Add(new ModButton(new ModNoMod())); modsContainer.Add(new ModButton(new ModNoMod()));
modsContainer.AddRange(ruleset.NewValue.CreateInstance().GetAllMods().Where(m => m.UserPlayable).Select(m => new ModButton(m))); modsContainer.AddRange(ruleset.NewValue.CreateInstance().AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m.CreateInstance())));
modsContainer.ForEach(button => modsContainer.ForEach(button =>
{ {

View File

@ -107,9 +107,9 @@ namespace osu.Game.Overlays.Mods
var incompatibleTypes = mod.IncompatibleMods; var incompatibleTypes = mod.IncompatibleMods;
var allMods = ruleset.Value.CreateInstance().GetAllMods(); var allMods = ruleset.Value.CreateInstance().AllMods;
incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).ToList(); incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).Select(m => m.CreateInstance()).ToList();
incompatibleText.Text = incompatibleMods.Value.Any() ? "Incompatible with:" : "Compatible with all mods"; incompatibleText.Text = incompatibleMods.Value.Any() ? "Incompatible with:" : "Compatible with all mods";
} }
} }

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using Newtonsoft.Json; using osu.Framework.Graphics.Sprites;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -11,7 +11,22 @@ namespace osu.Game.Rulesets.Mods
/// <summary> /// <summary>
/// The shortened name of this mod. /// The shortened name of this mod.
/// </summary> /// </summary>
[JsonProperty("acronym")]
string Acronym { get; } string Acronym { get; }
/// <summary>
/// The icon of this mod.
/// </summary>
IconUsage? Icon { get; }
/// <summary>
/// Whether this mod is playable by an end user.
/// Should be <c>false</c> for cases where the user is not interacting with the game (so it can be excluded from multiplayer selection, for example).
/// </summary>
bool UserPlayable { get; }
/// <summary>
/// Create a fresh <see cref="Mod"/> instance based on this mod.
/// </summary>
Mod CreateInstance() => (Mod)Activator.CreateInstance(GetType());
} }
} }

View File

@ -33,9 +33,6 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
public abstract string Acronym { get; } public abstract string Acronym { get; }
/// <summary>
/// The icon of this mod.
/// </summary>
[JsonIgnore] [JsonIgnore]
public virtual IconUsage? Icon => null; public virtual IconUsage? Icon => null;
@ -106,10 +103,6 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore] [JsonIgnore]
public virtual bool HasImplementation => this is IApplicableMod; public virtual bool HasImplementation => this is IApplicableMod;
/// <summary>
/// Whether this mod is playable by an end user.
/// Should be <c>false</c> for cases where the user is not interacting with the game (so it can be excluded from mutliplayer selection, for example).
/// </summary>
[JsonIgnore] [JsonIgnore]
public virtual bool UserPlayable => true; public virtual bool UserPlayable => true;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -38,7 +39,34 @@ namespace osu.Game.Rulesets
{ {
public RulesetInfo RulesetInfo { get; internal set; } public RulesetInfo RulesetInfo { get; internal set; }
public IEnumerable<Mod> GetAllMods() => Enum.GetValues(typeof(ModType)).Cast<ModType>() private static readonly ConcurrentDictionary<int, IMod[]> mod_reference_cache = new ConcurrentDictionary<int, IMod[]>();
/// <summary>
/// A queryable source containing all available mods.
/// Call <see cref="IMod.CreateInstance"/> for consumption purposes.
/// </summary>
public IEnumerable<IMod> AllMods
{
get
{
if (!(RulesetInfo.ID is int id))
return CreateAllMods();
if (!mod_reference_cache.TryGetValue(id, out var mods))
mod_reference_cache[id] = mods = CreateAllMods().Cast<IMod>().ToArray();
return mods;
}
}
/// <summary>
/// Returns fresh instances of all mods.
/// </summary>
/// <remarks>
/// This comes with considerable allocation overhead. If only accessing for reference purposes (ie. not changing bindables / settings)
/// use <see cref="AllMods"/> instead.
/// </remarks>
public IEnumerable<Mod> CreateAllMods() => Enum.GetValues(typeof(ModType)).Cast<ModType>()
// Confine all mods of each mod type into a single IEnumerable<Mod> // Confine all mods of each mod type into a single IEnumerable<Mod>
.SelectMany(GetModsFor) .SelectMany(GetModsFor)
// Filter out all null mods // Filter out all null mods
@ -46,6 +74,24 @@ namespace osu.Game.Rulesets
// Resolve MultiMods as their .Mods property // Resolve MultiMods as their .Mods property
.SelectMany(mod => (mod as MultiMod)?.Mods ?? new[] { mod }); .SelectMany(mod => (mod as MultiMod)?.Mods ?? new[] { mod });
/// <summary>
/// Returns a fresh instance of the mod matching the specified acronym.
/// </summary>
/// <param name="acronym">The acronym to query for .</param>
public Mod CreateModFromAcronym(string acronym)
{
return AllMods.FirstOrDefault(m => m.Acronym == acronym)?.CreateInstance();
}
/// <summary>
/// Returns a fresh instance of the mod matching the specified type.
/// </summary>
public T CreateMod<T>()
where T : Mod
{
return AllMods.FirstOrDefault(m => m is T)?.CreateInstance() as T;
}
public abstract IEnumerable<Mod> GetModsFor(ModType type); public abstract IEnumerable<Mod> GetModsFor(ModType type);
/// <summary> /// <summary>
@ -126,7 +172,7 @@ namespace osu.Game.Rulesets
} }
[CanBeNull] [CanBeNull]
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().FirstOrDefault(); public ModAutoplay GetAutoplayMod() => CreateMod<ModAutoplay>();
public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null; public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null;

View File

@ -67,7 +67,7 @@ namespace osu.Game.Scoring.Legacy
// lazer replays get a really high version number. // lazer replays get a really high version number.
if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.GetAllMods().OfType<ModClassic>().Single()).ToArray(); scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.CreateMod<ModClassic>()).ToArray();
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; scoreInfo.Beatmap = currentBeatmap.BeatmapInfo;

View File

@ -34,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps
protected void TestToLegacy(LegacyMods expectedLegacyMods, Type[] providedModTypes) protected void TestToLegacy(LegacyMods expectedLegacyMods, Type[] providedModTypes)
{ {
var ruleset = CreateRuleset(); var ruleset = CreateRuleset();
var modInstances = ruleset.GetAllMods() var modInstances = ruleset.CreateAllMods()
.Where(mod => providedModTypes.Contains(mod.GetType())) .Where(mod => providedModTypes.Contains(mod.GetType()))
.ToArray(); .ToArray();
var actualLegacyMods = ruleset.ConvertToLegacyMods(modInstances); var actualLegacyMods = ruleset.ConvertToLegacyMods(modInstances);

View File

@ -28,7 +28,7 @@ namespace osu.Game.Tests
RulesetID = ruleset.ID ?? 0; RulesetID = ruleset.ID ?? 0;
Mods = excessMods Mods = excessMods
? ruleset.CreateInstance().GetAllMods().ToArray() ? ruleset.CreateInstance().CreateAllMods().ToArray()
: new Mod[] { new TestModHardRock(), new TestModDoubleTime() }; : new Mod[] { new TestModHardRock(), new TestModDoubleTime() };
TotalScore = 2845370; TotalScore = 2845370;

View File

@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual
if (!AllowFail) if (!AllowFail)
{ {
var noFailMod = ruleset.GetAllMods().FirstOrDefault(m => m is ModNoFail); var noFailMod = ruleset.CreateMod<ModNoFail>();
if (noFailMod != null) if (noFailMod != null)
SelectedMods.Value = new[] { noFailMod }; SelectedMods.Value = new[] { noFailMod };
} }