From 315c67a316343e8935c61e640aa502c418e887cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 19:59:30 +0200 Subject: [PATCH 01/15] Add failing test case for ruleset without all mod types --- .../UserInterface/TestSceneModSelectOverlay.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index fc543d9db7..0b037a10cd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Tests.Mods; using osuTK; using osuTK.Input; @@ -481,6 +482,21 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("3 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 3); } + [Test] + public void TestColumnHidingOnRulesetChange() + { + createScreen(); + + changeRuleset(0); + AddAssert("5 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 5); + + AddStep("change to ruleset without all mod types", () => Ruleset.Value = TestCustomisableModRuleset.CreateTestRulesetInfo()); + AddUntilStep("1 column visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 1); + + changeRuleset(0); + AddAssert("5 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 5); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); From 478cfc0b87ac3a6062e5948fe8df496df0fac991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 18:26:52 +0200 Subject: [PATCH 02/15] Split model class for mod state --- osu.Game/Overlays/Mods/ModState.cs | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 osu.Game/Overlays/Mods/ModState.cs diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs new file mode 100644 index 0000000000..8fdd5db00b --- /dev/null +++ b/osu.Game/Overlays/Mods/ModState.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Overlays.Mods +{ + /// + /// Wrapper class used to store the current state of a mod shown on the . + /// Used primarily to decouple data from drawable logic. + /// + public class ModState + { + /// + /// The mod that whose state this instance describes. + /// + public Mod Mod { get; } + + /// + /// Whether the mod is currently selected. + /// + public BindableBool Active { get; } = new BindableBool(); + + /// + /// Whether the mod is currently filtered out due to not matching imposed criteria. + /// + public BindableBool Filtered { get; } = new BindableBool(); + + public ModState(Mod mod) + { + Mod = mod; + } + } +} From 74599c9c625ae9902e16dfd80d2132d0fc0051e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 18:29:14 +0200 Subject: [PATCH 03/15] Use `ModState` in mod panels --- osu.Game/Overlays/Mods/ModPanel.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 4c4951307d..02b8c195ec 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -28,9 +28,11 @@ namespace osu.Game.Overlays.Mods { public class ModPanel : OsuClickableContainer { - public Mod Mod { get; } - public BindableBool Active { get; } = new BindableBool(); - public BindableBool Filtered { get; } = new BindableBool(); + public Mod Mod => modState.Mod; + public BindableBool Active => modState.Active; + public BindableBool Filtered => modState.Filtered; + + private readonly ModState modState; protected readonly Box Background; protected readonly Container SwitchContainer; @@ -57,7 +59,7 @@ namespace osu.Game.Overlays.Mods public ModPanel(Mod mod) { - Mod = mod; + modState = new ModState(mod); RelativeSizeAxes = Axes.X; Height = 42; From e86444c4bf433a34adce2a62f3f0eae9fc9a12ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 18:37:31 +0200 Subject: [PATCH 04/15] Hoist `ModState` to column level --- .../Mods/IncompatibilityDisplayingModPanel.cs | 5 +++++ osu.Game/Overlays/Mods/ModColumn.cs | 21 ++++++++++--------- osu.Game/Overlays/Mods/ModPanel.cs | 15 ++++++++----- .../Overlays/Mods/UserModSelectOverlay.cs | 2 +- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs index aeb983d352..34c4458a21 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs @@ -19,6 +19,11 @@ namespace osu.Game.Overlays.Mods [Resolved] private Bindable> selectedMods { get; set; } + public IncompatibilityDisplayingModPanel(ModState modState) + : base(modState) + { + } + public IncompatibilityDisplayingModPanel(Mod mod) : base(mod) { diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index b32ebb4a5c..c5364fb403 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -20,6 +20,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Lists; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -83,20 +84,20 @@ namespace osu.Game.Overlays.Mods protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; - protected virtual ModPanel CreateModPanel(Mod mod) => new ModPanel(mod); + protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod); private readonly Key[]? toggleKeys; private readonly Bindable>> availableMods = new Bindable>>(); /// - /// All mods that are available for the current ruleset in this particular column. + /// Contains information about state of all mods that are available for the current ruleset in this particular column. /// /// /// Note that the mod instances in this list are owned solely by this column /// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances). /// - private IReadOnlyList localAvailableMods = Array.Empty(); + private IReadOnlyList localAvailableMods = Array.Empty(); private readonly TextFlowContainer headerText; private readonly Box headerBackground; @@ -291,10 +292,10 @@ namespace osu.Game.Overlays.Mods private void updateLocalAvailableMods(bool asyncLoadContent) { var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()) - .Select(m => m.DeepClone()) + .Select(m => new ModState(m.DeepClone())) .ToList(); - if (newMods.SequenceEqual(localAvailableMods)) + if (newMods.SequenceEqual(localAvailableMods, new FuncEqualityComparer((x, y) => ReferenceEquals(x.Mod, y.Mod)))) return; localAvailableMods = newMods; @@ -393,18 +394,18 @@ namespace osu.Game.Overlays.Mods var newSelection = new List(); - foreach (var mod in localAvailableMods) + foreach (var modState in localAvailableMods) { - var matchingSelectedMod = mods.SingleOrDefault(selected => selected.GetType() == mod.GetType()); + var matchingSelectedMod = mods.SingleOrDefault(selected => selected.GetType() == modState.GetType()); if (matchingSelectedMod != null) { - mod.CopyFrom(matchingSelectedMod); - newSelection.Add(mod); + modState.Mod.CopyFrom(matchingSelectedMod); + newSelection.Add(modState.Mod); } else { - mod.ResetSettingsToDefaults(); + modState.Mod.ResetSettingsToDefaults(); } } diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 02b8c195ec..7010342bd8 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -57,9 +57,9 @@ namespace osu.Game.Overlays.Mods private Sample? sampleOff; private Sample? sampleOn; - public ModPanel(Mod mod) + public ModPanel(ModState modState) { - modState = new ModState(mod); + this.modState = modState; RelativeSizeAxes = Axes.X; Height = 42; @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Mods SwitchContainer = new Container { RelativeSizeAxes = Axes.Y, - Child = new ModSwitchSmall(mod) + Child = new ModSwitchSmall(Mod) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -117,7 +117,7 @@ namespace osu.Game.Overlays.Mods { new OsuSpriteText { - Text = mod.Name, + Text = Mod.Name, Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Margin = new MarginPadding @@ -127,7 +127,7 @@ namespace osu.Game.Overlays.Mods }, new OsuSpriteText { - Text = mod.Description, + Text = Mod.Description, Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, Truncate = true, @@ -143,6 +143,11 @@ namespace osu.Game.Overlays.Mods Action = Active.Toggle; } + public ModPanel(Mod mod) + : this(new ModState(mod)) + { + } + [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuColour colours, ISamplePlaybackDisabler? samplePlaybackDisabler) { diff --git a/osu.Game/Overlays/Mods/UserModSelectOverlay.cs b/osu.Game/Overlays/Mods/UserModSelectOverlay.cs index 8ff5e28c8f..7100446730 100644 --- a/osu.Game/Overlays/Mods/UserModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/UserModSelectOverlay.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Mods { } - protected override ModPanel CreateModPanel(Mod mod) => new IncompatibilityDisplayingModPanel(mod); + protected override ModPanel CreateModPanel(ModState modState) => new IncompatibilityDisplayingModPanel(modState); } } } From 05a21fbbe0563605fe002dfe49ff1bb7a87632da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 19:02:45 +0200 Subject: [PATCH 05/15] Hoist `ModState` to overlay level --- osu.Game/Overlays/Mods/ModColumn.cs | 69 ++++++++-------------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 47 +++++++++++---- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index c5364fb403..8b3896a88c 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -20,13 +20,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; -using osu.Framework.Lists; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; -using osu.Game.Utils; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -39,6 +37,23 @@ namespace osu.Game.Overlays.Mods public readonly ModType ModType; + private IReadOnlyList availableMods = Array.Empty(); + + /// + /// Sets the list of mods to show in this column. + /// + public IReadOnlyList AvailableMods + { + get => availableMods; + set + { + Debug.Assert(value.All(mod => mod.Mod.Type == ModType)); + + availableMods = value; + asyncLoadPanels(); + } + } + private Func? filter; /// @@ -88,17 +103,6 @@ namespace osu.Game.Overlays.Mods private readonly Key[]? toggleKeys; - private readonly Bindable>> availableMods = new Bindable>>(); - - /// - /// Contains information about state of all mods that are available for the current ruleset in this particular column. - /// - /// - /// Note that the mod instances in this list are owned solely by this column - /// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances). - /// - private IReadOnlyList localAvailableMods = Array.Empty(); - private readonly TextFlowContainer headerText; private readonly Box headerBackground; private readonly Container contentContainer; @@ -258,12 +262,8 @@ namespace osu.Game.Overlays.Mods } [BackgroundDependencyLoader] - private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { - availableMods.BindTo(game.AvailableMods); - updateLocalAvailableMods(asyncLoadContent: false); - availableMods.BindValueChanged(_ => updateLocalAvailableMods(asyncLoadContent: true)); - headerBackground.Colour = accentColour = colours.ForModType(ModType); if (toggleAllCheckbox != null) @@ -289,30 +289,13 @@ namespace osu.Game.Overlays.Mods toggleAllCheckbox.LabelText = toggleAllCheckbox.Current.Value ? CommonStrings.DeselectAll : CommonStrings.SelectAll; } - private void updateLocalAvailableMods(bool asyncLoadContent) - { - var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()) - .Select(m => new ModState(m.DeepClone())) - .ToList(); - - if (newMods.SequenceEqual(localAvailableMods, new FuncEqualityComparer((x, y) => ReferenceEquals(x.Mod, y.Mod)))) - return; - - localAvailableMods = newMods; - - if (asyncLoadContent) - asyncLoadPanels(); - else - onPanelsLoaded(createPanels()); - } - private CancellationTokenSource? cancellationTokenSource; private void asyncLoadPanels() { cancellationTokenSource?.Cancel(); - var panels = createPanels(); + var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0))); Task? loadTask; @@ -324,12 +307,6 @@ namespace osu.Game.Overlays.Mods }); } - private IEnumerable createPanels() - { - var panels = localAvailableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0))); - return panels; - } - private void onPanelsLoaded(IEnumerable loaded) { panelFlow.ChildrenEnumerable = loaded; @@ -394,7 +371,7 @@ namespace osu.Game.Overlays.Mods var newSelection = new List(); - foreach (var modState in localAvailableMods) + foreach (var modState in this.availableMods) { var matchingSelectedMod = mods.SingleOrDefault(selected => selected.GetType() == modState.GetType()); @@ -554,10 +531,10 @@ namespace osu.Game.Overlays.Mods int index = Array.IndexOf(toggleKeys, e.Key); if (index < 0) return false; - var panel = panelFlow.ElementAtOrDefault(index); - if (panel == null || panel.Filtered.Value) return false; + var modState = availableMods.ElementAtOrDefault(index); + if (modState == null || modState.Filtered.Value) return false; - panel.Active.Toggle(); + modState.Active.Toggle(); return true; } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b3c3eee15a..ace0576b96 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -22,6 +22,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -47,9 +48,7 @@ namespace osu.Game.Overlays.Mods set { isValidMod = value ?? throw new ArgumentNullException(nameof(value)); - - if (IsLoaded) - updateAvailableMods(); + filterMods(); } } @@ -64,6 +63,9 @@ namespace osu.Game.Overlays.Mods protected virtual IEnumerable CreateFooterButtons() => createDefaultFooterButtons(); + private readonly Bindable>> availableMods = new Bindable>>(); + private readonly Dictionary> localAvailableMods = new Dictionary>(); + private readonly BindableBool customisationVisible = new BindableBool(); private ModSettingsArea modSettingsArea = null!; @@ -82,7 +84,7 @@ namespace osu.Game.Overlays.Mods } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuGameBase game, OsuColour colours) { Header.Title = ModSelectOverlayStrings.ModSelectTitle; Header.Description = ModSelectOverlayStrings.ModSelectDescription; @@ -184,12 +186,16 @@ namespace osu.Game.Overlays.Mods LighterColour = colours.Pink1 }) }; + + availableMods.BindTo(game.AvailableMods); } protected override void LoadComplete() { base.LoadComplete(); + availableMods.BindValueChanged(_ => createLocalMods(), true); + State.BindValueChanged(_ => samplePlaybackDisabled.Value = State.Value == Visibility.Hidden, true); // This is an optimisation to prevent refreshing the available settings controls when it can be @@ -211,8 +217,6 @@ namespace osu.Game.Overlays.Mods customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); - updateAvailableMods(); - // Start scrolled slightly to the right to give the user a sense that // there is more horizontal content available. ScheduleAfterChildren(() => @@ -272,6 +276,31 @@ namespace osu.Game.Overlays.Mods } }; + private void createLocalMods() + { + localAvailableMods.Clear(); + + foreach (var (modType, mods) in availableMods.Value) + { + var modStates = mods.SelectMany(ModUtils.FlattenMod) + .Select(mod => new ModState(mod.DeepClone())) + .ToArray(); + + localAvailableMods[modType] = modStates; + } + + filterMods(); + + foreach (var column in columnFlow.Columns) + column.AvailableMods = localAvailableMods.GetValueOrDefault(column.ModType, Array.Empty()); + } + + private void filterMods() + { + foreach (var modState in localAvailableMods.Values.SelectMany(m => m)) + modState.Filtered.Value = !modState.Mod.HasImplementation || !IsValidMod.Invoke(modState.Mod); + } + private void updateMultiplier() { if (multiplierDisplay == null) @@ -285,12 +314,6 @@ namespace osu.Game.Overlays.Mods multiplierDisplay.Current.Value = multiplier; } - private void updateAvailableMods() - { - foreach (var column in columnFlow.Columns) - column.Filter = m => m.HasImplementation && isValidMod.Invoke(m); - } - private void updateCustomisation(ValueChangedEvent> valueChangedEvent) { if (customisationButton == null) From 11ae1da65a092266786f4eae988bd0713584dbc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 19:30:06 +0200 Subject: [PATCH 06/15] Hoist reference replacement logic to overlay level --- osu.Game/Overlays/Mods/ModColumn.cs | 116 ++------------------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 85 ++++++++++----- 2 files changed, 64 insertions(+), 137 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 8b3896a88c..deaaacd775 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -13,7 +13,6 @@ using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -54,49 +53,18 @@ namespace osu.Game.Overlays.Mods } } - private Func? filter; - /// /// A function determining whether each mod in the column should be displayed. /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden. /// - public Func? Filter - { - get => filter; - set - { - filter = value; - updateState(); - } - } + public Func? Filter { get; set; } // TODO: remove later /// /// Determines whether this column should accept user input. /// public Bindable Active = new BindableBool(true); - private readonly Bindable allFiltered = new BindableBool(); - - /// - /// True if all of the panels in this column have been filtered out by the current . - /// - public IBindable AllFiltered => allFiltered; - - /// - /// List of mods marked as selected in this column. - /// - /// - /// Note that the mod instances returned by this property are owned solely by this column - /// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances). - /// - public IReadOnlyList SelectedMods { get; private set; } = Array.Empty(); - - /// - /// Invoked when a mod panel has been selected interactively by the user. - /// - public event Action? SelectionChangedByUser; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod); @@ -299,7 +267,11 @@ namespace osu.Game.Overlays.Mods Task? loadTask; - latestLoadTask = loadTask = LoadComponentsAsync(panels, onPanelsLoaded, (cancellationTokenSource = new CancellationTokenSource()).Token); + latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded => + { + panelFlow.ChildrenEnumerable = loaded; + updateState(); + }, (cancellationTokenSource = new CancellationTokenSource()).Token); loadTask.ContinueWith(_ => { if (loadTask == latestLoadTask) @@ -307,27 +279,9 @@ namespace osu.Game.Overlays.Mods }); } - private void onPanelsLoaded(IEnumerable loaded) - { - panelFlow.ChildrenEnumerable = loaded; - - updateState(); - - foreach (var panel in panelFlow) - { - panel.Active.BindValueChanged(_ => panelStateChanged(panel)); - } - } - private void updateState() { - foreach (var panel in panelFlow) - { - panel.Active.Value = SelectedMods.Contains(panel.Mod); - panel.ApplyFilter(Filter); - } - - allFiltered.Value = panelFlow.All(panel => panel.Filtered.Value); + Alpha = availableMods.All(mod => mod.Filtered.Value) ? 0 : 1; if (toggleAllCheckbox != null && !SelectionAnimationRunning) { @@ -336,62 +290,6 @@ namespace osu.Game.Overlays.Mods } } - /// - /// This flag helps to determine the source of changes to . - /// If the value is false, then are changing due to a user selection on the UI. - /// If the value is true, then are changing due to an external call. - /// - private bool externalSelectionUpdateInProgress; - - private void panelStateChanged(ModPanel panel) - { - if (externalSelectionUpdateInProgress) - return; - - var newSelectedMods = panel.Active.Value - ? SelectedMods.Append(panel.Mod) - : SelectedMods.Except(panel.Mod.Yield()); - - SelectedMods = newSelectedMods.ToArray(); - updateState(); - SelectionChangedByUser?.Invoke(); - } - - /// - /// Adjusts the set of selected mods in this column to match the passed in . - /// - /// - /// This method exists to be able to receive mod instances that come from potentially-external sources and to copy the changes across to this column's state. - /// uses this to substitute any external mod references in - /// to references that are owned by this column. - /// - internal void SetSelection(IReadOnlyList mods) - { - externalSelectionUpdateInProgress = true; - - var newSelection = new List(); - - foreach (var modState in this.availableMods) - { - var matchingSelectedMod = mods.SingleOrDefault(selected => selected.GetType() == modState.GetType()); - - if (matchingSelectedMod != null) - { - modState.Mod.CopyFrom(matchingSelectedMod); - newSelection.Add(modState.Mod); - } - else - { - modState.Mod.ResetSettingsToDefaults(); - } - } - - SelectedMods = newSelection; - updateState(); - - externalSelectionUpdateInProgress = false; - } - #region Bulk select / deselect private const double initial_multiple_selection_delay = 120; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ace0576b96..567e91e3fa 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -207,14 +207,9 @@ namespace osu.Game.Overlays.Mods { updateMultiplier(); updateCustomisation(val); - updateSelectionFromBindable(); + updateFromExternalSelection(); }, true); - foreach (var column in columnFlow.Columns) - { - column.SelectionChangedByUser += updateBindableFromSelection; - } - customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); // Start scrolled slightly to the right to give the user a sense that @@ -248,7 +243,6 @@ namespace osu.Game.Overlays.Mods { var column = CreateModColumn(modType, toggleKeys).With(column => { - column.Filter = IsValidMod; // spacing applied here rather than via `columnFlow.Spacing` to avoid uneven gaps when some of the columns are hidden. column.Margin = new MarginPadding { Right = 10 }; }); @@ -286,6 +280,9 @@ namespace osu.Game.Overlays.Mods .Select(mod => new ModState(mod.DeepClone())) .ToArray(); + foreach (var modState in modStates) + modState.Active.BindValueChanged(_ => updateFromInternalSelection()); + localAvailableMods[modType] = modStates; } @@ -362,20 +359,50 @@ namespace osu.Game.Overlays.Mods TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic); } - private void updateSelectionFromBindable() - { - // `SelectedMods` may contain mod references that come from external sources. - // to ensure isolation, first pull in the potentially-external change into the mod columns... - foreach (var column in columnFlow.Columns) - column.SetSelection(SelectedMods.Value); + /// + /// This flag helps to determine the source of changes to . + /// If the value is false, then are changing due to a user selection on the UI. + /// If the value is true, then are changing due to an external change. + /// + private bool externalSelectionUpdateInProgress; - // and then, when done, replace the potentially-external mod references in `SelectedMods` with ones we own. - updateBindableFromSelection(); + private void updateFromExternalSelection() + { + externalSelectionUpdateInProgress = true; + + var newSelection = new List(); + + foreach (var modState in localAvailableMods.SelectMany(pair => pair.Value)) + { + var matchingSelectedMod = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == modState.Mod.GetType()); + + if (matchingSelectedMod != null) + { + modState.Mod.CopyFrom(matchingSelectedMod); + modState.Active.Value = true; + newSelection.Add(modState.Mod); + } + else + { + modState.Mod.ResetSettingsToDefaults(); + modState.Active.Value = false; + } + } + + SelectedMods.Value = newSelection; + + externalSelectionUpdateInProgress = false; } - private void updateBindableFromSelection() + private void updateFromInternalSelection() { - var candidateSelection = columnFlow.Columns.SelectMany(column => column.SelectedMods).ToArray(); + if (externalSelectionUpdateInProgress) + return; + + var candidateSelection = localAvailableMods.SelectMany(pair => pair.Value) + .Where(modState => modState.Active.Value) + .Select(modState => modState.Mod) + .ToArray(); // the following guard intends to check cases where we've already replaced potentially-external mod references with our own and avoid endless recursion. // TODO: replace custom comparer with System.Collections.Generic.ReferenceEqualityComparer when fully on .NET 6 @@ -406,10 +433,12 @@ namespace osu.Game.Overlays.Mods { var column = columnFlow[i].Column; - double delay = column.AllFiltered.Value ? 0 : nonFilteredColumnCount * 30; - double duration = column.AllFiltered.Value ? 0 : fade_in_duration; + bool allFiltered = column.AvailableMods.All(modState => modState.Filtered.Value); + + double delay = allFiltered ? 0 : nonFilteredColumnCount * 30; + double duration = allFiltered ? 0 : fade_in_duration; float startingYPosition = 0; - if (!column.AllFiltered.Value) + if (!allFiltered) startingYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance; column.TopLevelContent @@ -418,7 +447,7 @@ namespace osu.Game.Overlays.Mods .MoveToY(0, duration, Easing.OutQuint) .FadeIn(duration, Easing.OutQuint); - if (!column.AllFiltered.Value) + if (!allFiltered) nonFilteredColumnCount += 1; } } @@ -439,9 +468,11 @@ namespace osu.Game.Overlays.Mods { var column = columnFlow[i].Column; - double duration = column.AllFiltered.Value ? 0 : fade_out_duration; + bool allFiltered = column.AvailableMods.All(modState => modState.Filtered.Value); + + double duration = allFiltered ? 0 : fade_out_duration; float newYPosition = 0; - if (!column.AllFiltered.Value) + if (!allFiltered) newYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance; column.FlushPendingSelections(); @@ -449,7 +480,7 @@ namespace osu.Game.Overlays.Mods .MoveToY(newYPosition, duration, Easing.OutQuint) .FadeOut(duration, Easing.OutQuint); - if (!column.AllFiltered.Value) + if (!allFiltered) nonFilteredColumnCount += 1; } } @@ -593,8 +624,8 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { base.LoadComplete(); - Active.BindValueChanged(_ => updateState()); - Column.AllFiltered.BindValueChanged(_ => updateState(), true); + + Active.BindValueChanged(_ => updateState(), true); FinishTransforms(); } @@ -604,8 +635,6 @@ namespace osu.Game.Overlays.Mods { Colour4 targetColour; - Column.Alpha = Column.AllFiltered.Value ? 0 : 1; - if (Column.Active.Value) targetColour = Colour4.White; else From 83ba06e7afc42d59273515a1a8b277daeca64af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 19:33:52 +0200 Subject: [PATCH 07/15] Extract helper property for accessing all mods --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 567e91e3fa..7e28ab4b1f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -65,6 +65,7 @@ namespace osu.Game.Overlays.Mods private readonly Bindable>> availableMods = new Bindable>>(); private readonly Dictionary> localAvailableMods = new Dictionary>(); + private IEnumerable allLocalAvailableMods => localAvailableMods.SelectMany(pair => pair.Value); private readonly BindableBool customisationVisible = new BindableBool(); @@ -294,7 +295,7 @@ namespace osu.Game.Overlays.Mods private void filterMods() { - foreach (var modState in localAvailableMods.Values.SelectMany(m => m)) + foreach (var modState in allLocalAvailableMods) modState.Filtered.Value = !modState.Mod.HasImplementation || !IsValidMod.Invoke(modState.Mod); } @@ -372,7 +373,7 @@ namespace osu.Game.Overlays.Mods var newSelection = new List(); - foreach (var modState in localAvailableMods.SelectMany(pair => pair.Value)) + foreach (var modState in allLocalAvailableMods) { var matchingSelectedMod = SelectedMods.Value.SingleOrDefault(selected => selected.GetType() == modState.Mod.GetType()); @@ -399,10 +400,9 @@ namespace osu.Game.Overlays.Mods if (externalSelectionUpdateInProgress) return; - var candidateSelection = localAvailableMods.SelectMany(pair => pair.Value) - .Where(modState => modState.Active.Value) - .Select(modState => modState.Mod) - .ToArray(); + var candidateSelection = allLocalAvailableMods.Where(modState => modState.Active.Value) + .Select(modState => modState.Mod) + .ToArray(); // the following guard intends to check cases where we've already replaced potentially-external mod references with our own and avoid endless recursion. // TODO: replace custom comparer with System.Collections.Generic.ReferenceEqualityComparer when fully on .NET 6 From fc24a564782aa31381b8eada9e547f871f5c0978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 19:40:56 +0200 Subject: [PATCH 08/15] Add protection from recursive updates from external selection --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 7e28ab4b1f..e478b2afcd 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -369,6 +369,9 @@ namespace osu.Game.Overlays.Mods private void updateFromExternalSelection() { + if (externalSelectionUpdateInProgress) + return; + externalSelectionUpdateInProgress = true; var newSelection = new List(); From 7ac6073f13cf31bf6e15489c9f54f31ed87503c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 20:23:24 +0200 Subject: [PATCH 09/15] Fix column test scene to work --- .../UserInterface/TestSceneModColumn.cs | 59 +++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index e47ae860c6..331509e10f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Linq; using NUnit.Framework; @@ -12,11 +14,9 @@ using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; -using osu.Game.Rulesets.Catch; -using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Taiko; +using osu.Game.Utils; using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface @@ -41,20 +41,16 @@ namespace osu.Game.Tests.Visual.UserInterface Child = new ModColumn(modType, false) { Anchor = Anchor.Centre, - Origin = Anchor.Centre + Origin = Anchor.Centre, + AvailableMods = getExampleModsFor(modType) } }); - - AddStep("change ruleset to osu!", () => Ruleset.Value = new OsuRuleset().RulesetInfo); - AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); - AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo); - AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); } [Test] public void TestMultiSelection() { - ModColumn column = null; + ModColumn column = null!; AddStep("create content", () => Child = new Container { RelativeSizeAxes = Axes.Both, @@ -62,7 +58,8 @@ namespace osu.Game.Tests.Visual.UserInterface Child = column = new ModColumn(ModType.DifficultyIncrease, true) { Anchor = Anchor.Centre, - Origin = Anchor.Centre + Origin = Anchor.Centre, + AvailableMods = getExampleModsFor(ModType.DifficultyIncrease) } }); @@ -91,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestFiltering() { - TestModColumn column = null; + TestModColumn column = null!; AddStep("create content", () => Child = new Container { @@ -100,30 +97,31 @@ namespace osu.Game.Tests.Visual.UserInterface Child = column = new TestModColumn(ModType.Fun, true) { Anchor = Anchor.Centre, - Origin = Anchor.Centre + Origin = Anchor.Centre, + AvailableMods = getExampleModsFor(ModType.Fun) } }); - AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)); + AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); clickToggle(); AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value)); - AddStep("unset filter", () => column.Filter = null); + AddStep("unset filter", () => setFilter(null)); AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); AddAssert("checkbox not selected", () => !column.ChildrenOfType().Single().Current.Value); - AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)); + AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value); - AddStep("filter out everything", () => column.Filter = _ => false); + AddStep("filter out everything", () => setFilter(_ => false)); AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => panel.Filtered.Value)); AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent); - AddStep("inset filter", () => column.Filter = null); + AddStep("inset filter", () => setFilter(null)); AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); AddUntilStep("checkbox visible", () => column.ChildrenOfType().Single().IsPresent); @@ -138,7 +136,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestKeyboardSelection() { - ModColumn column = null; + ModColumn column = null!; AddStep("create content", () => Child = new Container { RelativeSizeAxes = Axes.Both, @@ -146,7 +144,8 @@ namespace osu.Game.Tests.Visual.UserInterface Child = column = new ModColumn(ModType.DifficultyReduction, true, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }) { Anchor = Anchor.Centre, - Origin = Anchor.Centre + Origin = Anchor.Centre, + AvailableMods = getExampleModsFor(ModType.DifficultyReduction) } }); @@ -158,7 +157,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("press W again", () => InputManager.Key(Key.W)); AddAssert("NF panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); - AddStep("set filter to NF", () => column.Filter = mod => mod.Acronym == "NF"); + AddStep("set filter to NF", () => setFilter(mod => mod.Acronym == "NF")); AddStep("press W", () => InputManager.Key(Key.W)); AddAssert("NF panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); @@ -166,12 +165,18 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("press W again", () => InputManager.Key(Key.W)); AddAssert("NF panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); - AddStep("filter out everything", () => column.Filter = _ => false); + AddStep("filter out everything", () => setFilter(_ => false)); AddStep("press W", () => InputManager.Key(Key.W)); AddAssert("NF panel not selected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); - AddStep("clear filter", () => column.Filter = null); + AddStep("clear filter", () => setFilter(null)); + } + + private void setFilter(Func? filter) + { + foreach (var modState in this.ChildrenOfType().Single().AvailableMods) + modState.Filtered.Value = filter?.Invoke(modState.Mod) == false; } private class TestModColumn : ModColumn @@ -183,5 +188,13 @@ namespace osu.Game.Tests.Visual.UserInterface { } } + + private static ModState[] getExampleModsFor(ModType modType) + { + return new OsuRuleset().GetModsFor(modType) + .SelectMany(ModUtils.FlattenMod) + .Select(mod => new ModState(mod)) + .ToArray(); + } } } From 52bbce12f16d3ba769c8da6fa6ee6a51a8a65d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 20:23:56 +0200 Subject: [PATCH 10/15] Fix not being able to set `AvailableMods` before loaded --- osu.Game/Overlays/Mods/ModColumn.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index deaaacd775..06e96afdca 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -49,7 +49,9 @@ namespace osu.Game.Overlays.Mods Debug.Assert(value.All(mod => mod.Mod.Type == ModType)); availableMods = value; - asyncLoadPanels(); + + if (IsLoaded) + asyncLoadPanels(); } } @@ -249,6 +251,7 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); toggleAllCheckbox?.Current.BindValueChanged(_ => updateToggleAllText(), true); + asyncLoadPanels(); } private void updateToggleAllText() From b5a9f1310a706491c6019b2a2d159583620f4972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 20:28:11 +0200 Subject: [PATCH 11/15] Fix select/deselect all toggle not working correctly after changes --- osu.Game/Overlays/Mods/ModColumn.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 06e96afdca..c1904a0bc0 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -50,6 +50,14 @@ namespace osu.Game.Overlays.Mods availableMods = value; + foreach (var mod in availableMods) + { + mod.Active.BindValueChanged(_ => updateState()); + mod.Filtered.BindValueChanged(_ => updateState()); + } + + updateState(); + if (IsLoaded) asyncLoadPanels(); } From 1c0166367d1f12cfaf5194f467d7c64220e1409b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 20:29:28 +0200 Subject: [PATCH 12/15] Fix remaining column operations being coupled to drawables --- osu.Game/Overlays/Mods/ModColumn.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index c1904a0bc0..d7ce08e124 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -296,8 +296,8 @@ namespace osu.Game.Overlays.Mods if (toggleAllCheckbox != null && !SelectionAnimationRunning) { - toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0; - toggleAllCheckbox.Current.Value = panelFlow.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); + toggleAllCheckbox.Alpha = availableMods.Any(panel => !panel.Filtered.Value) ? 1 : 0; + toggleAllCheckbox.Current.Value = availableMods.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); } } @@ -342,7 +342,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in panelFlow.Where(b => !b.Active.Value && !b.Filtered.Value)) + foreach (var button in availableMods.Where(b => !b.Active.Value && !b.Filtered.Value)) pendingSelectionOperations.Enqueue(() => button.Active.Value = true); } @@ -353,7 +353,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in panelFlow.Where(b => b.Active.Value && !b.Filtered.Value)) + foreach (var button in availableMods.Where(b => b.Active.Value && !b.Filtered.Value)) pendingSelectionOperations.Enqueue(() => button.Active.Value = false); } From 2266a5c9a0588beb5e188197a0dd03c67457dd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 20:38:53 +0200 Subject: [PATCH 13/15] Remove no-longer-necessary `ModColumn.Filter` --- osu.Game/Overlays/Mods/ModColumn.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index d7ce08e124..9bb3f8bd9e 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -63,13 +63,6 @@ namespace osu.Game.Overlays.Mods } } - /// - /// A function determining whether each mod in the column should be displayed. - /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. - /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden. - /// - public Func? Filter { get; set; } // TODO: remove later - /// /// Determines whether this column should accept user input. /// From 93539160adb48a6be40d69ed33c216d46f52327e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 21:57:57 +0200 Subject: [PATCH 14/15] Remove no-longer-necessary guard --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index e478b2afcd..d94f663962 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -12,7 +12,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Framework.Lists; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; @@ -407,11 +406,6 @@ namespace osu.Game.Overlays.Mods .Select(modState => modState.Mod) .ToArray(); - // the following guard intends to check cases where we've already replaced potentially-external mod references with our own and avoid endless recursion. - // TODO: replace custom comparer with System.Collections.Generic.ReferenceEqualityComparer when fully on .NET 6 - if (candidateSelection.SequenceEqual(SelectedMods.Value, new FuncEqualityComparer(ReferenceEquals))) - return; - SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection); } From 981ead68bf8a54ece314e00bf0d68a16aa331622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 May 2022 22:20:00 +0200 Subject: [PATCH 15/15] Ensure local mods are constructed in time for `Pop{In,Out}()` --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d94f663962..d068839ab0 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -192,10 +192,11 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { - base.LoadComplete(); - + // this is called before base call so that the mod state is populated early, and the transition in `PopIn()` can play out properly. availableMods.BindValueChanged(_ => createLocalMods(), true); + base.LoadComplete(); + State.BindValueChanged(_ => samplePlaybackDisabled.Value = State.Value == Visibility.Hidden, true); // This is an optimisation to prevent refreshing the available settings controls when it can be