diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index df0a41455f..4b0939db16 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModTraceable : ModWithVisibilityAdjustment + public class OsuModTraceable : ModWithVisibilityAdjustment { public override string Name => "Traceable"; public override string Acronym => "TC"; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 95c333e9f4..faa5d9e6fc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -7,17 +7,23 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Select; @@ -137,8 +143,30 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("mods not changed", () => SelectedMods.Value.Single() is TaikoModDoubleTime); } + [TestCase(typeof(OsuModHidden), typeof(OsuModHidden))] // Same mod. + [TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible. + public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod) + { + AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) }); + AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) }); + + AddAssert("freemods empty", () => songSelect.FreeMods.Value.Count == 0); + assertHasFreeModButton(allowedMod, false); + assertHasFreeModButton(requiredMod, false); + } + + private void assertHasFreeModButton(Type type, bool hasButton = true) + { + AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay", + () => songSelect.ChildrenOfType().Single().ChildrenOfType().All(b => b.Mod.GetType() != type)); + } + private class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelect { + public new Bindable> Mods => base.Mods; + + public new Bindable> FreeMods => base.FreeMods; + public new BeatmapCarousel Carousel => base.Carousel; } } diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index c3e56abd05..aa8a5efd39 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -23,13 +23,15 @@ namespace osu.Game.Overlays.Mods public FillFlowContainer ButtonsContainer { get; } + protected IReadOnlyList Buttons { get; private set; } = Array.Empty(); + public Action Action; public Key[] ToggleKeys; public readonly ModType ModType; - public IEnumerable SelectedMods => buttons.Select(b => b.SelectedMod).Where(m => m != null); + public IEnumerable SelectedMods => Buttons.Select(b => b.SelectedMod).Where(m => m != null); private CancellationTokenSource modsLoadCts; @@ -77,7 +79,7 @@ namespace osu.Game.Overlays.Mods ButtonsContainer.ChildrenEnumerable = c; }, (modsLoadCts = new CancellationTokenSource()).Token); - buttons = modContainers.OfType().ToArray(); + Buttons = modContainers.OfType().ToArray(); header.FadeIn(200); this.FadeIn(200); @@ -88,8 +90,6 @@ namespace osu.Game.Overlays.Mods { } - private ModButton[] buttons = Array.Empty(); - protected override bool OnKeyDown(KeyDownEvent e) { if (e.ControlPressed) return false; @@ -97,8 +97,8 @@ namespace osu.Game.Overlays.Mods if (ToggleKeys != null) { var index = Array.IndexOf(ToggleKeys, e.Key); - if (index > -1 && index < buttons.Length) - buttons[index].SelectNext(e.ShiftPressed ? -1 : 1); + if (index > -1 && index < Buttons.Count) + Buttons[index].SelectNext(e.ShiftPressed ? -1 : 1); } return base.OnKeyDown(e); @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in buttons.Where(b => !b.Selected)) + foreach (var button in Buttons.Where(b => !b.Selected)) pendingSelectionOperations.Enqueue(() => button.SelectAt(0)); } @@ -151,7 +151,7 @@ namespace osu.Game.Overlays.Mods public void DeselectAll() { pendingSelectionOperations.Clear(); - DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); + DeselectTypes(Buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); } /// @@ -161,7 +161,7 @@ namespace osu.Game.Overlays.Mods /// Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow. public void DeselectTypes(IEnumerable modTypes, bool immediate = false) { - foreach (var button in buttons) + foreach (var button in Buttons) { if (button.SelectedMod == null) continue; @@ -184,7 +184,7 @@ namespace osu.Game.Overlays.Mods /// The new list of selected mods to select. public void UpdateSelectedButtons(IReadOnlyList newSelectedMods) { - foreach (var button in buttons) + foreach (var button in Buttons) updateButtonSelection(button, newSelectedMods); } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index eef91deb4c..26b8632d7f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -456,6 +456,7 @@ namespace osu.Game.Overlays.Mods } updateSelectedButtons(); + OnAvailableModsChanged(); } /// @@ -533,6 +534,13 @@ namespace osu.Game.Overlays.Mods private void playSelectedSound() => sampleOn?.Play(); private void playDeselectedSound() => sampleOff?.Play(); + /// + /// Invoked after has changed. + /// + protected virtual void OnAvailableModsChanged() + { + } + /// /// Invoked when a new has been selected. /// diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index ab7be13479..66262e7dc4 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -75,6 +75,14 @@ namespace osu.Game.Screens.OnlinePlay section.DeselectAll(); } + protected override void OnAvailableModsChanged() + { + base.OnAvailableModsChanged(); + + foreach (var section in ModSectionsContainer.Children) + ((FreeModSection)section).UpdateCheckboxState(); + } + protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); private class FreeModSection : ModSection @@ -108,10 +116,14 @@ namespace osu.Game.Screens.OnlinePlay protected override void ModButtonStateChanged(Mod mod) { base.ModButtonStateChanged(mod); + UpdateCheckboxState(); + } + public void UpdateCheckboxState() + { if (!SelectionAnimationRunning) { - var validButtons = ButtonsContainer.OfType().Where(b => b.Mod.HasImplementation); + var validButtons = Buttons.Where(b => b.Mod.HasImplementation); checkbox.Current.Value = validButtons.All(b => b.Selected); } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index f0c77b79bf..3f30ef1176 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -75,9 +75,18 @@ namespace osu.Game.Screens.OnlinePlay Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); } + private void onModsChanged(ValueChangedEvent> mods) + { + FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); + + // Reset the validity delegate to update the overlay's display. + freeModSelectOverlay.IsValidMod = IsValidFreeMod; + } + private void onRulesetChanged(ValueChangedEvent ruleset) { FreeMods.Value = Array.Empty(); @@ -155,6 +164,10 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a selectable free-mod. - protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod); + protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod); + + private bool checkCompatibleFreeMod(Mod mod) + => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods. } }