diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index e4ded602aa..7dcaabca3d 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using Moq; using NUnit.Framework; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Utils; namespace osu.Game.Tests.Mods @@ -99,6 +102,53 @@ namespace osu.Game.Tests.Mods Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); } + 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[] + { + new Mod[] { new OsuModDoubleTime(), new OsuModTouchDevice() }, + new[] { typeof(OsuModTouchDevice) } + }, + // multi mod. + new object[] + { + new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModHalfTime() }, + new[] { typeof(MultiMod) } + }, + // valid pair. + new object[] + { + new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() }, + null + } + }; + + [TestCaseSource(nameof(invalid_mod_test_scenarios))] + public void TestInvalidModScenarios(Mod[] inputMods, Type[] expectedInvalid) + { + bool isValid = ModUtils.CheckValidForGameplay(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 { } diff --git a/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs b/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs index eef9582af9..10216c3339 100644 --- a/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs +++ b/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs @@ -3,8 +3,12 @@ using System; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Screens; using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual; @@ -58,5 +62,45 @@ namespace osu.Game.Tests.NonVisual AddStep("end operation", () => operation.Dispose()); AddAssert("operation is ended", () => !operationInProgress.Value); } + + [Test] + public void TestOperationDisposalAfterScreenExit() + { + TestScreenWithTracker screen = null; + OsuScreenStack stack; + IDisposable operation = null; + + AddStep("create screen with tracker", () => + { + Child = stack = new OsuScreenStack + { + RelativeSizeAxes = Axes.Both + }; + + stack.Push(screen = new TestScreenWithTracker()); + }); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + + AddStep("begin operation", () => operation = screen.OngoingOperationTracker.BeginOperation()); + AddAssert("operation in progress", () => screen.OngoingOperationTracker.InProgress.Value); + + AddStep("dispose after screen exit", () => + { + screen.Exit(); + operation.Dispose(); + }); + AddAssert("operation ended", () => !screen.OngoingOperationTracker.InProgress.Value); + } + + private class TestScreenWithTracker : OsuScreen + { + public OngoingOperationTracker OngoingOperationTracker { get; private set; } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = OngoingOperationTracker = new OngoingOperationTracker(); + } + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 874c1694eb..960aad10c6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -11,8 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -20,6 +20,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Beatmaps; +using osu.Game.Users; using osuTK; using osuTK.Input; @@ -241,7 +242,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } private void moveToItem(int index, Vector2? offset = null) - => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType>().ElementAt(index), offset)); + => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); private void moveToDragger(int index, Vector2? offset = null) => AddStep($"move mouse to dragger {index}", () => { @@ -252,7 +253,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () => { var item = playlist.ChildrenOfType>().ElementAt(index); - InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset); + InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset); }); private void assertHandleVisibility(int index, bool visible) @@ -260,7 +261,7 @@ namespace osu.Game.Tests.Visual.Multiplayer () => (playlist.ChildrenOfType.PlaylistItemHandle>().ElementAt(index).Alpha > 0) == visible); private void assertDeleteButtonVisibility(int index, bool visible) - => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible); + => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible); private void createPlaylist(bool allowEdit, bool allowSelection) { @@ -278,7 +279,21 @@ namespace osu.Game.Tests.Visual.Multiplayer playlist.Items.Add(new PlaylistItem { ID = i, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Beatmap = + { + Value = i % 2 == 1 + ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo + : new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "Artist", + Author = new User { Username = "Creator name here" }, + Title = "Long title used to check background colour", + }, + BeatmapSet = new BeatmapSetInfo() + } + }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, RequiredMods = { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs new file mode 100644 index 0000000000..e7fa7d9235 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneModIcon : OsuTestScene + { + [Test] + public void TestChangeModType() + { + ModIcon icon = null; + + AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime())); + AddStep("change mod", () => icon.Mod = new OsuModEasy()); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/DownloadButton.cs b/osu.Game/Graphics/UserInterface/DownloadButton.cs index 5168ff646b..7a8db158c1 100644 --- a/osu.Game/Graphics/UserInterface/DownloadButton.cs +++ b/osu.Game/Graphics/UserInterface/DownloadButton.cs @@ -4,54 +4,38 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Online; using osuTK; namespace osu.Game.Graphics.UserInterface { - public class DownloadButton : OsuAnimatedButton + public class DownloadButton : GrayButton { - public readonly Bindable State = new Bindable(); - - private readonly SpriteIcon icon; - private readonly SpriteIcon checkmark; - private readonly Box background; - [Resolved] private OsuColour colours { get; set; } + public readonly Bindable State = new Bindable(); + + private SpriteIcon checkmark; + public DownloadButton() + : base(FontAwesome.Solid.Download) { - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue - }, - icon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(13), - Icon = FontAwesome.Solid.Download, - }, - checkmark = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - X = 8, - Size = Vector2.Zero, - Icon = FontAwesome.Solid.Check, - } - }; } [BackgroundDependencyLoader] private void load() { + AddInternal(checkmark = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + X = 8, + Size = Vector2.Zero, + Icon = FontAwesome.Solid.Check, + }); + State.BindValueChanged(updateState, true); } @@ -60,27 +44,27 @@ namespace osu.Game.Graphics.UserInterface switch (state.NewValue) { case DownloadState.NotDownloaded: - background.FadeColour(colours.Gray4, 500, Easing.InOutExpo); - icon.MoveToX(0, 500, Easing.InOutExpo); + Background.FadeColour(colours.Gray4, 500, Easing.InOutExpo); + Icon.MoveToX(0, 500, Easing.InOutExpo); checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo); TooltipText = "Download"; break; case DownloadState.Downloading: - background.FadeColour(colours.Blue, 500, Easing.InOutExpo); - icon.MoveToX(0, 500, Easing.InOutExpo); + Background.FadeColour(colours.Blue, 500, Easing.InOutExpo); + Icon.MoveToX(0, 500, Easing.InOutExpo); checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo); TooltipText = "Downloading..."; break; case DownloadState.Importing: - background.FadeColour(colours.Yellow, 500, Easing.InOutExpo); + Background.FadeColour(colours.Yellow, 500, Easing.InOutExpo); TooltipText = "Importing"; break; case DownloadState.LocallyAvailable: - background.FadeColour(colours.Green, 500, Easing.InOutExpo); - icon.MoveToX(-8, 500, Easing.InOutExpo); + Background.FadeColour(colours.Green, 500, Easing.InOutExpo); + Icon.MoveToX(-8, 500, Easing.InOutExpo); checkmark.ScaleTo(new Vector2(13), 500, Easing.InOutExpo); break; } diff --git a/osu.Game/Graphics/UserInterface/GrayButton.cs b/osu.Game/Graphics/UserInterface/GrayButton.cs new file mode 100644 index 0000000000..88c46f29e0 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/GrayButton.cs @@ -0,0 +1,48 @@ +// 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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + public class GrayButton : OsuAnimatedButton + { + protected SpriteIcon Icon { get; private set; } + protected Box Background { get; private set; } + + private readonly IconUsage icon; + + [Resolved] + private OsuColour colours { get; set; } + + public GrayButton(IconUsage icon) + { + this.icon = icon; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + Background = new Box + { + Colour = colours.Gray4, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }, + Icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(13), + Icon = icon, + }, + }; + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5acd6bc73d..a00cd5e6a0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -468,6 +468,12 @@ namespace osu.Game private void modsChanged(ValueChangedEvent> mods) { updateModDefaults(); + + if (!ModUtils.CheckValidForGameplay(mods.NewValue, out var invalid)) + { + // ensure we always have a valid set of mods. + SelectedMods.Value = mods.NewValue.Except(invalid).ToArray(); + } } private void updateModDefaults() diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 4c629aef54..b3ddd30772 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -11,26 +11,23 @@ using System; using System.Linq; using System.Collections.Generic; using System.Threading; +using Humanizer; using osu.Framework.Input.Events; using osu.Game.Graphics; namespace osu.Game.Overlays.Mods { - public abstract class ModSection : Container + public class ModSection : CompositeDrawable { - private readonly OsuSpriteText headerLabel; + private readonly Drawable header; public FillFlowContainer ButtonsContainer { get; } public Action Action; - protected abstract Key[] ToggleKeys { get; } - public abstract ModType ModType { get; } - public string Header - { - get => headerLabel.Text; - set => headerLabel.Text = value; - } + public Key[] ToggleKeys; + + public readonly ModType ModType; public IEnumerable SelectedMods => buttons.Select(b => b.SelectedMod).Where(m => m != null); @@ -61,7 +58,7 @@ namespace osu.Game.Overlays.Mods if (modContainers.Length == 0) { ModIconsLoaded = true; - headerLabel.Hide(); + header.Hide(); Hide(); return; } @@ -76,7 +73,7 @@ namespace osu.Game.Overlays.Mods buttons = modContainers.OfType().ToArray(); - headerLabel.FadeIn(200); + header.FadeIn(200); this.FadeIn(200); } } @@ -153,23 +150,19 @@ namespace osu.Game.Overlays.Mods button.Deselect(); } - protected ModSection() + public ModSection(ModType type) { + ModType = type; + AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; Origin = Anchor.TopCentre; Anchor = Anchor.TopCentre; - Children = new Drawable[] + InternalChildren = new[] { - headerLabel = new OsuSpriteText - { - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - Position = new Vector2(0f, 0f), - Font = OsuFont.GetFont(weight: FontWeight.Bold) - }, + header = CreateHeader(type.Humanize(LetterCasing.Title)), ButtonsContainer = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -185,5 +178,11 @@ namespace osu.Game.Overlays.Mods }, }; } + + protected virtual Drawable CreateHeader(string text) => new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Text = text + }; } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index c400e4cc43..e064a6fb84 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -20,7 +20,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; -using osu.Game.Overlays.Mods.Sections; using osu.Game.Rulesets.Mods; using osu.Game.Screens; using osu.Game.Utils; @@ -211,13 +210,31 @@ namespace osu.Game.Overlays.Mods Width = content_width, LayoutDuration = 200, LayoutEasing = Easing.OutQuint, - Children = new ModSection[] + Children = new[] { - new DifficultyReductionSection { Action = modButtonPressed }, - new DifficultyIncreaseSection { Action = modButtonPressed }, - new AutomationSection { Action = modButtonPressed }, - new ConversionSection { Action = modButtonPressed }, - new FunSection { Action = modButtonPressed }, + CreateModSection(ModType.DifficultyReduction).With(s => + { + s.ToggleKeys = new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }; + s.Action = modButtonPressed; + }), + CreateModSection(ModType.DifficultyIncrease).With(s => + { + s.ToggleKeys = new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }; + s.Action = modButtonPressed; + }), + CreateModSection(ModType.Automation).With(s => + { + s.ToggleKeys = new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }; + s.Action = modButtonPressed; + }), + CreateModSection(ModType.Conversion).With(s => + { + s.Action = modButtonPressed; + }), + CreateModSection(ModType.Fun).With(s => + { + s.Action = modButtonPressed; + }), } }, } @@ -505,6 +522,13 @@ namespace osu.Game.Overlays.Mods private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); + /// + /// Creates a that groups s with the same . + /// + /// The of s in the section. + /// The . + protected virtual ModSection CreateModSection(ModType type) => new ModSection(type); + #region Disposal protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/Mods/Sections/AutomationSection.cs b/osu.Game/Overlays/Mods/Sections/AutomationSection.cs deleted file mode 100644 index a2d7fec15f..0000000000 --- a/osu.Game/Overlays/Mods/Sections/AutomationSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class AutomationSection : ModSection - { - protected override Key[] ToggleKeys => new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }; - public override ModType ModType => ModType.Automation; - - public AutomationSection() - { - Header = @"Automation"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/ConversionSection.cs b/osu.Game/Overlays/Mods/Sections/ConversionSection.cs deleted file mode 100644 index 24fd8c30dd..0000000000 --- a/osu.Game/Overlays/Mods/Sections/ConversionSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class ConversionSection : ModSection - { - protected override Key[] ToggleKeys => null; - public override ModType ModType => ModType.Conversion; - - public ConversionSection() - { - Header = @"Conversion"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs b/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs deleted file mode 100644 index 0b7ccd1f25..0000000000 --- a/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class DifficultyIncreaseSection : ModSection - { - protected override Key[] ToggleKeys => new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }; - public override ModType ModType => ModType.DifficultyIncrease; - - public DifficultyIncreaseSection() - { - Header = @"Difficulty Increase"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs b/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs deleted file mode 100644 index 508e92508b..0000000000 --- a/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class DifficultyReductionSection : ModSection - { - protected override Key[] ToggleKeys => new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }; - public override ModType ModType => ModType.DifficultyReduction; - - public DifficultyReductionSection() - { - Header = @"Difficulty Reduction"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/FunSection.cs b/osu.Game/Overlays/Mods/Sections/FunSection.cs deleted file mode 100644 index af1f5836b1..0000000000 --- a/osu.Game/Overlays/Mods/Sections/FunSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class FunSection : ModSection - { - protected override Key[] ToggleKeys => null; - public override ModType ModType => ModType.Fun; - - public FunSection() - { - Header = @"Fun"; - } - } -} diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 04a2e052fa..cae5da3d16 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.UI private const float size = 80; - private readonly ModType type; - public virtual string TooltipText => showTooltip ? mod.IconTooltip : null; private Mod mod; @@ -42,10 +40,18 @@ namespace osu.Game.Rulesets.UI set { mod = value; - updateMod(value); + + if (IsLoaded) + updateMod(value); } } + [Resolved] + private OsuColour colours { get; set; } + + private Color4 backgroundColour; + private Color4 highlightedColour; + /// /// Construct a new instance. /// @@ -56,8 +62,6 @@ namespace osu.Game.Rulesets.UI this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); this.showTooltip = showTooltip; - type = mod.Type; - Size = new Vector2(size); Children = new Drawable[] @@ -89,6 +93,13 @@ namespace osu.Game.Rulesets.UI Icon = FontAwesome.Solid.Question }, }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Selected.BindValueChanged(_ => updateColour()); updateMod(mod); } @@ -102,20 +113,14 @@ namespace osu.Game.Rulesets.UI { modIcon.FadeOut(); modAcronym.FadeIn(); - return; + } + else + { + modIcon.FadeIn(); + modAcronym.FadeOut(); } - modIcon.FadeIn(); - modAcronym.FadeOut(); - } - - private Color4 backgroundColour; - private Color4 highlightedColour; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - switch (type) + switch (value.Type) { default: case ModType.DifficultyIncrease: @@ -149,12 +154,13 @@ namespace osu.Game.Rulesets.UI modIcon.Colour = colours.Yellow; break; } + + updateColour(); } - protected override void LoadComplete() + private void updateColour() { - base.LoadComplete(); - Selected.BindValueChanged(selected => background.Colour = selected.NewValue ? highlightedColour : backgroundColour, true); + background.Colour = Selected.Value ? highlightedColour : backgroundColour; } } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index f8982582d5..23c713a2c1 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -124,101 +124,111 @@ namespace osu.Game.Screens.OnlinePlay modDisplay.Current.Value = requiredMods.ToArray(); } - protected override Drawable CreateContent() => maskingContainer = new Container + protected override Drawable CreateContent() { - RelativeSizeAxes = Axes.X, - Height = 50, - Masking = true, - CornerRadius = 10, - Children = new Drawable[] + Action fontParameters = s => s.Font = OsuFont.Default.With(weight: FontWeight.SemiBold); + + return maskingContainer = new Container { - new Box // A transparent box that forces the border to be drawn if the panel background is opaque + RelativeSizeAxes = Axes.X, + Height = 50, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - new PanelBackground - { - RelativeSizeAxes = Axes.Both, - Beatmap = { BindTarget = beatmap } - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 8 }, - Spacing = new Vector2(8, 0), - Direction = FillDirection.Horizontal, - Children = new Drawable[] + new Box // A transparent box that forces the border to be drawn if the panel background is opaque { - difficultyIconContainer = new Container + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new PanelBackground + { + RelativeSizeAxes = Axes.Both, + Beatmap = { BindTarget = beatmap } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 8 }, + Spacing = new Vector2(8, 0), + Direction = FillDirection.Horizontal, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] + difficultyIconContainer = new Container { - beatmapText = new LinkFlowContainer { AutoSizeAxes = Axes.Both }, - new FillFlowContainer + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10f, 0), - Children = new Drawable[] + beatmapText = new LinkFlowContainer(fontParameters) { AutoSizeAxes = Axes.Both }, + new FillFlowContainer { - new FillFlowContainer + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10f, 0), + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10f, 0), - Children = new Drawable[] + new FillFlowContainer { - authorText = new LinkFlowContainer { AutoSizeAxes = Axes.Both }, - explicitContentPill = new ExplicitContentBeatmapPill + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10f, 0), + Children = new Drawable[] { - Alpha = 0f, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Top = 3f }, - } + authorText = new LinkFlowContainer(fontParameters) { AutoSizeAxes = Axes.Both }, + explicitContentPill = new ExplicitContentBeatmapPill + { + Alpha = 0f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Top = 3f }, + } + }, }, - }, - new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Child = modDisplay = new ModDisplay + new Container { - Scale = new Vector2(0.4f), - DisplayUnrankedText = false, - ExpansionMode = ExpansionMode.AlwaysExpanded + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Child = modDisplay = new ModDisplay + { + Scale = new Vector2(0.4f), + DisplayUnrankedText = false, + ExpansionMode = ExpansionMode.AlwaysExpanded + } } } } } } } + }, + new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + X = -10, + ChildrenEnumerable = CreateButtons().Select(button => button.With(b => + { + b.Anchor = Anchor.Centre; + b.Origin = Anchor.Centre; + })) } - }, - new FillFlowContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - X = -18, - ChildrenEnumerable = CreateButtons() } - } - }; + }; + } protected virtual IEnumerable CreateButtons() => new Drawable[] @@ -227,14 +237,29 @@ namespace osu.Game.Screens.OnlinePlay { Size = new Vector2(50, 30) }, - new IconButton + new PlaylistRemoveButton { - Icon = FontAwesome.Solid.MinusSquare, + Size = new Vector2(30, 30), Alpha = allowEdit ? 1 : 0, Action = () => RequestDeletion?.Invoke(Model), }, }; + public class PlaylistRemoveButton : GrayButton + { + public PlaylistRemoveButton() + : base(FontAwesome.Solid.MinusSquare) + { + TooltipText = "Remove from playlist"; + } + + [BackgroundDependencyLoader] + private void load() + { + Icon.Scale = new Vector2(0.8f); + } + } + protected override bool OnClick(ClickEvent e) { if (allowSelection) @@ -318,24 +343,18 @@ namespace osu.Game.Screens.OnlinePlay Colour = Color4.Black, Width = 0.4f, }, - // Piecewise-linear gradient with 3 segments to make it appear smoother + // Piecewise-linear gradient with 2 segments to make it appear smoother new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), - Width = 0.05f, + Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.7f)), + Width = 0.4f, }, new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), - Width = 0.2f, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), - Width = 0.05f, + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.7f), new Color4(0, 0, 0, 0.4f)), + Width = 0.4f, }, } } diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs index b7ee84eb9e..aabeafe460 100644 --- a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -47,26 +47,21 @@ namespace osu.Game.Screens.OnlinePlay private void endOperationWithKnownLease(LeasedBindable lease) { - if (lease != leasedInProgress) - return; - // for extra safety, marshal the end of operation back to the update thread if necessary. Scheduler.Add(() => { - leasedInProgress?.Return(); + if (lease != leasedInProgress) + return; + + // UnbindAll() is purposefully used instead of Return() - the two do roughly the same thing, with one difference: + // the former won't throw if the lease has already been returned before. + // this matters because framework can unbind the lease via the internal UnbindAllBindables(), which is not always detectable + // (it is in the case of disposal, but not in the case of screen exit - at least not cleanly). + leasedInProgress?.UnbindAll(); leasedInProgress = null; }, false); } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - // base call does an UnbindAllBindables(). - // clean up the leased reference here so that it doesn't get returned twice. - leasedInProgress = null; - } - private class OngoingOperation : IDisposable { private readonly OngoingOperationTracker tracker; diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 8ac5bde65a..c12b5a9fd4 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -83,6 +83,30 @@ namespace osu.Game.Utils .All(m => allowedSet.Contains(m.GetType())); } + /// + /// Check the provided combination of mods are valid for a local gameplay session. + /// + /// The mods to check. + /// Invalid mods, if any were found. Can be null if all mods were valid. + /// Whether the input mods were all valid. If false, will contain all invalid entries. + public static bool CheckValidForGameplay(IEnumerable mods, out List? invalidMods) + { + mods = mods.ToArray(); + + CheckCompatibleSet(mods, out invalidMods); + + foreach (var mod in mods) + { + if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod) + { + invalidMods ??= new List(); + invalidMods.Add(mod); + } + } + + return invalidMods == null; + } + /// /// Flattens a set of s, returning a new set with all s removed. ///