From 1025e1939ba90b96595c3ab0d9fc5a86a3f0527a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 16 Mar 2022 11:54:18 +0300 Subject: [PATCH 01/26] Disable "Adaptive Speed" mod in multiplayer --- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index e30ec36e9c..5ccf89e703 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -117,6 +117,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && !(mod is ModAdaptiveSpeed); + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust); } } From d90a33485318f926e51c6f9da270876756bfab30 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Mar 2022 03:40:15 +0300 Subject: [PATCH 02/26] Introduce multiplayer playability and free mod validity in `Mod` --- osu.Game/Rulesets/Mods/IMod.cs | 12 ++++++++++++ osu.Game/Rulesets/Mods/Mod.cs | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index d5d1de91de..cda59bae55 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -39,6 +39,18 @@ namespace osu.Game.Rulesets.Mods /// bool UserPlayable { get; } + /// + /// Whether this mod is playable in a multiplayer match. + /// Should be false for mods that affect the gameplay progress based on user input (e.g. ). + /// + bool PlayableInMultiplayer { get; } + + /// + /// Whether this mod is valid to be a "free mod" in a multiplayer match. + /// Should be false for mods that affect the gameplay progress (e.g. and ). + /// + bool ValidFreeModInMultiplayer { get; } + /// /// Create a fresh instance based on this mod. /// diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b2d4be54ce..a6562b4f4c 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -94,6 +94,12 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool UserPlayable => true; + [JsonIgnore] + public virtual bool PlayableInMultiplayer => UserPlayable; + + [JsonIgnore] + public virtual bool ValidFreeModInMultiplayer => PlayableInMultiplayer; + [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009 public virtual bool Ranked => false; From 187059a37f69ed4ceff8ea4e2e2a0b8dda3b0085 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Mar 2022 03:40:40 +0300 Subject: [PATCH 03/26] Replace hardcoded overrides with the newly introduced `Mod` properties --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 2 ++ osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 ++ osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 ++ .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 4 ++-- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 1115b95e6f..54a4c054c9 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; + public override bool PlayableInMultiplayer => false; + public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; [SettingSource("Initial rate", "The starting speed of the track")] diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index ebe18f2188..3a11c3034b 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModRateAdjust : Mod, IApplicableToRate { + public override bool ValidFreeModInMultiplayer => false; + public abstract BindableNumber SpeedChange { get; } public virtual void ApplyToTrack(ITrack track) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index b6b2decede..59aac62686 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } + public override bool ValidFreeModInMultiplayer => false; + public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 5ccf89e703..e4f1d9587d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -117,8 +117,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && !(mod is ModAdaptiveSpeed); + protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.PlayableInMultiplayer; - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust); + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidFreeModInMultiplayer; } } From 59741ccee60d9b05466d9b7a1fea6157106ba5d8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Mar 2022 05:15:05 +0300 Subject: [PATCH 04/26] Add multiplayer mod validity check methods for server consumption --- osu.Game/OsuGame.cs | 15 ++++++++++----- osu.Game/Utils/ModUtils.cs | 26 +++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ae117d03d2..bc2d2083fe 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -575,11 +575,16 @@ namespace osu.Game if (SelectedMods.Disabled) return; - if (!ModUtils.CheckValidForGameplay(mods.NewValue, out var invalid)) - { - // ensure we always have a valid set of mods. - SelectedMods.Value = mods.NewValue.Except(invalid).ToArray(); - } + var validMods = mods.NewValue; + + if (!ModUtils.CheckCompatibleSet(validMods, out var incompatible)) + validMods = validMods.Except(incompatible).ToArray(); + + if (!ModUtils.CheckValidForGameplay(validMods, out var invalid)) + validMods = validMods.Except(invalid).ToArray(); + + // ensure we always have a valid set of mods. + SelectedMods.Value = validMods; } #endregion diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index d5ea74c404..d169ace80a 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -112,14 +112,34 @@ namespace osu.Game.Utils /// 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) + => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && !(m is MultiMod), out invalidMods); + + /// + /// Check the provided combination of mods are valid for a multiplayer match 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 CheckValidForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) + => checkValid(mods, m => m.PlayableInMultiplayer, out invalidMods); + + /// + /// Check the provided combination of mods are valid as "free mods" in a multiplayer match 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 CheckValidFreeModsForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) + => checkValid(mods, m => m.ValidFreeModInMultiplayer, out invalidMods); + + private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods) { mods = mods.ToArray(); - - CheckCompatibleSet(mods, out invalidMods); + invalidMods = null; foreach (var mod in mods) { - if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod) + if (!valid(mod)) { invalidMods ??= new List(); invalidMods.Add(mod); From 07e9f3780a8335ca07d3e076193c0b7cbdc80982 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Mar 2022 05:15:48 +0300 Subject: [PATCH 05/26] Consider `UnknownMod` to be "playable in multiplayer" --- osu.Game/Rulesets/Mods/UnknownMod.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs index b426386d7a..790306c0ca 100644 --- a/osu.Game/Rulesets/Mods/UnknownMod.cs +++ b/osu.Game/Rulesets/Mods/UnknownMod.cs @@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 0; public override bool UserPlayable => false; + public override bool PlayableInMultiplayer => true; public override ModType Type => ModType.System; From b3ac544d655e6846a94b6bf2d31332b471d90de4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Mar 2022 06:31:51 +0300 Subject: [PATCH 06/26] Revert "Consider `UnknownMod` to be "playable in multiplayer"" This reverts commit 07e9f3780a8335ca07d3e076193c0b7cbdc80982. --- osu.Game/Rulesets/Mods/UnknownMod.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs index 790306c0ca..b426386d7a 100644 --- a/osu.Game/Rulesets/Mods/UnknownMod.cs +++ b/osu.Game/Rulesets/Mods/UnknownMod.cs @@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 0; public override bool UserPlayable => false; - public override bool PlayableInMultiplayer => true; public override ModType Type => ModType.System; From d90f21e140bea785d07fd33c8ef1af126c16a16b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Mar 2022 00:13:40 +0300 Subject: [PATCH 07/26] Reword mod documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Rulesets/Mods/IMod.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index cda59bae55..5f4fecb649 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -41,13 +41,13 @@ namespace osu.Game.Rulesets.Mods /// /// Whether this mod is playable in a multiplayer match. - /// Should be false for mods that affect the gameplay progress based on user input (e.g. ). + /// Should be false for mods that make gameplay duration dependent on user input (e.g. ). /// bool PlayableInMultiplayer { get; } /// /// Whether this mod is valid to be a "free mod" in a multiplayer match. - /// Should be false for mods that affect the gameplay progress (e.g. and ). + /// Should be false for mods that affect the gameplay duration (e.g. and ). /// bool ValidFreeModInMultiplayer { get; } From b0d04a78f7d2e4b7fadaee32841dca3387c44f34 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Mar 2022 00:21:16 +0300 Subject: [PATCH 08/26] Reword mod utility documentation regarding nullability --- osu.Game/Utils/ModUtils.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index d169ace80a..81b78c18ac 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -109,7 +109,7 @@ namespace osu.Game.Utils /// 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. + /// 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) => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && !(m is MultiMod), out invalidMods); @@ -118,7 +118,7 @@ namespace osu.Game.Utils /// Check the provided combination of mods are valid for a multiplayer match session. /// /// The mods to check. - /// Invalid mods, if any were found. Can be null if all mods were valid. + /// 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 CheckValidForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) => checkValid(mods, m => m.PlayableInMultiplayer, out invalidMods); @@ -127,7 +127,7 @@ namespace osu.Game.Utils /// Check the provided combination of mods are valid as "free mods" in a multiplayer match session. /// /// The mods to check. - /// Invalid mods, if any were found. Can be null if all mods were valid. + /// 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.ValidFreeModInMultiplayer, out invalidMods); From 51e5dd7d0e58fb681db6da9e34e9d435e1026a58 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Mar 2022 02:08:11 +0300 Subject: [PATCH 09/26] Introduce `IsPlayable(...)` and obsolete `UserPlayable` --- .../BeatmapSet/LeaderboardModSelector.cs | 2 +- osu.Game/Rulesets/Mods/IMod.cs | 26 ++++++++++--------- osu.Game/Rulesets/Mods/Mod.cs | 11 +++----- osu.Game/Rulesets/Mods/ModUsage.cs | 26 +++++++++++++++++++ 4 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/ModUsage.cs diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs index 25aed4c980..97b89c6f74 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs @@ -63,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet return; modsContainer.Add(new ModButton(new ModNoMod())); - modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m))); + modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.IsPlayable(ModUsage.Solo)).Select(m => new ModButton(m))); modsContainer.ForEach(button => { diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index 5f4fecb649..bdfb273b13 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -33,24 +33,26 @@ namespace osu.Game.Rulesets.Mods /// IconUsage? Icon { get; } + /// + /// Whether this mod is playable for the given usage. + /// + /// + /// + /// Should be always false for cases where the user is not interacting with the game. + /// Should be false in for mods that make gameplay duration dependent on user input (e.g. ). + /// Should be false in for mods that affect the gameplay duration (e.g. and ). + /// + /// + /// The mod usage. + bool IsPlayable(ModUsage usage); + /// /// Whether this mod is playable by an end user. /// Should be false for cases where the user is not interacting with the game (so it can be excluded from multiplayer selection, for example). /// + [Obsolete("Override IsPlayable instead.")] // Can be removed 20220918 bool UserPlayable { get; } - /// - /// Whether this mod is playable in a multiplayer match. - /// Should be false for mods that make gameplay duration dependent on user input (e.g. ). - /// - bool PlayableInMultiplayer { get; } - - /// - /// Whether this mod is valid to be a "free mod" in a multiplayer match. - /// Should be false for mods that affect the gameplay duration (e.g. and ). - /// - bool ValidFreeModInMultiplayer { get; } - /// /// Create a fresh instance based on this mod. /// diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index a6562b4f4c..00aef1a598 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -91,16 +91,13 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool HasImplementation => this is IApplicableMod; + public virtual bool IsPlayable(ModUsage usage) => true; + [JsonIgnore] + [Obsolete("Override IsPlayable instead.")] // Can be removed 20220918 public virtual bool UserPlayable => true; - [JsonIgnore] - public virtual bool PlayableInMultiplayer => UserPlayable; - - [JsonIgnore] - public virtual bool ValidFreeModInMultiplayer => PlayableInMultiplayer; - - [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009 + [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override IsPlayable to false.")] // Can be removed 20211009 public virtual bool Ranked => false; /// diff --git a/osu.Game/Rulesets/Mods/ModUsage.cs b/osu.Game/Rulesets/Mods/ModUsage.cs new file mode 100644 index 0000000000..82ff6bc418 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModUsage.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Mods +{ + /// + /// The usage of this mod to determine its playability. + /// + public enum ModUsage + { + /// + /// In a solo gameplay session. + /// + Solo, + + /// + /// In a multiplayer match, as a required mod. + /// + MultiplayerRequired, + + /// + /// In a multiplayer match, as a "free" mod. + /// + MultiplayerFree, + } +} From f2248ecc08000c1c259e306ad8d599b0a1c48c8c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Mar 2022 02:11:18 +0300 Subject: [PATCH 10/26] Update usages to use `IsPlayable` instead --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 2 +- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- osu.Game/Rulesets/Mods/UnknownMod.cs | 2 +- osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs | 2 +- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 4 ++-- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- .../Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 3 ++- osu.Game/Screens/Play/SubmittingPlayer.cs | 3 ++- osu.Game/Screens/Ranking/ResultsScreen.cs | 3 ++- osu.Game/Utils/ModUtils.cs | 8 ++++---- 12 files changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 54a4c054c9..17ef9f926e 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; - public override bool PlayableInMultiplayer => false; + public override bool IsPlayable(ModUsage usage) => usage == ModUsage.Solo; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 60b9c29fe0..5491cbec07 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mods public bool RestartOnFail => false; - public override bool UserPlayable => false; + public override bool IsPlayable(ModUsage usage) => false; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 3a11c3034b..810b93c4dd 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModRateAdjust : Mod, IApplicableToRate { - public override bool ValidFreeModInMultiplayer => false; + public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerFree; public abstract BindableNumber SpeedChange { get; } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 59aac62686..fa6a9f3e5b 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } - public override bool ValidFreeModInMultiplayer => false; + public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerFree; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs index b426386d7a..75d86e67bc 100644 --- a/osu.Game/Rulesets/Mods/UnknownMod.cs +++ b/osu.Game/Rulesets/Mods/UnknownMod.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods public override string Description => "This mod could not be resolved by the game."; public override double ScoreMultiplier => 0; - public override bool UserPlayable => false; + public override bool IsPlayable(ModUsage usage) => false; public override ModType Type => ModType.System; diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index d5abaaab4e..974e2b9305 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay public new Func IsValidMod { get => base.IsValidMod; - set => base.IsValidMod = m => m.HasImplementation && m.UserPlayable && value(m); + set => base.IsValidMod = m => m.HasImplementation && m.IsPlayable(ModUsage.Solo) && value(m); } public FreeModSelectOverlay() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index e4f1d9587d..4c1350a56b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -117,8 +117,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.PlayableInMultiplayer; + protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.IsPlayable(ModUsage.MultiplayerRequired); - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidFreeModInMultiplayer; + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.IsPlayable(ModUsage.MultiplayerFree); } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 7b64784316..75e5ea60a4 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a valid mod for online play. - protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable); + protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.IsPlayable(ModUsage.Solo)); /// /// Checks whether a given is valid for per-player free-mod selection. diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 42091c521f..27aa4fe1f1 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; @@ -187,7 +188,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (score.NewValue == null) return; - if (score.NewValue.Mods.Any(m => !m.UserPlayable)) + if (score.NewValue.Mods.Any(m => !m.IsPlayable(ModUsage.Solo))) return; var hitEvents = score.NewValue.HitEvents; diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index b1f2bccddf..5de8aa1dc1 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -11,6 +11,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.API; using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -47,7 +48,7 @@ namespace osu.Game.Screens.Play // Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request. var tcs = new TaskCompletionSource(); - if (Mods.Value.Any(m => !m.UserPlayable)) + if (Mods.Value.Any(m => !m.IsPlayable(ModUsage.Solo))) { handleTokenFailure(new InvalidOperationException("Non-user playable mod selected.")); return false; diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index cb842ce4a0..09b6cc0f23 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.API; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Statistics; @@ -145,7 +146,7 @@ namespace osu.Game.Screens.Ranking if (Score != null) { // only show flair / animation when arriving after watching a play that isn't autoplay. - bool shouldFlair = player != null && Score.Mods.All(m => m.UserPlayable); + bool shouldFlair = player != null && Score.Mods.All(m => m.IsPlayable(ModUsage.Solo)); ScorePanelList.AddScore(Score, shouldFlair); } diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 81b78c18ac..4915f5bdc6 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -115,13 +115,13 @@ namespace osu.Game.Utils => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && !(m is MultiMod), out invalidMods); /// - /// Check the provided combination of mods are valid for a multiplayer match session. + /// Check the provided combination of mods 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 CheckValidForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) - => checkValid(mods, m => m.PlayableInMultiplayer, out invalidMods); + public static bool CheckValidRequiredModsForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) + => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerRequired), out invalidMods); /// /// Check the provided combination of mods are valid as "free mods" in a multiplayer match session. @@ -130,7 +130,7 @@ namespace osu.Game.Utils /// 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.ValidFreeModInMultiplayer, out invalidMods); + => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerFree), out invalidMods); private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods) { From 70e943fbccb1d1e2746aeb605f63b208cf8b6a76 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 20 Mar 2022 06:36:51 +0300 Subject: [PATCH 11/26] `ModUsage.Solo` -> `ModUsage.User` --- osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs | 2 +- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 2 +- osu.Game/Rulesets/Mods/ModUsage.cs | 2 +- osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs | 2 +- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs index 97b89c6f74..caeb757d7a 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs @@ -63,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet return; modsContainer.Add(new ModButton(new ModNoMod())); - modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.IsPlayable(ModUsage.Solo)).Select(m => new ModButton(m))); + modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.IsPlayable(ModUsage.User)).Select(m => new ModButton(m))); modsContainer.ForEach(button => { diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 17ef9f926e..801cd3cba7 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; - public override bool IsPlayable(ModUsage usage) => usage == ModUsage.Solo; + public override bool IsPlayable(ModUsage usage) => usage == ModUsage.User; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; diff --git a/osu.Game/Rulesets/Mods/ModUsage.cs b/osu.Game/Rulesets/Mods/ModUsage.cs index 82ff6bc418..714a99056b 100644 --- a/osu.Game/Rulesets/Mods/ModUsage.cs +++ b/osu.Game/Rulesets/Mods/ModUsage.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods /// /// In a solo gameplay session. /// - Solo, + User, /// /// In a multiplayer match, as a required mod. diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 974e2b9305..37cffd8343 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay public new Func IsValidMod { get => base.IsValidMod; - set => base.IsValidMod = m => m.HasImplementation && m.IsPlayable(ModUsage.Solo) && value(m); + set => base.IsValidMod = m => m.HasImplementation && m.IsPlayable(ModUsage.User) && value(m); } public FreeModSelectOverlay() diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 75e5ea60a4..d4f89b5206 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a valid mod for online play. - protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.IsPlayable(ModUsage.Solo)); + protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.IsPlayable(ModUsage.User)); /// /// Checks whether a given is valid for per-player free-mod selection. diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 27aa4fe1f1..bab391337a 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -188,7 +188,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (score.NewValue == null) return; - if (score.NewValue.Mods.Any(m => !m.IsPlayable(ModUsage.Solo))) + if (score.NewValue.Mods.Any(m => !m.IsPlayable(ModUsage.User))) return; var hitEvents = score.NewValue.HitEvents; diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 5de8aa1dc1..205ee9ab9c 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Play // Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request. var tcs = new TaskCompletionSource(); - if (Mods.Value.Any(m => !m.IsPlayable(ModUsage.Solo))) + if (Mods.Value.Any(m => !m.IsPlayable(ModUsage.User))) { handleTokenFailure(new InvalidOperationException("Non-user playable mod selected.")); return false; diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 09b6cc0f23..ac3816c4ec 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -146,7 +146,7 @@ namespace osu.Game.Screens.Ranking if (Score != null) { // only show flair / animation when arriving after watching a play that isn't autoplay. - bool shouldFlair = player != null && Score.Mods.All(m => m.IsPlayable(ModUsage.Solo)); + bool shouldFlair = player != null && Score.Mods.All(m => m.IsPlayable(ModUsage.User)); ScorePanelList.AddScore(Score, shouldFlair); } From 820a672940b151a43b54f510dc03b8476bd95ce9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 20 Mar 2022 06:37:08 +0300 Subject: [PATCH 12/26] Reword xmldoc to make more sense --- osu.Game/Rulesets/Mods/ModUsage.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModUsage.cs b/osu.Game/Rulesets/Mods/ModUsage.cs index 714a99056b..0892f528d5 100644 --- a/osu.Game/Rulesets/Mods/ModUsage.cs +++ b/osu.Game/Rulesets/Mods/ModUsage.cs @@ -4,22 +4,22 @@ namespace osu.Game.Rulesets.Mods { /// - /// The usage of this mod to determine its playability. + /// The usage of this mod to determine whether it's playable in such context. /// public enum ModUsage { /// - /// In a solo gameplay session. + /// Used for a per-user gameplay session. Determines whether the mod is playable by an end user. /// User, /// - /// In a multiplayer match, as a required mod. + /// Used as a "required mod" for a multiplayer match. /// MultiplayerRequired, /// - /// In a multiplayer match, as a "free" mod. + /// Used as a "free mod" for a multiplayer match. /// MultiplayerFree, } From add9f3ec9177815f7dab386927c47ad436d9576f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 20 Mar 2022 13:12:24 +0300 Subject: [PATCH 13/26] Rename multiplayer mod usages to make more sense --- osu.Game/Rulesets/Mods/IMod.cs | 4 ++-- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- osu.Game/Rulesets/Mods/ModUsage.cs | 12 +++++++----- .../Multiplayer/MultiplayerMatchSongSelect.cs | 4 ++-- osu.Game/Utils/ModUtils.cs | 4 ++-- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index bdfb273b13..325b75b76e 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -39,8 +39,8 @@ namespace osu.Game.Rulesets.Mods /// /// /// Should be always false for cases where the user is not interacting with the game. - /// Should be false in for mods that make gameplay duration dependent on user input (e.g. ). - /// Should be false in for mods that affect the gameplay duration (e.g. and ). + /// Should be false in for mods that make gameplay duration dependent on user input (e.g. ). + /// Should be false in for mods that affect the gameplay duration (e.g. and ). /// /// /// The mod usage. diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 810b93c4dd..ab724673b6 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModRateAdjust : Mod, IApplicableToRate { - public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerFree; + public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerPerPlayer; public abstract BindableNumber SpeedChange { get; } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index fa6a9f3e5b..96b38301b5 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } - public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerFree; + public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerPerPlayer; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; diff --git a/osu.Game/Rulesets/Mods/ModUsage.cs b/osu.Game/Rulesets/Mods/ModUsage.cs index 0892f528d5..59a62bfc6f 100644 --- a/osu.Game/Rulesets/Mods/ModUsage.cs +++ b/osu.Game/Rulesets/Mods/ModUsage.cs @@ -9,18 +9,20 @@ namespace osu.Game.Rulesets.Mods public enum ModUsage { /// - /// Used for a per-user gameplay session. Determines whether the mod is playable by an end user. + /// Used for a per-user gameplay session. + /// Determines whether the mod is playable by an end user. /// User, /// - /// Used as a "required mod" for a multiplayer match. + /// Used in multiplayer but must be applied to all users. + /// This is generally the case for mods which affect the length of gameplay. /// - MultiplayerRequired, + MultiplayerRoomWide, /// - /// Used as a "free mod" for a multiplayer match. + /// Used in multiplayer either at a room or per-player level (i.e. "free mod"). /// - MultiplayerFree, + MultiplayerPerPlayer, } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 4c1350a56b..63467dbb9d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -117,8 +117,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.IsPlayable(ModUsage.MultiplayerRequired); + protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.IsPlayable(ModUsage.MultiplayerRoomWide); - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.IsPlayable(ModUsage.MultiplayerFree); + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.IsPlayable(ModUsage.MultiplayerPerPlayer); } } diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 4915f5bdc6..946386a7ce 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -121,7 +121,7 @@ namespace osu.Game.Utils /// 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) - => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerRequired), out invalidMods); + => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerRoomWide), out invalidMods); /// /// Check the provided combination of mods are valid as "free mods" in a multiplayer match session. @@ -130,7 +130,7 @@ namespace osu.Game.Utils /// 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.IsPlayable(ModUsage.MultiplayerFree), out invalidMods); + => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerPerPlayer), out invalidMods); private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods) { From 5f878ed82b98a1d3ec14af96879c4d68bee8d167 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 20 Mar 2022 16:07:08 +0300 Subject: [PATCH 14/26] Delegate `IsPlayable` to the obsoleted `UserPlayable` by default Handles consumers who still haven't updated to use `IsPlayable` yet. --- osu.Game/Rulesets/Mods/Mod.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 00aef1a598..debb1a1e48 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -91,7 +91,9 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool HasImplementation => this is IApplicableMod; - public virtual bool IsPlayable(ModUsage usage) => true; +#pragma warning disable 618 + public virtual bool IsPlayable(ModUsage usage) => UserPlayable; +#pragma warning restore 618 [JsonIgnore] [Obsolete("Override IsPlayable instead.")] // Can be removed 20220918 From 145fca2704457fe58baaf294b960812ce41dd49d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 20 Mar 2022 16:17:19 +0300 Subject: [PATCH 15/26] Fix failing test scenes --- osu.Game.Tests/Mods/ModUtilsTest.cs | 12 ------------ osu.Game/Utils/ModUtils.cs | 6 +++--- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 4c126f0a3b..e858abfe44 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -134,18 +134,6 @@ namespace osu.Game.Tests.Mods private static readonly object[] invalid_mod_test_scenarios = { - // incompatible pair. - new object[] - { - new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() }, - new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) } - }, - // incompatible pair with derived class. - new object[] - { - new Mod[] { new OsuModNightcore(), new OsuModHalfTime() }, - new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) } - }, // system mod. new object[] { diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 946386a7ce..0d46494319 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -106,7 +106,7 @@ namespace osu.Game.Utils } /// - /// Check the provided combination of mods are valid for a local gameplay session. + /// 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. @@ -115,7 +115,7 @@ namespace osu.Game.Utils => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && !(m is MultiMod), out invalidMods); /// - /// Check the provided combination of mods are valid as "required mods" in a multiplayer match session. + /// 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. @@ -124,7 +124,7 @@ namespace osu.Game.Utils => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerRoomWide), out invalidMods); /// - /// Check the provided combination of mods are valid as "free mods" in a multiplayer match session. + /// Checks that all s in a combination are valid as "free mods" in a multiplayer match session. /// /// The mods to check. /// Invalid mods, if any were found. Will be null if all mods were valid. From b218046fa28ef9ebb257663adf5388d9174345ee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 23 Mar 2022 15:38:48 +0300 Subject: [PATCH 16/26] Remove redundant line from mod usage --- osu.Game/Rulesets/Mods/ModUsage.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModUsage.cs b/osu.Game/Rulesets/Mods/ModUsage.cs index 59a62bfc6f..746387a062 100644 --- a/osu.Game/Rulesets/Mods/ModUsage.cs +++ b/osu.Game/Rulesets/Mods/ModUsage.cs @@ -10,7 +10,6 @@ namespace osu.Game.Rulesets.Mods { /// /// Used for a per-user gameplay session. - /// Determines whether the mod is playable by an end user. /// User, From 6cd67928aba27e2936fd8f0a6cfc0ae7838e1e67 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 23 Mar 2022 15:48:50 +0300 Subject: [PATCH 17/26] Simplify documentation of `ModUsage` --- osu.Game/Rulesets/Mods/ModUsage.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModUsage.cs b/osu.Game/Rulesets/Mods/ModUsage.cs index 746387a062..04d99149dd 100644 --- a/osu.Game/Rulesets/Mods/ModUsage.cs +++ b/osu.Game/Rulesets/Mods/ModUsage.cs @@ -4,23 +4,23 @@ namespace osu.Game.Rulesets.Mods { /// - /// The usage of this mod to determine whether it's playable in such context. + /// The context in which a is playable. /// public enum ModUsage { /// - /// Used for a per-user gameplay session. + /// This mod can be used for a per-user gameplay session. /// User, /// - /// Used in multiplayer but must be applied to all users. + /// This mod can be used in multiplayer but must be applied to all users. /// This is generally the case for mods which affect the length of gameplay. /// MultiplayerRoomWide, /// - /// Used in multiplayer either at a room or per-player level (i.e. "free mod"). + /// This mod can be used in multiplayer either at a room or per-player level (i.e. "free mod"). /// MultiplayerPerPlayer, } From 20e277d2e587069d0534bf457efa8999d0ac9629 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 4 May 2022 17:08:41 +0300 Subject: [PATCH 18/26] Apply proposed naming changes --- osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs | 2 +- osu.Game/Rulesets/Mods/IMod.cs | 4 ++-- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 2 +- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- osu.Game/Rulesets/Mods/ModUsage.cs | 6 +++--- osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs | 2 +- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 4 ++-- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- .../Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- osu.Game/Utils/ModUtils.cs | 4 ++-- 13 files changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs index caeb757d7a..17ea117018 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs @@ -63,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet return; modsContainer.Add(new ModButton(new ModNoMod())); - modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.IsPlayable(ModUsage.User)).Select(m => new ModButton(m))); + modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.IsPlayable(ModUsage.SoloLocal)).Select(m => new ModButton(m))); modsContainer.ForEach(button => { diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index 325b75b76e..d37ef53a66 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -39,8 +39,8 @@ namespace osu.Game.Rulesets.Mods /// /// /// Should be always false for cases where the user is not interacting with the game. - /// Should be false in for mods that make gameplay duration dependent on user input (e.g. ). - /// Should be false in for mods that affect the gameplay duration (e.g. and ). + /// Should be false in for mods that make gameplay duration dependent on user input (e.g. ). + /// Should be false in for mods that affect the gameplay duration (e.g. and ). /// /// /// The mod usage. diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 801cd3cba7..700ad30c08 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; - public override bool IsPlayable(ModUsage usage) => usage == ModUsage.User; + public override bool IsPlayable(ModUsage usage) => usage == ModUsage.SoloLocal; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 59d9026f3b..acb39bfbe6 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModRateAdjust : Mod, IApplicableToRate { - public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerPerPlayer; + public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerLocal; public abstract BindableNumber SpeedChange { get; } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 96b38301b5..c554c39010 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } - public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerPerPlayer; + public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerLocal; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; diff --git a/osu.Game/Rulesets/Mods/ModUsage.cs b/osu.Game/Rulesets/Mods/ModUsage.cs index 04d99149dd..3f25154d73 100644 --- a/osu.Game/Rulesets/Mods/ModUsage.cs +++ b/osu.Game/Rulesets/Mods/ModUsage.cs @@ -11,17 +11,17 @@ namespace osu.Game.Rulesets.Mods /// /// This mod can be used for a per-user gameplay session. /// - User, + SoloLocal, /// /// This mod can be used in multiplayer but must be applied to all users. /// This is generally the case for mods which affect the length of gameplay. /// - MultiplayerRoomWide, + MultiplayerGlobal, /// /// This mod can be used in multiplayer either at a room or per-player level (i.e. "free mod"). /// - MultiplayerPerPlayer, + MultiplayerLocal, } } diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 37cffd8343..13d5dd8927 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay public new Func IsValidMod { get => base.IsValidMod; - set => base.IsValidMod = m => m.HasImplementation && m.IsPlayable(ModUsage.User) && value(m); + set => base.IsValidMod = m => m.HasImplementation && m.IsPlayable(ModUsage.SoloLocal) && value(m); } public FreeModSelectOverlay() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 5bf217031a..d7e752623d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -95,8 +95,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.IsPlayable(ModUsage.MultiplayerRoomWide); + protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.IsPlayable(ModUsage.MultiplayerGlobal); - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.IsPlayable(ModUsage.MultiplayerPerPlayer); + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.IsPlayable(ModUsage.MultiplayerLocal); } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index d9da230f58..b8b0604c79 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a valid mod for online play. - protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.IsPlayable(ModUsage.User)); + protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.IsPlayable(ModUsage.SoloLocal)); /// /// Checks whether a given is valid for per-player free-mod selection. diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 554b58f481..af2cecfba5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -188,7 +188,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (score.NewValue == null) return; - if (score.NewValue.Mods.Any(m => !m.IsPlayable(ModUsage.User))) + if (score.NewValue.Mods.Any(m => !m.IsPlayable(ModUsage.SoloLocal))) return; var hitEvents = score.NewValue.HitEvents; diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 6846992b13..b33ff87d55 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Play // Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request. var tcs = new TaskCompletionSource(); - if (Mods.Value.Any(m => !m.IsPlayable(ModUsage.User))) + if (Mods.Value.Any(m => !m.IsPlayable(ModUsage.SoloLocal))) { handleTokenFailure(new InvalidOperationException("Non-user playable mod selected.")); return false; diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index ee2c00a135..e0ae0309ce 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -146,7 +146,7 @@ namespace osu.Game.Screens.Ranking if (Score != null) { // only show flair / animation when arriving after watching a play that isn't autoplay. - bool shouldFlair = player != null && Score.Mods.All(m => m.IsPlayable(ModUsage.User)); + bool shouldFlair = player != null && Score.Mods.All(m => m.IsPlayable(ModUsage.SoloLocal)); ScorePanelList.AddScore(Score, shouldFlair); } diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index c5cf469f7c..e19020a959 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -121,7 +121,7 @@ namespace osu.Game.Utils /// 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) - => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerRoomWide), out invalidMods); + => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerGlobal), out invalidMods); /// /// Checks that all s in a combination are valid as "free mods" in a multiplayer match session. @@ -130,7 +130,7 @@ namespace osu.Game.Utils /// 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.IsPlayable(ModUsage.MultiplayerPerPlayer), out invalidMods); + => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerLocal), out invalidMods); private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods) { From 8f04db5df5136f9431e62a6fffce273f2ba8d6bc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 4 May 2022 17:21:19 +0300 Subject: [PATCH 19/26] Bring back behaviour of checking incompatibility on gameplay validity --- osu.Game.Tests/Mods/ModUtilsTest.cs | 12 ++++++++++++ osu.Game/OsuGame.cs | 15 +++++---------- osu.Game/Utils/ModUtils.cs | 15 +++++++++++---- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index e858abfe44..4c126f0a3b 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -134,6 +134,18 @@ namespace osu.Game.Tests.Mods private static readonly object[] invalid_mod_test_scenarios = { + // incompatible pair. + new object[] + { + new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() }, + new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) } + }, + // incompatible pair with derived class. + new object[] + { + new Mod[] { new OsuModNightcore(), new OsuModHalfTime() }, + new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) } + }, // system mod. new object[] { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 29985b0893..e9fe8c43de 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -577,16 +577,11 @@ namespace osu.Game if (SelectedMods.Disabled) return; - var validMods = mods.NewValue; - - if (!ModUtils.CheckCompatibleSet(validMods, out var incompatible)) - validMods = validMods.Except(incompatible).ToArray(); - - if (!ModUtils.CheckValidForGameplay(validMods, out var invalid)) - validMods = validMods.Except(invalid).ToArray(); - - // ensure we always have a valid set of mods. - SelectedMods.Value = validMods; + if (!ModUtils.CheckValidForGameplay(mods.NewValue, out var invalid)) + { + // ensure we always have a valid set of mods. + SelectedMods.Value = mods.NewValue.Except(invalid).ToArray(); + } } #endregion diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index e19020a959..c18125054a 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -112,7 +112,7 @@ namespace osu.Game.Utils /// 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) - => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && !(m is MultiMod), out invalidMods); + => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && !(m is MultiMod), out invalidMods, true); /// /// Checks that all s in a combination are valid as "required mods" in a multiplayer match session. @@ -121,7 +121,7 @@ namespace osu.Game.Utils /// 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) - => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerGlobal), out invalidMods); + => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerGlobal), out invalidMods, true); /// /// Checks that all s in a combination are valid as "free mods" in a multiplayer match session. @@ -130,13 +130,20 @@ namespace osu.Game.Utils /// 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.IsPlayable(ModUsage.MultiplayerLocal), out invalidMods); + => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerLocal), out invalidMods, false); - private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods) + private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods, bool checkCompatibility) { mods = mods.ToArray(); invalidMods = null; + if (checkCompatibility) + { + // exclude multi mods from compatibility checks. + // the loop below automatically marks all multi mods as not valid for gameplay anyway. + CheckCompatibleSet(mods.Where(m => !(m is MultiMod)), out invalidMods); + } + foreach (var mod in mods) { if (!valid(mod)) From 8488a29e9e4db0dd26ab6ec2ecacd24681b764b2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 4 May 2022 17:22:11 +0300 Subject: [PATCH 20/26] Renew obsoletion date --- osu.Game/Rulesets/Mods/IMod.cs | 2 +- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index d37ef53a66..c0d4aa5c9f 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mods /// Whether this mod is playable by an end user. /// Should be false for cases where the user is not interacting with the game (so it can be excluded from multiplayer selection, for example). /// - [Obsolete("Override IsPlayable instead.")] // Can be removed 20220918 + [Obsolete("Override IsPlayable instead.")] // Can be removed 20221104 bool UserPlayable { get; } /// diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index debb1a1e48..b2f1b7e24f 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Mods #pragma warning restore 618 [JsonIgnore] - [Obsolete("Override IsPlayable instead.")] // Can be removed 20220918 + [Obsolete("Override IsPlayable instead.")] // Can be removed 20221104 public virtual bool UserPlayable => true; [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override IsPlayable to false.")] // Can be removed 20211009 From d0df9e8051f7580b94e888079557d6cf62018b40 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 4 May 2022 18:56:27 +0300 Subject: [PATCH 21/26] Inline `CheckCompatibleSet` method to avoid ugly boolean flag --- osu.Game/Utils/ModUtils.cs | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index c18125054a..40db981ef0 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -112,7 +112,16 @@ namespace osu.Game.Utils /// 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) - => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && !(m is MultiMod), out invalidMods, true); + { + mods = mods.ToArray(); + + // exclude multi mods from compatibility checks. + // the loop below automatically marks all multi mods as not valid for gameplay anyway. + if (!CheckCompatibleSet(mods.Where(m => !(m is MultiMod)), out invalidMods)) + return false; + + return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && !(m is MultiMod), out invalidMods); + } /// /// Checks that all s in a combination are valid as "required mods" in a multiplayer match session. @@ -121,7 +130,14 @@ namespace osu.Game.Utils /// 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) - => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerGlobal), out invalidMods, true); + { + mods = mods.ToArray(); + + if (!CheckCompatibleSet(mods, out invalidMods)) + return false; + + return checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerGlobal), out invalidMods); + } /// /// Checks that all s in a combination are valid as "free mods" in a multiplayer match session. @@ -130,20 +146,13 @@ namespace osu.Game.Utils /// 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.IsPlayable(ModUsage.MultiplayerLocal), out invalidMods, false); + => checkValid(mods, m => m.IsPlayable(ModUsage.MultiplayerLocal), out invalidMods); - private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods, bool checkCompatibility) + private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods) { mods = mods.ToArray(); invalidMods = null; - if (checkCompatibility) - { - // exclude multi mods from compatibility checks. - // the loop below automatically marks all multi mods as not valid for gameplay anyway. - CheckCompatibleSet(mods.Where(m => !(m is MultiMod)), out invalidMods); - } - foreach (var mod in mods) { if (!valid(mod)) From e3c7c5d0b9ceca3ea19683280321c66e08155c42 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 5 May 2022 08:12:54 +0300 Subject: [PATCH 22/26] Improve validity methods to include system, non-implemented, and multi mods --- osu.Game/Utils/ModUtils.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 40db981ef0..f11a06e3be 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -115,12 +115,15 @@ namespace osu.Game.Utils { mods = mods.ToArray(); - // exclude multi mods from compatibility checks. - // the loop below automatically marks all multi mods as not valid for gameplay anyway. - if (!CheckCompatibleSet(mods.Where(m => !(m is MultiMod)), out invalidMods)) + // 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; - return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && !(m is MultiMod), out invalidMods); + if (!CheckCompatibleSet(mods, out invalidMods)) + return false; + + return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation, out invalidMods); } /// @@ -133,10 +136,15 @@ namespace osu.Game.Utils { 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.IsPlayable(ModUsage.MultiplayerGlobal), out invalidMods); + return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.IsPlayable(ModUsage.MultiplayerGlobal), out invalidMods); } /// @@ -146,7 +154,7 @@ namespace osu.Game.Utils /// 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.IsPlayable(ModUsage.MultiplayerLocal), out invalidMods); + => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.IsPlayable(ModUsage.MultiplayerLocal) && !(m is MultiMod), out invalidMods); private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods) { From 86aa2125fe9878eda71857a0e1d0ca0823ab2899 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 5 May 2022 08:13:29 +0300 Subject: [PATCH 23/26] Add test coverage for multiplayer mod validity methods --- osu.Game.Tests/Mods/ModUtilsTest.cs | 161 ++++++++++++++++++++++++++-- 1 file changed, 153 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 4c126f0a3b..2447233d3c 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -137,33 +137,137 @@ namespace osu.Game.Tests.Mods // incompatible pair. new object[] { - new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() }, - new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) } + new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) } }, // incompatible pair with derived class. new object[] { - new Mod[] { new OsuModNightcore(), new OsuModHalfTime() }, - new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) } + new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) } }, // system mod. new object[] { - new Mod[] { new OsuModDoubleTime(), new OsuModTouchDevice() }, + new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, new[] { typeof(OsuModTouchDevice) } }, // multi mod. new object[] { - new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() }, + new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) }, new[] { typeof(MultiMod) } }, + // invalid multiplayer mod is valid for local. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() }, + null + }, + // invalid free mod is valid for local. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, + null + }, // valid pair. new object[] { - new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() }, + new Mod[] { new OsuModHidden(), new OsuModHardRock() }, null - } + }, + }; + + private static readonly object[] invalid_multiplayer_mod_test_scenarios = + { + // incompatible pair. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) } + }, + // incompatible pair with derived class. + new object[] + { + new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) } + }, + // system mod. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, + new[] { typeof(OsuModTouchDevice) } + }, + // multi mod. + new object[] + { + new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) }, + new[] { typeof(MultiMod) } + }, + // invalid multiplayer mod. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() }, + new[] { typeof(InvalidMultiplayerMod) } + }, + // invalid free mod is valid for multiplayer global. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, + null + }, + // valid pair. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + null + }, + }; + + private static readonly object[] invalid_free_mod_test_scenarios = + { + // system mod. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, + new[] { typeof(OsuModTouchDevice) } + }, + // multi mod. + new object[] + { + new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) }, + new[] { typeof(MultiMod) } + }, + // invalid multiplayer mod. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() }, + new[] { typeof(InvalidMultiplayerMod) } + }, + // invalid free mod. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, + new[] { typeof(InvalidMultiplayerFreeMod) } + }, + // incompatible pair is valid for free mods. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() }, + null, + }, + // incompatible pair with derived class is valid for free mods. + new object[] + { + new Mod[] { new OsuModDeflate(), new OsuModSpinIn() }, + null, + }, + // valid pair. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + null + }, }; [TestCaseSource(nameof(invalid_mod_test_scenarios))] @@ -179,6 +283,32 @@ namespace osu.Game.Tests.Mods Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); } + [TestCaseSource(nameof(invalid_multiplayer_mod_test_scenarios))] + public void TestInvalidMultiplayerModScenarios(Mod[] inputMods, Type[] expectedInvalid) + { + bool isValid = ModUtils.CheckValidRequiredModsForMultiplayer(inputMods, out var invalid); + + Assert.That(isValid, Is.EqualTo(expectedInvalid == null)); + + if (isValid) + Assert.IsNull(invalid); + else + Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); + } + + [TestCaseSource(nameof(invalid_free_mod_test_scenarios))] + public void TestInvalidFreeModScenarios(Mod[] inputMods, Type[] expectedInvalid) + { + bool isValid = ModUtils.CheckValidFreeModsForMultiplayer(inputMods, out var invalid); + + Assert.That(isValid, Is.EqualTo(expectedInvalid == null)); + + if (isValid) + Assert.IsNull(invalid); + else + Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); + } + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } @@ -187,6 +317,21 @@ namespace osu.Game.Tests.Mods { } + public class InvalidMultiplayerMod : Mod + { + public override string Name => string.Empty; + public override string Description => string.Empty; + public override string Acronym => string.Empty; + public override double ScoreMultiplier => 1; + public override bool HasImplementation => true; + public override bool IsPlayable(ModUsage usage) => usage == ModUsage.SoloLocal; + } + + private class InvalidMultiplayerFreeMod : InvalidMultiplayerMod + { + public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerLocal; + } + public interface IModCompatibilitySpecification { } From 8501a4161958514bfae52c5e78aee2ee26a870e0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 5 May 2022 14:37:38 +0300 Subject: [PATCH 24/26] Bring back separate bool properties as non-cascading --- osu.Game.Tests/Mods/ModUtilsTest.cs | 12 ++++++--- .../TestSceneFreeModSelectScreen.cs | 3 +-- .../BeatmapSet/LeaderboardModSelector.cs | 2 +- osu.Game/Rulesets/Mods/IMod.cs | 26 +++++++++--------- osu.Game/Rulesets/Mods/Mod.cs | 11 ++++---- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 3 ++- osu.Game/Rulesets/Mods/ModAutoplay.cs | 4 ++- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- osu.Game/Rulesets/Mods/ModUsage.cs | 27 ------------------- osu.Game/Rulesets/Mods/UnknownMod.cs | 4 ++- .../OnlinePlay/FreeModSelectOverlay.cs | 2 +- .../Screens/OnlinePlay/FreeModSelectScreen.cs | 2 +- .../Multiplayer/MultiplayerMatchSongSelect.cs | 4 +-- .../OnlinePlay/OnlinePlaySongSelect.cs | 2 +- .../PlayerSettings/BeatmapOffsetControl.cs | 3 +-- osu.Game/Screens/Play/SubmittingPlayer.cs | 3 +-- osu.Game/Screens/Ranking/ResultsScreen.cs | 3 +-- osu.Game/Utils/ModUtils.cs | 4 +-- 19 files changed, 49 insertions(+), 70 deletions(-) delete mode 100644 osu.Game/Rulesets/Mods/ModUsage.cs diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 2447233d3c..6c9dddf51f 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -324,12 +324,18 @@ namespace osu.Game.Tests.Mods public override string Acronym => string.Empty; public override double ScoreMultiplier => 1; public override bool HasImplementation => true; - public override bool IsPlayable(ModUsage usage) => usage == ModUsage.SoloLocal; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; } - private class InvalidMultiplayerFreeMod : InvalidMultiplayerMod + private class InvalidMultiplayerFreeMod : Mod { - public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerLocal; + public override string Name => string.Empty; + public override string Description => string.Empty; + public override string Acronym => string.Empty; + public override double ScoreMultiplier => 1; + public override bool HasImplementation => true; + public override bool ValidForMultiplayerAsFreeMod => false; } public interface IModCompatibilitySpecification diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs index fad784a09e..b5f901e51d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Overlays.Mods; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer @@ -29,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("all visible mods are playable", () => this.ChildrenOfType() .Where(panel => panel.IsPresent) - .All(panel => panel.Mod.HasImplementation && panel.Mod.IsPlayable(ModUsage.SoloLocal))); + .All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable)); AddToggleStep("toggle visibility", visible => { diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs index 17ea117018..25aed4c980 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs @@ -63,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet return; modsContainer.Add(new ModButton(new ModNoMod())); - modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.IsPlayable(ModUsage.SoloLocal)).Select(m => new ModButton(m))); + modsContainer.AddRange(rulesetInstance.AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m))); modsContainer.ForEach(button => { diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index c0d4aa5c9f..30fa1ea8cb 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -33,26 +33,24 @@ namespace osu.Game.Rulesets.Mods /// IconUsage? Icon { get; } - /// - /// Whether this mod is playable for the given usage. - /// - /// - /// - /// Should be always false for cases where the user is not interacting with the game. - /// Should be false in for mods that make gameplay duration dependent on user input (e.g. ). - /// Should be false in for mods that affect the gameplay duration (e.g. and ). - /// - /// - /// The mod usage. - bool IsPlayable(ModUsage usage); - /// /// Whether this mod is playable by an end user. /// Should be false for cases where the user is not interacting with the game (so it can be excluded from multiplayer selection, for example). /// - [Obsolete("Override IsPlayable instead.")] // Can be removed 20221104 bool UserPlayable { get; } + /// + /// Whether this mod is valid for multiplayer matches. + /// Should be false for mods that make gameplay duration dependent on user input (e.g. ). + /// + bool ValidForMultiplayer { get; } + + /// + /// Whether this mod is valid as a free mod in multiplayer matches. + /// Should be false for mods that affect the gameplay duration (e.g. and ). + /// + bool ValidForMultiplayerAsFreeMod { get; } + /// /// Create a fresh instance based on this mod. /// diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b2f1b7e24f..93990d36e1 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -91,13 +91,14 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool HasImplementation => this is IApplicableMod; -#pragma warning disable 618 - public virtual bool IsPlayable(ModUsage usage) => UserPlayable; -#pragma warning restore 618 + [JsonIgnore] + public virtual bool UserPlayable => true; [JsonIgnore] - [Obsolete("Override IsPlayable instead.")] // Can be removed 20221104 - public virtual bool UserPlayable => true; + public virtual bool ValidForMultiplayer => true; + + [JsonIgnore] + public virtual bool ValidForMultiplayerAsFreeMod => true; [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override IsPlayable to false.")] // Can be removed 20211009 public virtual bool Ranked => false; diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 700ad30c08..93251f7b2d 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -31,7 +31,8 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; - public override bool IsPlayable(ModUsage usage) => usage == ModUsage.SoloLocal; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 9df25b006d..0ebe11b393 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -24,7 +24,9 @@ namespace osu.Game.Rulesets.Mods public bool RestartOnFail => false; - public override bool IsPlayable(ModUsage usage) => false; + public override bool UserPlayable => false; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index acb39bfbe6..05953f903f 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModRateAdjust : Mod, IApplicableToRate { - public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerLocal; + public override bool ValidForMultiplayerAsFreeMod => false; public abstract BindableNumber SpeedChange { get; } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index c554c39010..fe6d54332c 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } - public override bool IsPlayable(ModUsage usage) => usage != ModUsage.MultiplayerLocal; + public override bool ValidForMultiplayerAsFreeMod => false; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; diff --git a/osu.Game/Rulesets/Mods/ModUsage.cs b/osu.Game/Rulesets/Mods/ModUsage.cs deleted file mode 100644 index 3f25154d73..0000000000 --- a/osu.Game/Rulesets/Mods/ModUsage.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Mods -{ - /// - /// The context in which a is playable. - /// - public enum ModUsage - { - /// - /// This mod can be used for a per-user gameplay session. - /// - SoloLocal, - - /// - /// This mod can be used in multiplayer but must be applied to all users. - /// This is generally the case for mods which affect the length of gameplay. - /// - MultiplayerGlobal, - - /// - /// This mod can be used in multiplayer either at a room or per-player level (i.e. "free mod"). - /// - MultiplayerLocal, - } -} diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs index 75d86e67bc..72de0ad653 100644 --- a/osu.Game/Rulesets/Mods/UnknownMod.cs +++ b/osu.Game/Rulesets/Mods/UnknownMod.cs @@ -15,7 +15,9 @@ namespace osu.Game.Rulesets.Mods public override string Description => "This mod could not be resolved by the game."; public override double ScoreMultiplier => 0; - public override bool IsPlayable(ModUsage usage) => false; + public override bool UserPlayable => false; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; public override ModType Type => ModType.System; diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 13d5dd8927..d5abaaab4e 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay public new Func IsValidMod { get => base.IsValidMod; - set => base.IsValidMod = m => m.HasImplementation && m.IsPlayable(ModUsage.SoloLocal) && value(m); + set => base.IsValidMod = m => m.HasImplementation && m.UserPlayable && value(m); } public FreeModSelectOverlay() diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs index 415ae60706..5a7a60b479 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay public new Func IsValidMod { get => base.IsValidMod; - set => base.IsValidMod = m => m.IsPlayable(ModUsage.SoloLocal) && value.Invoke(m); + set => base.IsValidMod = m => m.UserPlayable && value.Invoke(m); } public FreeModSelectScreen() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index d7e752623d..929c3ee321 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -95,8 +95,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.IsPlayable(ModUsage.MultiplayerGlobal); + protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer; - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.IsPlayable(ModUsage.MultiplayerLocal); + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index b8b0604c79..6a559dbb2c 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a valid mod for online play. - protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.IsPlayable(ModUsage.SoloLocal)); + protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable); /// /// Checks whether a given is valid for per-player free-mod selection. diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index af2cecfba5..1662ca399f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -20,7 +20,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays.Settings; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; @@ -188,7 +187,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (score.NewValue == null) return; - if (score.NewValue.Mods.Any(m => !m.IsPlayable(ModUsage.SoloLocal))) + if (score.NewValue.Mods.Any(m => !m.UserPlayable)) return; var hitEvents = score.NewValue.HitEvents; diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index b33ff87d55..b62dc1e5a6 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -11,7 +11,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -48,7 +47,7 @@ namespace osu.Game.Screens.Play // Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request. var tcs = new TaskCompletionSource(); - if (Mods.Value.Any(m => !m.IsPlayable(ModUsage.SoloLocal))) + if (Mods.Value.Any(m => !m.UserPlayable)) { handleTokenFailure(new InvalidOperationException("Non-user playable mod selected.")); return false; diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index e0ae0309ce..98514cd846 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -18,7 +18,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.API; -using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Statistics; @@ -146,7 +145,7 @@ namespace osu.Game.Screens.Ranking if (Score != null) { // only show flair / animation when arriving after watching a play that isn't autoplay. - bool shouldFlair = player != null && Score.Mods.All(m => m.IsPlayable(ModUsage.SoloLocal)); + bool shouldFlair = player != null && Score.Mods.All(m => m.UserPlayable); ScorePanelList.AddScore(Score, shouldFlair); } diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index f11a06e3be..a252a9b416 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -144,7 +144,7 @@ namespace osu.Game.Utils if (!CheckCompatibleSet(mods, out invalidMods)) return false; - return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.IsPlayable(ModUsage.MultiplayerGlobal), out invalidMods); + return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayer, out invalidMods); } /// @@ -154,7 +154,7 @@ namespace osu.Game.Utils /// 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.IsPlayable(ModUsage.MultiplayerLocal) && !(m is MultiMod), out 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) { From 43c9058d09dbff83898f8b7f841c524d8a72d454 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 5 May 2022 14:39:05 +0300 Subject: [PATCH 25/26] Fix wrong obsolete message --- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 93990d36e1..af1550f8a9 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool ValidForMultiplayerAsFreeMod => true; - [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override IsPlayable to false.")] // Can be removed 20211009 + [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009 public virtual bool Ranked => false; /// From 74c0cb2f6eb9ddf40563e70da00f9454e29b22ca Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 5 May 2022 16:16:55 +0300 Subject: [PATCH 26/26] Add note about not checking compatibility in free mods validity method --- osu.Game/Utils/ModUtils.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index a252a9b416..ea092a8ca3 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -150,6 +150,11 @@ namespace osu.Game.Utils /// /// 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.