// 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.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Rulesets; 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, out _) && 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) => CheckCompatibleSet(combination, out _); /// /// Checks that all s in a combination are compatible with each-other. /// /// The combination to check. /// Any invalid mods in the set. /// Whether all s in the combination are compatible with each-other. public static bool CheckCompatibleSet(IEnumerable combination, [NotNullWhen(false)] out List? invalidMods) { var mods = FlattenMods(combination).ToArray(); invalidMods = null; // ensure there are no duplicate mod definitions. for (int i = 0; i < mods.Length; i++) { var candidate = mods[i]; for (int j = i + 1; j < mods.Length; j++) { var m = mods[j]; if (candidate.Equals(m)) { invalidMods ??= new List(); invalidMods.Add(m); } } } foreach (var mod in mods) { foreach (var type in mod.IncompatibleMods) { foreach (var invalid in mods.Where(m => type.IsInstanceOfType(m))) { if (invalid == mod) continue; invalidMods ??= new List(); invalidMods.Add(invalid); } } } return invalidMods == null; } /// /// 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())); } /// /// Check the provided combination of mods are valid for a local gameplay session. /// /// 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, [NotNullWhen(false)] out List? invalidMods) { mods = mods.ToArray(); CheckCompatibleSet(mods, out invalidMods); foreach (var mod in mods) { if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod) { invalidMods ??= new List(); invalidMods.Add(mod); } } return invalidMods == null; } /// /// 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; } /// /// Returns the underlying value of the given mod setting object. /// Used in for serialization and equality comparison purposes. /// /// The mod setting. public static object GetSettingUnderlyingValue(object setting) { switch (setting) { case Bindable d: return d.Value; case Bindable i: return i.Value; case Bindable f: return f.Value; case Bindable b: return b.Value; case IBindable u: // A mod with unknown (e.g. enum) generic type. var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); Debug.Assert(valueMethod != null); return valueMethod.GetValue(u); default: // fall back for non-bindable cases. return setting; } } /// /// Verifies all proposed mods are valid for a given ruleset and returns instantiated s for further processing. /// /// The ruleset to verify mods against. /// The proposed mods. /// Mods instantiated from which were valid for the given . /// Whether all were valid for the given . public static bool InstantiateValidModsForRuleset(Ruleset ruleset, IEnumerable proposedMods, out List valid) { valid = new List(); bool proposedWereValid = true; foreach (var apiMod in proposedMods) { try { // will throw if invalid valid.Add(apiMod.ToMod(ruleset)); } catch { proposedWereValid = false; } } return proposedWereValid; } } }