// 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.CodeAnalysis; using System.Linq; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; 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())); } /// /// Checks that all s in a combination are valid for a local gameplay session. /// /// The mods to check. /// Invalid mods, if any were found. Will 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(); // checking compatibility of multi mods would try to flatten them and return incompatible mods. // in gameplay context, we never want MultiMod selected in the first place, therefore check against it first. if (!checkValid(mods, m => !(m is MultiMod), out invalidMods)) return false; if (!CheckCompatibleSet(mods, out invalidMods)) return false; return checkValid(mods, m => m.HasImplementation, out invalidMods); } /// /// Checks that all s in a combination are valid as "required mods" in a multiplayer match session. /// /// The mods to check. /// Invalid mods, if any were found. Will be null if all mods were valid. /// Whether the input mods were all valid. If false, will contain all invalid entries. public static bool CheckValidRequiredModsForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) { mods = mods.ToArray(); // checking compatibility of multi mods would try to flatten them and return incompatible mods. // in gameplay context, we never want MultiMod selected in the first place, therefore check against it first. if (!checkValid(mods, m => !(m is MultiMod), out invalidMods)) return false; if (!CheckCompatibleSet(mods, out invalidMods)) return false; return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayer, out invalidMods); } /// /// Checks that all s in a combination are valid as "free mods" in a multiplayer match session. /// /// /// Note that this does not check compatibility between mods, /// given that the passed mods are expected to be the ones to be allowed for the multiplayer match, /// not to be confused with the list of mods the user currently has selected for the multiplayer match. /// /// The mods to check. /// Invalid mods, if any were found. Will be null if all mods were valid. /// Whether the input mods were all valid. If false, will contain all invalid entries. public static bool CheckValidFreeModsForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayerAsFreeMod && !(m is MultiMod), out invalidMods); private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods) { mods = mods.ToArray(); invalidMods = null; foreach (var mod in mods) { if (!valid(mod)) { 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; } /// /// 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) { var mod = apiMod.ToMod(ruleset); if (mod is UnknownMod) { proposedWereValid = false; continue; } valid.Add(mod); } return proposedWereValid; } /// /// Verifies all mods provided belong to the given ruleset. /// /// The ruleset to check the proposed mods against. /// The mods proposed for checking. /// Whether all belong to the given . public static bool CheckModsBelongToRuleset(Ruleset ruleset, IEnumerable proposedMods) { var rulesetModsTypes = ruleset.AllMods.Select(m => m.GetType()).ToList(); foreach (var proposedMod in proposedMods) { bool found = false; var proposedModType = proposedMod.GetType(); foreach (var rulesetModType in rulesetModsTypes) { if (rulesetModType.IsAssignableFrom(proposedModType)) { found = true; break; } } if (!found) return false; } return true; } /// /// Given a value of a score multiplier, returns a string version with special handling for a value near 1.00x. /// /// The value of the score multiplier. /// A formatted score multiplier with a trailing "x" symbol public static LocalisableString FormatScoreMultiplier(double scoreMultiplier) { // Round multiplier values away from 1.00x to two significant digits. if (scoreMultiplier > 1) scoreMultiplier = Math.Ceiling(Math.Round(scoreMultiplier * 100, 12)) / 100; else scoreMultiplier = Math.Floor(Math.Round(scoreMultiplier * 100, 12)) / 100; return scoreMultiplier.ToLocalisableString("0.00x"); } /// /// Calculate the rate for the song with the selected mods. /// /// The list of selected mods. /// The rate with mods. public static double CalculateRateWithMods(IEnumerable mods) { double rate = 1; foreach (var mod in mods.OfType()) rate = mod.ApplyToRate(0, rate); return rate; } } }