From b37f1cce3fc4bfd3c5408a8fe32a4fdc796f3b8d Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Tue, 20 Dec 2022 23:46:05 +0900 Subject: [PATCH 01/80] Added ability to report chat --- .../Visual/Online/TestSceneChatOverlay.cs | 38 +++++++ osu.Game/Overlays/Chat/ChatReportReason.cs | 36 ++++++ osu.Game/Overlays/Chat/DrawableUsername.cs | 16 ++- osu.Game/Overlays/Chat/ReportChatPopover.cs | 106 ++++++++++++++++++ osu.Game/Overlays/ChatOverlay.cs | 3 +- 5 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Overlays/Chat/ChatReportReason.cs create mode 100644 osu.Game/Overlays/Chat/ReportChatPopover.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 8cc4eabcd7..44d739a6e3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -30,6 +30,8 @@ using osu.Game.Overlays.Chat.Listing; using osu.Game.Overlays.Chat.ChannelList; using osuTK; using osuTK.Input; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Graphics.Sprites; namespace osu.Game.Tests.Visual.Online { @@ -530,6 +532,42 @@ namespace osu.Game.Tests.Visual.Online }); } + [Test] + public void TestChatReport() + { + AddStep("Show overlay with channel", () => + { + chatOverlay.Show(); + channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1); + }); + + AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); + waitForChannel1Visible(); + + AddStep("Right click username", () => + { + var username = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(username); + InputManager.Click(MouseButton.Right); + }); + + AddStep("Click report", () => + { + var btn = this.ChildrenOfType().First(x => x.Text == "Report"); + InputManager.MoveMouseTo(btn); + InputManager.Click(MouseButton.Left); + }); + + AddStep("Try to report", () => + { + var btn = this.ChildrenOfType().Single().ChildrenOfType().Single(); + InputManager.MoveMouseTo(btn); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("Report message sended", () => channelManager.CurrentChannel.Value.Messages.Any(x => x.Content.Contains("!report"))); + } + private void joinTestChannel(int i) { AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i])); diff --git a/osu.Game/Overlays/Chat/ChatReportReason.cs b/osu.Game/Overlays/Chat/ChatReportReason.cs new file mode 100644 index 0000000000..55593d29ad --- /dev/null +++ b/osu.Game/Overlays/Chat/ChatReportReason.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; + +namespace osu.Game.Overlays.Chat +{ + public enum ChatReportReason + { + [Description("Insulting People")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsInsults))] + Insults, + + [Description("Spam")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsSpam))] + Spam, + + [Description("Cheating")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsCheating))] + FoulPlay, + + [Description("Unwanted Content")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsUnwantedContent))] + UnwantedContent, + + [Description("Nonsense")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsNonsense))] + Nonsense, + + [Description("Other")] + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsOther))] + Other + } +} diff --git a/osu.Game/Overlays/Chat/DrawableUsername.cs b/osu.Game/Overlays/Chat/DrawableUsername.cs index 6bae498a6c..f70175f081 100644 --- a/osu.Game/Overlays/Chat/DrawableUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableUsername.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -25,7 +26,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Chat { - public partial class DrawableUsername : OsuClickableContainer, IHasContextMenu + public partial class DrawableUsername : OsuClickableContainer, IHasContextMenu, IHasPopover { public Color4 AccentColour { get; } @@ -152,12 +153,20 @@ namespace osu.Game.Overlays.Chat }; if (!user.Equals(api.LocalUser.Value)) + { items.Add(new OsuMenuItem("Start Chat", MenuItemType.Standard, openUserChannel)); + items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, this.ShowPopover)); + } return items.ToArray(); } } + private void report(ChatReportReason reason, string comments) + { + chatManager?.PostMessage($"!report {user.Username} ({reason.GetDescription()}): {comments}"); + } + private void openUserChannel() { chatManager?.OpenPrivateChannel(user); @@ -221,5 +230,10 @@ namespace osu.Game.Overlays.Chat Color4Extensions.FromHex("812a96"), Color4Extensions.FromHex("992861"), }; + + public Popover GetPopover() => new ReportChatPopover(user) + { + Action = report + }; } } diff --git a/osu.Game/Overlays/Chat/ReportChatPopover.cs b/osu.Game/Overlays/Chat/ReportChatPopover.cs new file mode 100644 index 0000000000..cf5e456874 --- /dev/null +++ b/osu.Game/Overlays/Chat/ReportChatPopover.cs @@ -0,0 +1,106 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Overlays.Chat +{ + public partial class ReportChatPopover : OsuPopover + { + public Action? Action; + + private readonly APIUser? user; + + private OsuEnumDropdown reasonDropdown = null!; + private OsuTextBox commentsTextBox = null!; + + public ReportChatPopover(APIUser? user) + { + this.user = user; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, AudioManager audio) + { + Child = new ReverseChildIDFillFlowContainer + { + Direction = FillDirection.Vertical, + Width = 500, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(7), + Children = new Drawable[] + { + new SpriteIcon + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Icon = FontAwesome.Solid.ExclamationTriangle, + Size = new Vector2(36), + }, + new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = ReportStrings.UserTitle(user?.Username ?? @"Someone"), + Font = OsuFont.Torus.With(size: 25), + Margin = new MarginPadding { Bottom = 10 } + }, + new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = UsersStrings.ReportReason, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 40, + Child = reasonDropdown = new OsuEnumDropdown + { + RelativeSizeAxes = Axes.X + } + }, + new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = UsersStrings.ReportComments, + }, + commentsTextBox = new OsuTextBox + { + RelativeSizeAxes = Axes.X, + PlaceholderText = UsersStrings.ReportPlaceholder, + }, + new RoundedButton + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Width = 200, + BackgroundColour = colours.Red3, + Text = UsersStrings.ReportActionsSend, + Action = () => + { + Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text); + this.HidePopover(); + }, + Margin = new MarginPadding { Bottom = 5, Top = 10 }, + } + } + }; + } + } +} diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index c539cdc5ec..c3a6ed0175 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Bindings; @@ -122,7 +123,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Y, Width = side_bar_width, }, - new Container + new PopoverContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopRight, From ffa32307c3ef8c9917a210a31871b9000cf80246 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 21 Dec 2022 01:37:16 +0900 Subject: [PATCH 02/80] abstract ReportPopover --- .../Graphics/UserInterfaceV2/ReportPopover.cs | 114 ++++++++++++++++++ osu.Game/Overlays/Chat/ReportChatPopover.cs | 94 +-------------- .../Overlays/Comments/ReportCommentPopover.cs | 98 +-------------- 3 files changed, 119 insertions(+), 187 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs diff --git a/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs new file mode 100644 index 0000000000..44af108ab1 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs @@ -0,0 +1,114 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public abstract partial class ReportPopover : OsuPopover + where T : struct, Enum + { + public Action? Action; + + private OsuEnumDropdown reasonDropdown = null!; + private OsuTextBox commentsTextBox = null!; + private RoundedButton submitButton = null!; + + public bool CanSubmitEmptyReason = false; + public LocalisableString Header; + + protected ReportPopover(LocalisableString headerString) + { + Header = headerString; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Child = new ReverseChildIDFillFlowContainer + { + Direction = FillDirection.Vertical, + Width = 500, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(7), + Children = new Drawable[] + { + new SpriteIcon + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Icon = FontAwesome.Solid.ExclamationTriangle, + Size = new Vector2(36), + }, + new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = Header, + Font = OsuFont.Torus.With(size: 25), + Margin = new MarginPadding { Bottom = 10 } + }, + new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = UsersStrings.ReportReason, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 40, + Child = reasonDropdown = new OsuEnumDropdown + { + RelativeSizeAxes = Axes.X + } + }, + new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = UsersStrings.ReportComments, + }, + commentsTextBox = new OsuTextBox + { + RelativeSizeAxes = Axes.X, + PlaceholderText = UsersStrings.ReportPlaceholder, + }, + submitButton = new RoundedButton + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Width = 200, + BackgroundColour = colours.Red3, + Text = UsersStrings.ReportActionsSend, + Action = () => + { + Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text); + this.HidePopover(); + }, + Margin = new MarginPadding { Bottom = 5, Top = 10 }, + } + } + }; + + if (!CanSubmitEmptyReason) + { + commentsTextBox.Current.BindValueChanged(e => + { + submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(e.NewValue); + }, true); + } + } + } +} diff --git a/osu.Game/Overlays/Chat/ReportChatPopover.cs b/osu.Game/Overlays/Chat/ReportChatPopover.cs index cf5e456874..79c3922795 100644 --- a/osu.Game/Overlays/Chat/ReportChatPopover.cs +++ b/osu.Game/Overlays/Chat/ReportChatPopover.cs @@ -1,106 +1,18 @@ // 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 osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; -using osuTK; namespace osu.Game.Overlays.Chat { - public partial class ReportChatPopover : OsuPopover + public partial class ReportChatPopover : ReportPopover { - public Action? Action; - - private readonly APIUser? user; - - private OsuEnumDropdown reasonDropdown = null!; - private OsuTextBox commentsTextBox = null!; - public ReportChatPopover(APIUser? user) + : base(ReportStrings.UserTitle(user?.Username ?? @"Someone")) { - this.user = user; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours, AudioManager audio) - { - Child = new ReverseChildIDFillFlowContainer - { - Direction = FillDirection.Vertical, - Width = 500, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(7), - Children = new Drawable[] - { - new SpriteIcon - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Icon = FontAwesome.Solid.ExclamationTriangle, - Size = new Vector2(36), - }, - new OsuSpriteText - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Text = ReportStrings.UserTitle(user?.Username ?? @"Someone"), - Font = OsuFont.Torus.With(size: 25), - Margin = new MarginPadding { Bottom = 10 } - }, - new OsuSpriteText - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Text = UsersStrings.ReportReason, - }, - new Container - { - RelativeSizeAxes = Axes.X, - Height = 40, - Child = reasonDropdown = new OsuEnumDropdown - { - RelativeSizeAxes = Axes.X - } - }, - new OsuSpriteText - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Text = UsersStrings.ReportComments, - }, - commentsTextBox = new OsuTextBox - { - RelativeSizeAxes = Axes.X, - PlaceholderText = UsersStrings.ReportPlaceholder, - }, - new RoundedButton - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Width = 200, - BackgroundColour = colours.Red3, - Text = UsersStrings.ReportActionsSend, - Action = () => - { - Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text); - this.HidePopover(); - }, - Margin = new MarginPadding { Bottom = 5, Top = 10 }, - } - } - }; + CanSubmitEmptyReason = true; } } } diff --git a/osu.Game/Overlays/Comments/ReportCommentPopover.cs b/osu.Game/Overlays/Comments/ReportCommentPopover.cs index f3b2a2f97c..e688dad755 100644 --- a/osu.Game/Overlays/Comments/ReportCommentPopover.cs +++ b/osu.Game/Overlays/Comments/ReportCommentPopover.cs @@ -1,111 +1,17 @@ // 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 osu.Framework.Allocation; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; -using osuTK; namespace osu.Game.Overlays.Comments { - public partial class ReportCommentPopover : OsuPopover + public partial class ReportCommentPopover : ReportPopover { - public Action? Action; - - private readonly Comment? comment; - - private OsuEnumDropdown reasonDropdown = null!; - private OsuTextBox commentsTextBox = null!; - private RoundedButton submitButton = null!; - public ReportCommentPopover(Comment? comment) + : base(ReportStrings.CommentTitle(comment?.User?.Username ?? comment?.LegacyName ?? @"Someone")) { - this.comment = comment; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Child = new ReverseChildIDFillFlowContainer - { - Direction = FillDirection.Vertical, - Width = 500, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(7), - Children = new Drawable[] - { - new SpriteIcon - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Icon = FontAwesome.Solid.ExclamationTriangle, - Size = new Vector2(36), - }, - new OsuSpriteText - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Text = ReportStrings.CommentTitle(comment?.User?.Username ?? comment?.LegacyName ?? @"Someone"), - Font = OsuFont.Torus.With(size: 25), - Margin = new MarginPadding { Bottom = 10 } - }, - new OsuSpriteText - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Text = UsersStrings.ReportReason, - }, - new Container - { - RelativeSizeAxes = Axes.X, - Height = 40, - Child = reasonDropdown = new OsuEnumDropdown - { - RelativeSizeAxes = Axes.X - } - }, - new OsuSpriteText - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Text = UsersStrings.ReportComments, - }, - commentsTextBox = new OsuTextBox - { - RelativeSizeAxes = Axes.X, - PlaceholderText = UsersStrings.ReportPlaceholder, - }, - submitButton = new RoundedButton - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Width = 200, - BackgroundColour = colours.Red3, - Text = UsersStrings.ReportActionsSend, - Action = () => - { - Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text); - this.HidePopover(); - }, - Margin = new MarginPadding { Bottom = 5, Top = 10 }, - } - } - }; - - commentsTextBox.Current.BindValueChanged(e => - { - submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(e.NewValue); - }, true); } } } From 1b2c821346da690e932b4d74fda4ac9307d99bb3 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 21 Dec 2022 15:44:02 +0900 Subject: [PATCH 03/80] showpopover directly --- .../Visual/Online/TestSceneChatOverlay.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 44d739a6e3..3a6cbdfb0f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -12,6 +12,7 @@ using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -544,19 +545,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); waitForChannel1Visible(); - AddStep("Right click username", () => - { - var username = this.ChildrenOfType().First(); - InputManager.MoveMouseTo(username); - InputManager.Click(MouseButton.Right); - }); - - AddStep("Click report", () => - { - var btn = this.ChildrenOfType().First(x => x.Text == "Report"); - InputManager.MoveMouseTo(btn); - InputManager.Click(MouseButton.Left); - }); + AddStep("Show report popover", () => this.ChildrenOfType().First().ShowPopover()); AddStep("Try to report", () => { From 191604340f8cbaf4d42c31cbc2ef2665fdfbb318 Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 21 Feb 2023 19:05:10 +0100 Subject: [PATCH 04/80] Added a way for mod settings to be kept when changing ruleset + test --- .../TestSceneModSelectOverlay.cs | 48 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 17 ++++++- osu.Game/Rulesets/Mods/Mod.cs | 31 ++++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 5ccaebd721..25edcd4e0a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Tests.Mods; using osuTK; using osuTK.Input; @@ -371,6 +372,53 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("no mod selected", () => SelectedMods.Value.Count == 0); } + [Test] + public void TestKeepSharedSettingsFromSimilarMods() + { + const float setting_change = 1.2f; + + createScreen(); + changeRuleset(0); + + AddStep("select difficulty adjust mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod()! }); + + changeRuleset(0); + AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModDifficultyAdjust); + + AddStep("change mod settings", () => + { + var osuMod = (getMod() as OsuModDifficultyAdjust)!; + osuMod.ExtendedLimits.Value = true; + osuMod.CircleSize.Value = setting_change; + osuMod.DrainRate.Value = setting_change; + osuMod.OverallDifficulty.Value = setting_change; + osuMod.ApproachRate.Value = setting_change; + }); + + changeRuleset(1); + AddAssert("taiko variant selected", () => SelectedMods.Value.SingleOrDefault() is TaikoModDifficultyAdjust); + + AddAssert("shared settings didn't change", () => + { + var taikoMod = getMod() as TaikoModDifficultyAdjust; + return + taikoMod?.ExtendedLimits.Value == true && + taikoMod?.DrainRate.Value == setting_change && + taikoMod?.OverallDifficulty.Value == setting_change; + }); + + AddAssert("non-shared settings at default", () => + { + var taikoMod = getMod() as TaikoModDifficultyAdjust; + if (taikoMod == null) + return false; + + return taikoMod.ScrollSpeed.Value == taikoMod.ScrollSpeed.Default; + }); + + ModDifficultyAdjust getMod() => (SelectedMods.Value.Single() as ModDifficultyAdjust)!; + } + [Test] public void TestExternallySetCustomizedMod() { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index cf58d07b9e..9644e3fc75 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -393,8 +393,11 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); Beatmap.BindValueChanged(onBeatmapChanged); + SelectedMods.BindValueChanged(change => Logger.Log($"{modSetting(change.OldValue)} => {modSetting(change.NewValue)}")); } + private object modSetting(IReadOnlyList mods) => mods.OfType().FirstOrDefault()?.GetSettingsSourceProperties().First().Item2.GetValue(mods.OfType().First())!; + private void addFilesWarning() { var realmStore = new RealmFileStore(realm, Storage); @@ -626,7 +629,8 @@ namespace osu.Game return; } - var previouslySelectedMods = SelectedMods.Value.ToArray(); + //for some reason emptying SelectedMods resets all SettingSources Bindables to default value + var previouslySelectedMods = SelectedMods.Value.Select(mod => mod.DeepClone()).ToArray(); if (!SelectedMods.Disabled) SelectedMods.Value = Array.Empty(); @@ -634,7 +638,16 @@ namespace osu.Game AvailableMods.Value = dict; if (!SelectedMods.Disabled) - SelectedMods.Value = previouslySelectedMods.Select(m => instance.CreateModFromAcronym(m.Acronym)).Where(m => m != null).ToArray(); + { + SelectedMods.Value = previouslySelectedMods.Select(oldMod => + { + Mod newMod = instance.CreateModFromAcronym(oldMod.Acronym); + + newMod?.CopyFromSimilar(oldMod); + + return newMod; + }).Where(m => m != null).ToArray(); + } void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First(); } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 04d55bc5fe..7cb647d223 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Rulesets.UI; using osu.Game.Utils; @@ -139,6 +140,30 @@ namespace osu.Game.Rulesets.Mods return result; } + /// + /// Copies all shared mod setting values from into this instance. + /// + /// The mod to copy properties from. + public void CopyFromSimilar(Mod source) + { + Dictionary oldSettings = new Dictionary(); + + foreach (var (_, property) in source.GetSettingsSourceProperties()) + { + oldSettings.Add(property.Name.ToSnakeCase(), property.GetValue(source)!); + } + + foreach (var (_, property) in this.GetSettingsSourceProperties()) + { + var targetBindable = (IBindable)property.GetValue(this)!; + + if (!oldSettings.TryGetValue(property.Name.ToSnakeCase(), out object? sourceSetting)) + continue; + + CopyAdjustedSetting(targetBindable, sourceSetting); + } + } + /// /// Copies mod setting values from into this instance, overwriting all existing settings. /// @@ -148,10 +173,10 @@ namespace osu.Game.Rulesets.Mods if (source.GetType() != GetType()) throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source)); - foreach (var (_, prop) in this.GetSettingsSourceProperties()) + foreach (var (_, property) in this.GetSettingsSourceProperties()) { - var targetBindable = (IBindable)prop.GetValue(this)!; - var sourceBindable = (IBindable)prop.GetValue(source)!; + var targetBindable = (IBindable)property.GetValue(this)!; + var sourceBindable = (IBindable)property.GetValue(source)!; CopyAdjustedSetting(targetBindable, sourceBindable); } From dd53a700711dd79fac8466fca94ea881db5fa537 Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 21 Feb 2023 20:59:36 +0100 Subject: [PATCH 05/80] Addressed change requests --- .../TestSceneModSelectOverlay.cs | 4 +- osu.Game/OsuGameBase.cs | 8 +--- osu.Game/Rulesets/Mods/Mod.cs | 40 +++++++++---------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 25edcd4e0a..7a46876737 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -403,8 +403,8 @@ namespace osu.Game.Tests.Visual.UserInterface var taikoMod = getMod() as TaikoModDifficultyAdjust; return taikoMod?.ExtendedLimits.Value == true && - taikoMod?.DrainRate.Value == setting_change && - taikoMod?.OverallDifficulty.Value == setting_change; + taikoMod.DrainRate.Value == setting_change && + taikoMod.OverallDifficulty.Value == setting_change; }); AddAssert("non-shared settings at default", () => diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 9644e3fc75..af2fb123d3 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -58,7 +58,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Utils; -using File = System.IO.File; using RuntimeInfo = osu.Framework.RuntimeInfo; namespace osu.Game @@ -393,11 +392,8 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); Beatmap.BindValueChanged(onBeatmapChanged); - SelectedMods.BindValueChanged(change => Logger.Log($"{modSetting(change.OldValue)} => {modSetting(change.NewValue)}")); } - private object modSetting(IReadOnlyList mods) => mods.OfType().FirstOrDefault()?.GetSettingsSourceProperties().First().Item2.GetValue(mods.OfType().First())!; - private void addFilesWarning() { var realmStore = new RealmFileStore(realm, Storage); @@ -629,7 +625,7 @@ namespace osu.Game return; } - //for some reason emptying SelectedMods resets all SettingSources Bindables to default value + //DeepCloning collection, because emptying SelectedMods resets all SettingSources Bindables to their default value var previouslySelectedMods = SelectedMods.Value.Select(mod => mod.DeepClone()).ToArray(); if (!SelectedMods.Disabled) @@ -643,7 +639,7 @@ namespace osu.Game { Mod newMod = instance.CreateModFromAcronym(oldMod.Acronym); - newMod?.CopyFromSimilar(oldMod); + newMod?.CopySharedSettings(oldMod); return newMod; }).Where(m => m != null).ToArray(); diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 7cb647d223..bc588a5cd8 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -141,10 +141,28 @@ namespace osu.Game.Rulesets.Mods } /// - /// Copies all shared mod setting values from into this instance. + /// Copies mod setting values from into this instance, overwriting all existing settings. /// /// The mod to copy properties from. - public void CopyFromSimilar(Mod source) + public void CopyFrom(Mod source) + { + if (source.GetType() != GetType()) + throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source)); + + foreach (var (_, property) in this.GetSettingsSourceProperties()) + { + var targetBindable = (IBindable)property.GetValue(this)!; + var sourceBindable = (IBindable)property.GetValue(source)!; + + CopyAdjustedSetting(targetBindable, sourceBindable); + } + } + + /// + /// Copies all mod setting values sharing same from into this instance. + /// + /// The mod to copy properties from. + internal void CopySharedSettings(Mod source) { Dictionary oldSettings = new Dictionary(); @@ -164,24 +182,6 @@ namespace osu.Game.Rulesets.Mods } } - /// - /// Copies mod setting values from into this instance, overwriting all existing settings. - /// - /// The mod to copy properties from. - public void CopyFrom(Mod source) - { - if (source.GetType() != GetType()) - throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source)); - - foreach (var (_, property) in this.GetSettingsSourceProperties()) - { - var targetBindable = (IBindable)property.GetValue(this)!; - var sourceBindable = (IBindable)property.GetValue(source)!; - - CopyAdjustedSetting(targetBindable, sourceBindable); - } - } - /// /// When creating copies or clones of a Mod, this method will be called /// to copy explicitly adjusted user settings from . From 82b07d19f86374a24128ee4e8ff7918bf1aa7acf Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 21 Feb 2023 21:48:11 +0100 Subject: [PATCH 06/80] Fix of incorrect `using` optimization --- osu.Game/OsuGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index af2fb123d3..e16aaf39ee 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -58,6 +58,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Utils; +using File = System.IO.File; using RuntimeInfo = osu.Framework.RuntimeInfo; namespace osu.Game From e321536acc2299c19b131b457838c84f15c1aed3 Mon Sep 17 00:00:00 2001 From: Terochi Date: Wed, 22 Feb 2023 07:48:43 +0100 Subject: [PATCH 07/80] Small clean up --- osu.Game/OsuGameBase.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e16aaf39ee..de1f2e810c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -626,17 +626,12 @@ namespace osu.Game return; } - //DeepCloning collection, because emptying SelectedMods resets all SettingSources Bindables to their default value - var previouslySelectedMods = SelectedMods.Value.Select(mod => mod.DeepClone()).ToArray(); - - if (!SelectedMods.Disabled) - SelectedMods.Value = Array.Empty(); - AvailableMods.Value = dict; if (!SelectedMods.Disabled) { - SelectedMods.Value = previouslySelectedMods.Select(oldMod => + //converting mods from one ruleset to the other, while also keeping their shared settings unchanged + SelectedMods.Value = SelectedMods.Value.Select(oldMod => { Mod newMod = instance.CreateModFromAcronym(oldMod.Acronym); From 09e7c21b23a71c8df297c4037af11f1d45046052 Mon Sep 17 00:00:00 2001 From: Terochi Date: Fri, 24 Feb 2023 15:11:22 +0100 Subject: [PATCH 08/80] Implemented a more complex setting conversion logic + tests --- osu.Game.Tests/Mods/ModSettingsTest.cs | 42 ++++++++++++ .../TestSceneModSelectOverlay.cs | 67 +++++++++++++++---- osu.Game/Rulesets/Mods/Mod.cs | 38 ++++++++++- 3 files changed, 133 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index b9ea1f2567..99198f6bae 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -2,6 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -32,5 +35,44 @@ namespace osu.Game.Tests.Mods Assert.That(((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value, Is.EqualTo(2.0)); Assert.That(((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value, Is.EqualTo(1.5)); } + + [Test] + public void TestCopySharedSettingsOfDifferentType() + { + const double setting_change = 2.5; + + var osuMod = new TestNonMatchinSettingTypeOsuMod(); + var maniaMod = new TestNonMatchinSettingTypeManiaMod(); + + osuMod.TestSetting.Value = setting_change; + maniaMod.CopySharedSettings(osuMod); + osuMod.CopySharedSettings(maniaMod); + + Assert.That(maniaMod.TestSetting.IsDefault, "Value has been changed"); + Assert.That(osuMod.TestSetting.Value == setting_change); + } + + private class TestNonMatchinSettingTypeOsuMod : TestNonMatchinSettingTypeMod + { + public override string Acronym => "NMO"; + public override BindableNumber TestSetting { get; } = new BindableDouble(3.5); + } + + private class TestNonMatchinSettingTypeManiaMod : TestNonMatchinSettingTypeMod + { + public override string Acronym => "NMM"; + public override Bindable TestSetting { get; } = new BindableBool(true); + } + + private abstract class TestNonMatchinSettingTypeMod : Mod + { + public override string Name => "Non-matching setting type mod"; + public override LocalisableString Description => "Description"; + public override double ScoreMultiplier => 1; + public override ModType Type => ModType.Conversion; + + [SettingSource("Test setting")] + public abstract IBindable TestSetting { get; } + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 7a46876737..a354a22fb7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -18,6 +18,7 @@ using osu.Game.Overlays.Mods; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -387,7 +388,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("change mod settings", () => { - var osuMod = (getMod() as OsuModDifficultyAdjust)!; + var osuMod = getMod(); + osuMod.ExtendedLimits.Value = true; osuMod.CircleSize.Value = setting_change; osuMod.DrainRate.Value = setting_change; @@ -400,23 +402,60 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("shared settings didn't change", () => { - var taikoMod = getMod() as TaikoModDifficultyAdjust; - return - taikoMod?.ExtendedLimits.Value == true && - taikoMod.DrainRate.Value == setting_change && - taikoMod.OverallDifficulty.Value == setting_change; + var taikoMod = getMod(); + + return taikoMod.ExtendedLimits.Value && + taikoMod.DrainRate.Value == setting_change && + taikoMod.OverallDifficulty.Value == setting_change; }); - AddAssert("non-shared settings at default", () => + AddAssert("non-shared settings unchanged", () => { - var taikoMod = getMod() as TaikoModDifficultyAdjust; - if (taikoMod == null) - return false; + var taikoMod = getMod(); - return taikoMod.ScrollSpeed.Value == taikoMod.ScrollSpeed.Default; + return taikoMod.ScrollSpeed.IsDefault; + }); + } + + [Test] + public void TestKeepSharedSettingsRatio() + { + const float setting_change = 1.8f; + + createScreen(); + changeRuleset(0); + + AddStep("select flashlight mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod()! }); + + changeRuleset(0); + AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModFlashlight); + + AddStep("change mod settings", () => + { + var osuMod = getMod(); + + // range <0.5 - 2> + osuMod.SizeMultiplier.Value = setting_change; }); - ModDifficultyAdjust getMod() => (SelectedMods.Value.Single() as ModDifficultyAdjust)!; + changeRuleset(3); + AddAssert("mania variant selected", () => SelectedMods.Value.SingleOrDefault() is ManiaModFlashlight); + + AddAssert("shared settings changed to closest ratio", () => + { + var maniaMod = getMod(); + + // range <0.5 - 3> + // converted value based on ratio = (setting_change - 0.5) / (2 - 0.5) * (3 - 0.5) + 0.5 = 2.66 + return maniaMod.SizeMultiplier.Value == 2.7f; // taking precision into account + }); + + AddAssert("other settings unchanged", () => + { + var maniaMod = getMod(); + + return maniaMod.ComboBasedSize.IsDefault; + }); } [Test] @@ -514,6 +553,8 @@ namespace osu.Game.Tests.Visual.UserInterface waitForColumnLoad(); AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); + + AddStep("disable panel filtering", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value = false); } [Test] @@ -629,6 +670,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Value == active); } + private T getMod() where T : Mod => (T)SelectedMods.Value.Single(); + private ModPanel getPanelForMod(Type modType) => modSelectOverlay.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType); diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index bc588a5cd8..f6c0e851fd 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using AutoMapper.Internal; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; @@ -173,13 +174,46 @@ namespace osu.Game.Rulesets.Mods foreach (var (_, property) in this.GetSettingsSourceProperties()) { - var targetBindable = (IBindable)property.GetValue(this)!; + object targetSetting = property.GetValue(this)!; if (!oldSettings.TryGetValue(property.Name.ToSnakeCase(), out object? sourceSetting)) continue; - CopyAdjustedSetting(targetBindable, sourceSetting); + if (((IBindable)sourceSetting).IsDefault) + // keep at default value if the source is default + continue; + + Type? targetType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); + Type? sourceType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); + + if (targetType == null || sourceType == null) + { + if (getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == + getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single()) + // change settings only if the type is the same + CopyAdjustedSetting((IBindable)targetSetting, sourceSetting); + + continue; + } + + double targetMin = getValue(targetSetting, nameof(IBindableNumber.MinValue)); + double targetMax = getValue(targetSetting, nameof(IBindableNumber.MaxValue)); + double sourceMin = getValue(sourceSetting, nameof(IBindableNumber.MinValue)); + double sourceMax = getValue(sourceSetting, nameof(IBindableNumber.MaxValue)); + double sourceValue = getValue(sourceSetting, nameof(IBindableNumber.Value)); + + // convert value to same ratio + double targetValue = (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; + + targetType.GetProperty(nameof(IBindableNumber.Value))!.SetValue(targetSetting, + Convert.ChangeType(targetValue, targetType.GenericTypeArguments.Single())); } + + double getValue(object target, string name) => + Convert.ToDouble(target.GetType().GetProperty(name)!.GetValue(target)!); + + Type? getGenericBaseType(object target, Type genericType) => + target.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); } /// From 4858d3fd4282e0f2ea48f3a5f1138f341d00edc6 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Tue, 7 Mar 2023 02:00:40 +0900 Subject: [PATCH 09/80] Added ability to edit mod presets --- .../UserInterface/TestSceneModPresetColumn.cs | 171 +++++++++++++++++- osu.Game/Overlays/Mods/EditPresetPopover.cs | 140 ++++++++++++++ osu.Game/Overlays/Mods/ModPresetPanel.cs | 8 +- 3 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Overlays/Mods/EditPresetPopover.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 1090764788..f48c52ad40 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -243,7 +243,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); AddStep("click delete", () => { - var deleteItem = this.ChildrenOfType().Single(); + var deleteItem = this.ChildrenOfType().ElementAt(1); InputManager.MoveMouseTo(deleteItem); InputManager.Click(MouseButton.Left); }); @@ -261,6 +261,175 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("preset soft-deleted", () => Realm.Run(r => r.All().Count(preset => preset.DeletePending) == 1)); } + [Test] + public void TestEditPresentName() + { + ModPresetColumn modPresetColumn = null!; + string presetName = null!; + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); + AddStep("right click first panel", () => + { + var panel = this.ChildrenOfType().First(); + presetName = panel.Preset.Value.Name; + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Right); + }); + + AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); + AddStep("click edit", () => + { + var editItem = this.ChildrenOfType().ElementAt(0); + InputManager.MoveMouseTo(editItem); + InputManager.Click(MouseButton.Left); + }); + + OsuPopover? popover = null; + AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); + AddStep("clear preset name", () => popover.ChildrenOfType().First().Current.Value = ""); + AddStep("attempt preset edit", () => + { + InputManager.MoveMouseTo(popover.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddWaitStep("wait some", 3); + AddAssert("present is not changed", () => this.ChildrenOfType().First().Preset.Value.Name == presetName); + AddUntilStep("popover is unchanged", () => this.ChildrenOfType().FirstOrDefault() == popover); + AddStep("edit preset name", () => popover.ChildrenOfType().First().Current.Value = "something new"); + AddStep("attempt preset edit", () => + { + InputManager.MoveMouseTo(popover.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); + AddAssert("present is changed", () => this.ChildrenOfType().First().Preset.Value.Name != presetName); + } + + [Test] + public void TestEditPresetMod() + { + ModPresetColumn modPresetColumn = null!; + var mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }; + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); + AddStep("select mods", () => SelectedMods.Value = mods); + AddStep("right click first panel", () => + { + var panel = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Right); + }); + + AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); + AddStep("click edit", () => + { + var editItem = this.ChildrenOfType().ElementAt(0); + InputManager.MoveMouseTo(editItem); + InputManager.Click(MouseButton.Left); + }); + + OsuPopover? popover = null; + AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); + AddStep("enable switch", () => popover.ChildrenOfType().Single().Current.Value = true); + AddStep("attempt preset edit", () => + { + InputManager.MoveMouseTo(popover.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); + AddAssert("present mod is changed", () => this.ChildrenOfType().First().Preset.Value.Mods.Count == 2); + } + + [Test] + public void TestEditModSwitchDisableWithNoModSelest() + { + ModPresetColumn modPresetColumn = null!; + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); + AddStep("right click first panel", () => + { + var panel = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Right); + }); + + AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); + AddStep("click edit", () => + { + var editItem = this.ChildrenOfType().ElementAt(0); + InputManager.MoveMouseTo(editItem); + InputManager.Click(MouseButton.Left); + }); + + OsuPopover? popover = null; + AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); + AddAssert("use current mod is disblaed", () => popover.ChildrenOfType().Single().Current.Value == false); + AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled == true); + } + + [Test] + public void TestEditModSwitchDisableWithPresetSelest() + { + ModPresetColumn modPresetColumn = null!; + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); + AddStep("select first preset mod", () => + SelectedMods.Value = this.ChildrenOfType().First().Preset.Value.Mods.ToList()); + + AddStep("right click first panel", () => + { + var panel = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Right); + }); + + AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); + AddStep("click edit", () => + { + var editItem = this.ChildrenOfType().ElementAt(0); + InputManager.MoveMouseTo(editItem); + InputManager.Click(MouseButton.Left); + }); + + OsuPopover? popover = null; + AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); + AddAssert("use current mod is enabled", () => popover.ChildrenOfType().Single().Current.Value == true); + AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled == true); + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddAssert("use current mod is disblaed", () => popover.ChildrenOfType().Single().Current.Value == false); + AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled == true); + } + private ICollection createTestPresets() => new[] { new ModPreset diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs new file mode 100644 index 0000000000..270b6f43d9 --- /dev/null +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -0,0 +1,140 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + internal partial class EditPresetPopover : OsuPopover + { + private readonly ModPresetPanel button; + + private readonly LabelledTextBox nameTextBox; + private readonly LabelledTextBox descriptionTextBox; + private readonly LabelledSwitchButton useCurrentSwitch; + private readonly ShearedButton createButton; + + [Resolved] + private Bindable> selectedMods { get; set; } = null!; + + private readonly ModPreset preset; + + public EditPresetPopover(ModPresetPanel modPresetPanel) + { + button = modPresetPanel; + preset = button.Preset.Value; + + Child = new FillFlowContainer + { + Width = 300, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(7), + Children = new Drawable[] + { + nameTextBox = new LabelledTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Label = CommonStrings.Name, + TabbableContentContainer = this + }, + descriptionTextBox = new LabelledTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Label = CommonStrings.Description, + TabbableContentContainer = this + }, + useCurrentSwitch = new LabelledSwitchButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Label = "Use Current Mod select", + }, + createButton = new ShearedButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = CommonStrings.MenuBarEdit, + Action = tryEditPreset + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) + { + Body.BorderThickness = 3; + Body.BorderColour = colours.Orange1; + + nameTextBox.Current.Value = preset.Name; + descriptionTextBox.Current.Value = preset.Description; + + selectedMods.BindValueChanged(_ => updateMods(), true); + + createButton.DarkerColour = colours.Orange1; + createButton.LighterColour = colours.Orange0; + createButton.TextColour = colourProvider.Background6; + } + + private void updateMods() + { + useCurrentSwitch.Current.Disabled = false; + + // disable the switch when mod is equal. + if (button.Active.Value) + { + useCurrentSwitch.Current.Value = true; + useCurrentSwitch.Current.Disabled = true; + } + else + { + useCurrentSwitch.Current.Value = false; + useCurrentSwitch.Current.Disabled = !selectedMods.Value.Any(); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); + } + + private void tryEditPreset() + { + if (string.IsNullOrWhiteSpace(nameTextBox.Current.Value)) + { + Body.Shake(); + return; + } + + button.Preset.PerformWrite(s => + { + s.Name = nameTextBox.Current.Value; + s.Description = descriptionTextBox.Current.Value; + + if (useCurrentSwitch.Current.Value) + { + s.Mods = selectedMods.Value.ToArray(); + } + }); + + this.HidePopover(); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 6e12e34124..f4f1af50df 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; @@ -17,7 +18,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public partial class ModPresetPanel : ModSelectPanel, IHasCustomTooltip, IHasContextMenu + public partial class ModPresetPanel : ModSelectPanel, IHasCustomTooltip, IHasContextMenu, IHasPopover { public readonly Live Preset; @@ -91,7 +92,8 @@ namespace osu.Game.Overlays.Mods public MenuItem[] ContextMenuItems => new MenuItem[] { - new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset))) + new OsuMenuItem(CommonStrings.ButtonsEdit, MenuItemType.Highlighted, this.ShowPopover), + new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset))), }; #endregion @@ -102,5 +104,7 @@ namespace osu.Game.Overlays.Mods settingChangeTracker?.Dispose(); } + + public Popover GetPopover() => new EditPresetPopover(this); } } From 0095fd85cad0a7b8dcddd29c983991af959a57fb Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Tue, 7 Mar 2023 02:18:34 +0900 Subject: [PATCH 10/80] remove `== true` --- .../Visual/UserInterface/TestSceneModPresetColumn.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index f48c52ad40..8fbc8940c7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -386,7 +386,7 @@ namespace osu.Game.Tests.Visual.UserInterface OsuPopover? popover = null; AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); AddAssert("use current mod is disblaed", () => popover.ChildrenOfType().Single().Current.Value == false); - AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled == true); + AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled); } [Test] @@ -422,12 +422,12 @@ namespace osu.Game.Tests.Visual.UserInterface OsuPopover? popover = null; AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); - AddAssert("use current mod is enabled", () => popover.ChildrenOfType().Single().Current.Value == true); - AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled == true); + AddAssert("use current mod is enabled", () => popover.ChildrenOfType().Single().Current.Value); + AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled); AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); AddAssert("use current mod is disblaed", () => popover.ChildrenOfType().Single().Current.Value == false); - AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled == true); + AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled); } private ICollection createTestPresets() => new[] From 54564e05578df03133636cad564becd6b892378f Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Tue, 7 Mar 2023 21:13:35 +0900 Subject: [PATCH 11/80] new design --- .../UserInterface/TestSceneModPresetColumn.cs | 86 ++----------------- osu.Game/Overlays/Mods/EditPresetPopover.cs | 62 ++++++------- 2 files changed, 38 insertions(+), 110 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 8fbc8940c7..d6065374ac 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -296,7 +296,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("clear preset name", () => popover.ChildrenOfType().First().Current.Value = ""); AddStep("attempt preset edit", () => { - InputManager.MoveMouseTo(popover.ChildrenOfType().Single()); + InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Left); }); @@ -306,7 +306,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("edit preset name", () => popover.ChildrenOfType().First().Current.Value = "something new"); AddStep("attempt preset edit", () => { - InputManager.MoveMouseTo(popover.ChildrenOfType().Single()); + InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Left); }); AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); @@ -345,91 +345,15 @@ namespace osu.Game.Tests.Visual.UserInterface OsuPopover? popover = null; AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); - AddStep("enable switch", () => popover.ChildrenOfType().Single().Current.Value = true); - AddStep("attempt preset edit", () => + AddStep("click use current mods", () => { - InputManager.MoveMouseTo(popover.ChildrenOfType().Single()); + InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(0)); InputManager.Click(MouseButton.Left); }); - AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); + AddUntilStep("popover not closed", () => this.ChildrenOfType().Any()); AddAssert("present mod is changed", () => this.ChildrenOfType().First().Preset.Value.Mods.Count == 2); } - [Test] - public void TestEditModSwitchDisableWithNoModSelest() - { - ModPresetColumn modPresetColumn = null!; - - AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); - AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - - AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); - AddStep("right click first panel", () => - { - var panel = this.ChildrenOfType().First(); - InputManager.MoveMouseTo(panel); - InputManager.Click(MouseButton.Right); - }); - - AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); - AddStep("click edit", () => - { - var editItem = this.ChildrenOfType().ElementAt(0); - InputManager.MoveMouseTo(editItem); - InputManager.Click(MouseButton.Left); - }); - - OsuPopover? popover = null; - AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); - AddAssert("use current mod is disblaed", () => popover.ChildrenOfType().Single().Current.Value == false); - AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled); - } - - [Test] - public void TestEditModSwitchDisableWithPresetSelest() - { - ModPresetColumn modPresetColumn = null!; - - AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); - AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - - AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); - AddStep("select first preset mod", () => - SelectedMods.Value = this.ChildrenOfType().First().Preset.Value.Mods.ToList()); - - AddStep("right click first panel", () => - { - var panel = this.ChildrenOfType().First(); - InputManager.MoveMouseTo(panel); - InputManager.Click(MouseButton.Right); - }); - - AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); - AddStep("click edit", () => - { - var editItem = this.ChildrenOfType().ElementAt(0); - InputManager.MoveMouseTo(editItem); - InputManager.Click(MouseButton.Left); - }); - - OsuPopover? popover = null; - AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); - AddAssert("use current mod is enabled", () => popover.ChildrenOfType().Single().Current.Value); - AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled); - - AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); - AddAssert("use current mod is disblaed", () => popover.ChildrenOfType().Single().Current.Value == false); - AddAssert("use current mod switch cannot be switch", () => popover.ChildrenOfType().Single().Current.Disabled); - } - private ICollection createTestPresets() => new[] { new ModPreset diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 270b6f43d9..4eaf5e19db 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Mods private readonly LabelledTextBox nameTextBox; private readonly LabelledTextBox descriptionTextBox; - private readonly LabelledSwitchButton useCurrentSwitch; + private readonly ShearedButton useCurrentModButton; private readonly ShearedButton createButton; [Resolved] @@ -42,6 +42,7 @@ namespace osu.Game.Overlays.Mods Width = 300, AutoSizeAxes = Axes.Y, Spacing = new Vector2(7), + Direction = FillDirection.Vertical, Children = new Drawable[] { nameTextBox = new LabelledTextBox @@ -58,18 +59,30 @@ namespace osu.Game.Overlays.Mods Label = CommonStrings.Description, TabbableContentContainer = this }, - useCurrentSwitch = new LabelledSwitchButton + new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Label = "Use Current Mod select", - }, - createButton = new ShearedButton - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = CommonStrings.MenuBarEdit, - Action = tryEditPreset + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(7), + Children = new Drawable[] + { + useCurrentModButton = new ShearedButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Use Current Mods", + Action = trySaveCurrentMod + }, + createButton = new ShearedButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = Resources.Localisation.Web.CommonStrings.ButtonsSave, + Action = tryEditPreset + }, + } } } }; @@ -84,28 +97,24 @@ namespace osu.Game.Overlays.Mods nameTextBox.Current.Value = preset.Name; descriptionTextBox.Current.Value = preset.Description; - selectedMods.BindValueChanged(_ => updateMods(), true); - createButton.DarkerColour = colours.Orange1; createButton.LighterColour = colours.Orange0; createButton.TextColour = colourProvider.Background6; + + useCurrentModButton.DarkerColour = colours.Blue1; + useCurrentModButton.LighterColour = colours.Blue0; + useCurrentModButton.TextColour = colourProvider.Background6; } - private void updateMods() + private void trySaveCurrentMod() { - useCurrentSwitch.Current.Disabled = false; + if (button.Active.Value || !selectedMods.Value.Any()) + return; - // disable the switch when mod is equal. - if (button.Active.Value) + button.Preset.PerformWrite(s => { - useCurrentSwitch.Current.Value = true; - useCurrentSwitch.Current.Disabled = true; - } - else - { - useCurrentSwitch.Current.Value = false; - useCurrentSwitch.Current.Disabled = !selectedMods.Value.Any(); - } + s.Mods = selectedMods.Value.ToArray(); + }); } protected override void LoadComplete() @@ -127,11 +136,6 @@ namespace osu.Game.Overlays.Mods { s.Name = nameTextBox.Current.Value; s.Description = descriptionTextBox.Current.Value; - - if (useCurrentSwitch.Current.Value) - { - s.Mods = selectedMods.Value.ToArray(); - } }); this.HidePopover(); From bedf4cc2596f9202a8760cbbe731ca2f1f6bf8ed Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 7 Mar 2023 16:03:11 +0100 Subject: [PATCH 12/80] Remove extra code --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index a354a22fb7..75efc07d43 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -553,8 +553,6 @@ namespace osu.Game.Tests.Visual.UserInterface waitForColumnLoad(); AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); - - AddStep("disable panel filtering", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value = false); } [Test] From 9ea93e0a9fb7b5a9d629519cd6e257e80acd1510 Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 7 Mar 2023 20:38:33 +0100 Subject: [PATCH 13/80] Add more tests --- osu.Game.Tests/Mods/ModSettingsTest.cs | 91 +++++++++++++++++++++----- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index 99198f6bae..45e162df1a 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -1,6 +1,7 @@ // 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 NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Localisation; @@ -37,34 +38,92 @@ namespace osu.Game.Tests.Mods } [Test] - public void TestCopySharedSettingsOfDifferentType() + public void TestDifferentTypeSettingsKeptWhenCopied() { - const double setting_change = 2.5; + const double setting_change = 50.4; - var osuMod = new TestNonMatchinSettingTypeOsuMod(); - var maniaMod = new TestNonMatchinSettingTypeManiaMod(); + var modDouble = new TestNonMatchingSettingTypeModDouble(); + var modBool = new TestNonMatchingSettingTypeModBool(); - osuMod.TestSetting.Value = setting_change; - maniaMod.CopySharedSettings(osuMod); - osuMod.CopySharedSettings(maniaMod); + modDouble.TestSetting.Value = setting_change; + modBool.TestSetting.Value = !modBool.TestSetting.Default; + modDouble.CopySharedSettings(modBool); + modBool.CopySharedSettings(modDouble); - Assert.That(maniaMod.TestSetting.IsDefault, "Value has been changed"); - Assert.That(osuMod.TestSetting.Value == setting_change); + Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); + Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); } - private class TestNonMatchinSettingTypeOsuMod : TestNonMatchinSettingTypeMod + [Test] + public void TestCopyDoubleToIntWithDefaultRange() { - public override string Acronym => "NMO"; - public override BindableNumber TestSetting { get; } = new BindableDouble(3.5); + const double setting_change = 50.4; + + var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } }; + var modInt = new TestNonMatchingSettingTypeModInt(); + + modInt.CopySharedSettings(modDouble); + + Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change)); } - private class TestNonMatchinSettingTypeManiaMod : TestNonMatchinSettingTypeMod + [Test] + public void TestCopyIntToDoubleWithDefaultRange() { - public override string Acronym => "NMM"; - public override Bindable TestSetting { get; } = new BindableBool(true); + const int setting_change = 50; + + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = setting_change } }; + var modDouble = new TestNonMatchingSettingTypeModDouble(); + + modDouble.CopySharedSettings(modInt); + + Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); } - private abstract class TestNonMatchinSettingTypeMod : Mod + [Test] + public void TestCopyDoubleToIntWithClampedRange() + { + const double setting_change = 50.4; + + var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 100, MinValue = 0, Value = setting_change } }; + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MaxValue = 200, MinValue = 0 } }; + + modInt.CopySharedSettings(modDouble); + + Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change * 2))); + } + + [Test] + public void TestDefaultValueKeptWhenCopied() + { + var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } }; + var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; + + modBoolFalse.CopySharedSettings(modBoolTrue); + + Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false)); + Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value)); + } + + private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod + { + public override string Acronym => "NMD"; + public override BindableNumber TestSetting { get; } = new BindableDouble(); + } + + private class TestNonMatchingSettingTypeModInt : TestNonMatchingSettingTypeMod + { + public override string Acronym => "NMI"; + public override BindableNumber TestSetting { get; } = new BindableInt(); + } + + private class TestNonMatchingSettingTypeModBool : TestNonMatchingSettingTypeMod + { + public override string Acronym => "NMB"; + public override Bindable TestSetting { get; } = new BindableBool(); + } + + private abstract class TestNonMatchingSettingTypeMod : Mod { public override string Name => "Non-matching setting type mod"; public override LocalisableString Description => "Description"; From 8bf84869a5334c14aec47ebeb43470c8a0d808a1 Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 7 Mar 2023 20:39:50 +0100 Subject: [PATCH 14/80] Fixed errors covered in new tests --- osu.Game/Rulesets/Mods/Mod.cs | 44 +++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f6c0e851fd..e630a53045 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -165,6 +165,10 @@ namespace osu.Game.Rulesets.Mods /// The mod to copy properties from. internal void CopySharedSettings(Mod source) { + const string min_value = nameof(BindableNumber.MinValue); + const string max_value = nameof(BindableNumber.MaxValue); + const string value = nameof(Bindable.Value); + Dictionary oldSettings = new Dictionary(); foreach (var (_, property) in source.GetSettingsSourceProperties()) @@ -190,27 +194,47 @@ namespace osu.Game.Rulesets.Mods { if (getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single()) + { // change settings only if the type is the same - CopyAdjustedSetting((IBindable)targetSetting, sourceSetting); + setValue(targetSetting, value, getValue(sourceSetting, value)); + } continue; } - double targetMin = getValue(targetSetting, nameof(IBindableNumber.MinValue)); - double targetMax = getValue(targetSetting, nameof(IBindableNumber.MaxValue)); - double sourceMin = getValue(sourceSetting, nameof(IBindableNumber.MinValue)); - double sourceMax = getValue(sourceSetting, nameof(IBindableNumber.MaxValue)); - double sourceValue = getValue(sourceSetting, nameof(IBindableNumber.Value)); + Type targetGenericType = targetType.GenericTypeArguments.Single(); + Type sourceGenericType = sourceType.GenericTypeArguments.Single(); + + double allowedMin = Math.Max( + Convert.ToDouble(targetGenericType.GetField("MinValue")!.GetValue(null)), + Convert.ToDouble(sourceGenericType.GetField("MinValue")!.GetValue(null)) + ); + + double allowedMax = Math.Min( + Convert.ToDouble(targetGenericType.GetField("MaxValue")!.GetValue(null)), + Convert.ToDouble(sourceGenericType.GetField("MaxValue")!.GetValue(null)) + ); + + double targetMin = getValueDouble(targetSetting, min_value); + double targetMax = getValueDouble(targetSetting, max_value); + double sourceMin = getValueDouble(sourceSetting, min_value); + double sourceMax = getValueDouble(sourceSetting, max_value); + double sourceValue = getValueDouble(sourceSetting, value); // convert value to same ratio double targetValue = (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; - targetType.GetProperty(nameof(IBindableNumber.Value))!.SetValue(targetSetting, - Convert.ChangeType(targetValue, targetType.GenericTypeArguments.Single())); + setValue(targetSetting, value, Convert.ChangeType(targetValue, targetType.GenericTypeArguments.Single())); + + double getValueDouble(object target, string name) => + Math.Clamp(Convert.ToDouble(getValue(target, name)!), allowedMin, allowedMax); } - double getValue(object target, string name) => - Convert.ToDouble(target.GetType().GetProperty(name)!.GetValue(target)!); + object? getValue(object target, string name) => + target.GetType().GetProperty(name)!.GetValue(target); + + void setValue(object target, string name, object? newValue) => + target.GetType().GetProperty(name)!.SetValue(target, newValue); Type? getGenericBaseType(object target, Type genericType) => target.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); From 5a1316f0e5eb0d7acd8c813e84849cef5c584dbb Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 9 Mar 2023 22:23:13 +0900 Subject: [PATCH 15/80] split save logic --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 8 ++------ osu.Game/Overlays/Mods/ModPresetPanel.cs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 4eaf5e19db..32d4aeae76 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -108,13 +107,10 @@ namespace osu.Game.Overlays.Mods private void trySaveCurrentMod() { - if (button.Active.Value || !selectedMods.Value.Any()) + if (button.SaveCurrentMod()) return; - button.Preset.PerformWrite(s => - { - s.Mods = selectedMods.Value.ToArray(); - }); + Body.Shake(); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index f4f1af50df..f861245007 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -98,6 +98,20 @@ namespace osu.Game.Overlays.Mods #endregion + public bool SaveCurrentMod() + { + if (!checkCurrentModCanBeSave()) + return false; + + Preset.PerformWrite(s => + { + s.Mods = selectedMods.Value.ToArray(); + }); + return true; + } + + private bool checkCurrentModCanBeSave() => (!Active.Value && selectedMods.Value.Any()); + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 3d746e8dfbb6cfbd7a08a480eda3d56a3262ed67 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 9 Mar 2023 22:43:11 +0900 Subject: [PATCH 16/80] content Menu --- osu.Game/Overlays/Mods/ModPresetPanel.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index f861245007..f3fb5b5bb0 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -90,11 +90,24 @@ namespace osu.Game.Overlays.Mods #region IHasContextMenu - public MenuItem[] ContextMenuItems => new MenuItem[] + public MenuItem[] ContextMenuItems { - new OsuMenuItem(CommonStrings.ButtonsEdit, MenuItemType.Highlighted, this.ShowPopover), - new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset))), - }; + get + { + var menu = new List + { + new OsuMenuItem(CommonStrings.ButtonsEdit, MenuItemType.Highlighted, this.ShowPopover), + new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset))), + }; + + if (checkCurrentModCanBeSave()) + { + menu.Insert(1, new OsuMenuItem("Use Current Mods", MenuItemType.Destructive, () => SaveCurrentMod())); + } + + return menu.ToArray(); + } + } #endregion From d009cd8422780247f035bb586851790bac37f6b5 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 9 Mar 2023 22:45:33 +0900 Subject: [PATCH 17/80] test --- .../UserInterface/TestSceneModPresetColumn.cs | 73 ++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index d6065374ac..b67f67db2b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -327,6 +327,32 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); + + AddStep("right click first panel", () => + { + var panel = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Right); + }); + AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); + AddStep("click edit", () => + { + var editItem = this.ChildrenOfType().ElementAt(0); + InputManager.MoveMouseTo(editItem); + InputManager.Click(MouseButton.Left); + }); + + OsuPopover? popover = null; + AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); + AddStep("click use current mods", () => + { + InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(0)); + InputManager.Click(MouseButton.Left); + }); + AddWaitStep("wait some", 3); + AddUntilStep("popover not closed", () => this.ChildrenOfType().Any()); + AddAssert("present mod not changed", () => this.ChildrenOfType().First().Preset.Value.Mods.Count == 1); + AddStep("select mods", () => SelectedMods.Value = mods); AddStep("right click first panel", () => { @@ -343,14 +369,57 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); - OsuPopover? popover = null; AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); AddStep("click use current mods", () => { InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(0)); InputManager.Click(MouseButton.Left); }); - AddUntilStep("popover not closed", () => this.ChildrenOfType().Any()); + AddWaitStep("wait some", 3); + AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); + AddAssert("present mod is changed", () => this.ChildrenOfType().First().Preset.Value.Mods.Count == 2); + } + + [Test] + public void TestEditPresetModInContextMenu() + { + ModPresetColumn modPresetColumn = null!; + var mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }; + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); + + AddStep("right click first panel", () => + { + var panel = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Right); + }); + AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); + AddAssert("No Use Current Mods", () => this.ChildrenOfType().Count() == 2); + + AddStep("select mods", () => SelectedMods.Value = mods); + AddStep("right click first panel", () => + { + var panel = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Right); + }); + + AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); + AddAssert("Have Use Current Mods", () => this.ChildrenOfType().Count() == 3); + AddStep("Click Use Current Mods", () => + { + var editItem = this.ChildrenOfType().ElementAt(1); + InputManager.MoveMouseTo(editItem); + InputManager.Click(MouseButton.Left); + }); AddAssert("present mod is changed", () => this.ChildrenOfType().First().Preset.Value.Mods.Count == 2); } From ca416175bb6e168c3c8fa469b296d059a8991541 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 9 Mar 2023 22:58:44 +0900 Subject: [PATCH 18/80] remove useless property --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 32d4aeae76..3043f4a544 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -26,9 +26,6 @@ namespace osu.Game.Overlays.Mods private readonly ShearedButton useCurrentModButton; private readonly ShearedButton createButton; - [Resolved] - private Bindable> selectedMods { get; set; } = null!; - private readonly ModPreset preset; public EditPresetPopover(ModPresetPanel modPresetPanel) From f4e262040275e4466f4e43def09980a6a1b6d432 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Fri, 10 Mar 2023 00:56:22 +0900 Subject: [PATCH 19/80] fix test --- .../UserInterface/TestSceneModPresetColumn.cs | 22 +++++++++++-------- osu.Game/Overlays/Mods/EditPresetPopover.cs | 2 -- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index b67f67db2b..eaa3664623 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -266,6 +266,7 @@ namespace osu.Game.Tests.Visual.UserInterface { ModPresetColumn modPresetColumn = null!; string presetName = null!; + ModPresetPanel panel = null!; AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn @@ -277,7 +278,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); AddStep("right click first panel", () => { - var panel = this.ChildrenOfType().First(); + panel = this.ChildrenOfType().First(); presetName = panel.Preset.Value.Name; InputManager.MoveMouseTo(panel); InputManager.Click(MouseButton.Right); @@ -301,7 +302,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddWaitStep("wait some", 3); - AddAssert("present is not changed", () => this.ChildrenOfType().First().Preset.Value.Name == presetName); + AddAssert("present is not changed", () => panel.Preset.Value.Name == presetName); AddUntilStep("popover is unchanged", () => this.ChildrenOfType().FirstOrDefault() == popover); AddStep("edit preset name", () => popover.ChildrenOfType().First().Current.Value = "something new"); AddStep("attempt preset edit", () => @@ -310,7 +311,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); - AddAssert("present is changed", () => this.ChildrenOfType().First().Preset.Value.Name != presetName); + AddAssert("present is changed", () => panel.Preset.Value.Name != presetName); } [Test] @@ -351,7 +352,8 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddWaitStep("wait some", 3); AddUntilStep("popover not closed", () => this.ChildrenOfType().Any()); - AddAssert("present mod not changed", () => this.ChildrenOfType().First().Preset.Value.Mods.Count == 1); + AddAssert("present mod not changed", () => + !new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); AddStep("select mods", () => SelectedMods.Value = mods); AddStep("right click first panel", () => @@ -377,14 +379,15 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddWaitStep("wait some", 3); AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); - AddAssert("present mod is changed", () => this.ChildrenOfType().First().Preset.Value.Mods.Count == 2); + AddAssert("present mod is changed", () => + new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); } [Test] public void TestEditPresetModInContextMenu() { ModPresetColumn modPresetColumn = null!; - var mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }; + var mods = new Mod[] { new OsuModHidden() }; AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn @@ -405,9 +408,9 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("No Use Current Mods", () => this.ChildrenOfType().Count() == 2); AddStep("select mods", () => SelectedMods.Value = mods); - AddStep("right click first panel", () => + AddStep("right click second panel", () => { - var panel = this.ChildrenOfType().First(); + var panel = this.ChildrenOfType().ElementAt(1); InputManager.MoveMouseTo(panel); InputManager.Click(MouseButton.Right); }); @@ -420,7 +423,8 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.MoveMouseTo(editItem); InputManager.Click(MouseButton.Left); }); - AddAssert("present mod is changed", () => this.ChildrenOfType().First().Preset.Value.Mods.Count == 2); + AddAssert("present mod is changed", () => + new HashSet(this.ChildrenOfType().ElementAt(1).Preset.Value.Mods).SetEquals(mods)); } private ICollection createTestPresets() => new[] diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 3043f4a544..a5c562914b 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -1,9 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; From b51c41a8043ae04fdc57aad461beb9f74b2320b5 Mon Sep 17 00:00:00 2001 From: Terochi Date: Thu, 9 Mar 2023 20:14:58 +0100 Subject: [PATCH 20/80] Addressed changes --- osu.Game.Tests/Mods/ModSettingsTest.cs | 92 ++++++++++++++++++-------- osu.Game/Rulesets/Mods/Mod.cs | 69 ++++++++++++------- 2 files changed, 109 insertions(+), 52 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index 45e162df1a..e2e050870e 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Mods public class ModSettingsTest { [Test] - public void TestModSettingsUnboundWhenCopied() + public void TestModSettingsUnboundWhenCloned() { var original = new OsuModDoubleTime(); var copy = (OsuModDoubleTime)original.DeepClone(); @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Mods } [Test] - public void TestMultiModSettingsUnboundWhenCopied() + public void TestMultiModSettingsUnboundWhenCloned() { var original = new MultiMod(new OsuModDoubleTime()); var copy = (MultiMod)original.DeepClone(); @@ -54,6 +54,42 @@ namespace osu.Game.Tests.Mods Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); } + [Test] + public void TestDefaultValueKeptWhenCopied() + { + var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } }; + var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; + + modBoolFalse.CopySharedSettings(modBoolTrue); + + Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false)); + Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value)); + } + + [Test] + public void TestValueResetsToDefaultWhenCopied() + { + var modDouble = new TestNonMatchingSettingTypeModDouble(); + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = 1 } }; + + modInt.CopySharedSettings(modDouble); + + Assert.That(modInt.TestSetting.Value, Is.EqualTo(modInt.TestSetting.Default)); + } + + [Test] + public void TestRelativelyScaleWithClampedRangeWhenCopied() + { + const double setting_change = 50.4; + + var modDouble100 = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 100, MinValue = 0, Value = setting_change } }; + var modDouble200 = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 200, MinValue = 0 } }; + + modDouble200.CopySharedSettings(modDouble100); + + Assert.That(modDouble200.TestSetting.Value, Is.EqualTo(setting_change * 2)); + } + [Test] public void TestCopyDoubleToIntWithDefaultRange() { @@ -64,7 +100,32 @@ namespace osu.Game.Tests.Mods modInt.CopySharedSettings(modDouble); - Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change)); + Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change))); + } + + [Test] + public void TestCopyDoubleToIntWithOutOfBoundsRange() + { + const double setting_change = 50.4; + + var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MinValue = int.MinValue - 1d, Value = setting_change } }; + // make RangeConstrainedBindable.HasDefinedRange return true + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MinValue = int.MinValue + 1 } }; + + modInt.CopySharedSettings(modDouble); + + Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change))); + } + + [Test] + public void TestCopyDoubleToIntWithOutOfBoundsValue() + { + var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MinValue = int.MinValue + 1, Value = int.MaxValue + 1d } }; + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MinValue = int.MinValue + 1 } }; + + modInt.CopySharedSettings(modDouble); + + Assert.That(modInt.TestSetting.Value, Is.EqualTo(int.MaxValue)); } [Test] @@ -80,31 +141,6 @@ namespace osu.Game.Tests.Mods Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); } - [Test] - public void TestCopyDoubleToIntWithClampedRange() - { - const double setting_change = 50.4; - - var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 100, MinValue = 0, Value = setting_change } }; - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MaxValue = 200, MinValue = 0 } }; - - modInt.CopySharedSettings(modDouble); - - Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change * 2))); - } - - [Test] - public void TestDefaultValueKeptWhenCopied() - { - var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } }; - var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - - modBoolFalse.CopySharedSettings(modBoolTrue); - - Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false)); - Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value)); - } - private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod { public override string Acronym => "NMD"; diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index e630a53045..85014d555d 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -169,28 +169,31 @@ namespace osu.Game.Rulesets.Mods const string max_value = nameof(BindableNumber.MaxValue); const string value = nameof(Bindable.Value); - Dictionary oldSettings = new Dictionary(); + Dictionary sourceSettings = new Dictionary(); - foreach (var (_, property) in source.GetSettingsSourceProperties()) + foreach (var (_, sourceProperty) in source.GetSettingsSourceProperties()) { - oldSettings.Add(property.Name.ToSnakeCase(), property.GetValue(source)!); + sourceSettings.Add(sourceProperty.Name.ToSnakeCase(), sourceProperty.GetValue(source)!); } - foreach (var (_, property) in this.GetSettingsSourceProperties()) + foreach (var (_, targetProperty) in this.GetSettingsSourceProperties()) { - object targetSetting = property.GetValue(this)!; + object targetSetting = targetProperty.GetValue(this)!; - if (!oldSettings.TryGetValue(property.Name.ToSnakeCase(), out object? sourceSetting)) + if (!sourceSettings.TryGetValue(targetProperty.Name.ToSnakeCase(), out object? sourceSetting)) continue; if (((IBindable)sourceSetting).IsDefault) - // keep at default value if the source is default + { + // reset to default value if the source is default + targetSetting.GetType().GetMethod(nameof(Bindable.SetDefault))!.Invoke(targetSetting, null); continue; + } - Type? targetType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); - Type? sourceType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); + Type? targetBindableNumberType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); + Type? sourceBindableNumberType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); - if (targetType == null || sourceType == null) + if (targetBindableNumberType == null || sourceBindableNumberType == null) { if (getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single()) @@ -202,8 +205,16 @@ namespace osu.Game.Rulesets.Mods continue; } - Type targetGenericType = targetType.GenericTypeArguments.Single(); - Type sourceGenericType = sourceType.GenericTypeArguments.Single(); + bool rangeOutOfBounds = false; + + Type targetGenericType = targetBindableNumberType.GenericTypeArguments.Single(); + Type sourceGenericType = sourceBindableNumberType.GenericTypeArguments.Single(); + + if (!Convert.ToBoolean(getValue(targetSetting, nameof(RangeConstrainedBindable.HasDefinedRange))) || + !Convert.ToBoolean(getValue(sourceSetting, nameof(RangeConstrainedBindable.HasDefinedRange)))) + // check if we have a range to rescale from and a range to rescale to + // if not, copy the raw value + rangeOutOfBounds = true; double allowedMin = Math.Max( Convert.ToDouble(targetGenericType.GetField("MinValue")!.GetValue(null)), @@ -219,25 +230,35 @@ namespace osu.Game.Rulesets.Mods double targetMax = getValueDouble(targetSetting, max_value); double sourceMin = getValueDouble(sourceSetting, min_value); double sourceMax = getValueDouble(sourceSetting, max_value); - double sourceValue = getValueDouble(sourceSetting, value); + double sourceValue = Math.Clamp(getValueDouble(sourceSetting, value), allowedMin, allowedMax); - // convert value to same ratio - double targetValue = (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; + double targetValue = rangeOutOfBounds + // keep raw value + ? sourceValue + // convert value to same ratio + : (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; - setValue(targetSetting, value, Convert.ChangeType(targetValue, targetType.GenericTypeArguments.Single())); + setValue(targetSetting, value, Convert.ChangeType(targetValue, targetBindableNumberType.GenericTypeArguments.Single())); - double getValueDouble(object target, string name) => - Math.Clamp(Convert.ToDouble(getValue(target, name)!), allowedMin, allowedMax); + double getValueDouble(object setting, string name) + { + double settingValue = Convert.ToDouble(getValue(setting, name)!); + + if (settingValue < allowedMin || settingValue > allowedMax) + rangeOutOfBounds = true; + + return settingValue; + } } - object? getValue(object target, string name) => - target.GetType().GetProperty(name)!.GetValue(target); + object? getValue(object setting, string name) => + setting.GetType().GetProperty(name)!.GetValue(setting); - void setValue(object target, string name, object? newValue) => - target.GetType().GetProperty(name)!.SetValue(target, newValue); + void setValue(object setting, string name, object? newValue) => + setting.GetType().GetProperty(name)!.SetValue(setting, newValue); - Type? getGenericBaseType(object target, Type genericType) => - target.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); + Type? getGenericBaseType(object setting, Type genericType) => + setting.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); } /// From 8b0f127ff2a652cb2d8381d4c500c6cd040b878e Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sat, 11 Mar 2023 11:25:52 +0900 Subject: [PATCH 21/80] split ModPresetRow --- osu.Game/Overlays/Mods/ModPresetRow.cs | 64 ++++++++++++++++++++++ osu.Game/Overlays/Mods/ModPresetTooltip.cs | 54 ------------------ 2 files changed, 64 insertions(+), 54 deletions(-) create mode 100644 osu.Game/Overlays/Mods/ModPresetRow.cs diff --git a/osu.Game/Overlays/Mods/ModPresetRow.cs b/osu.Game/Overlays/Mods/ModPresetRow.cs new file mode 100644 index 0000000000..4829e93b87 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModPresetRow.cs @@ -0,0 +1,64 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public partial class ModPresetRow : FillFlowContainer + { + public ModPresetRow(Mod mod) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + Spacing = new Vector2(4); + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(7), + Children = new Drawable[] + { + new ModSwitchTiny(mod) + { + Active = { Value = true }, + Scale = new Vector2(0.6f), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new OsuSpriteText + { + Text = mod.Name, + Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Bottom = 2 } + } + } + } + }; + + if (!string.IsNullOrEmpty(mod.SettingDescription)) + { + AddInternal(new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = 14 }, + Text = mod.SettingDescription + }); + } + } + } +} diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index ff4f00da69..8e8259de45 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -6,11 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.UI; using osuTK; namespace osu.Game.Overlays.Mods @@ -61,55 +57,5 @@ namespace osu.Game.Overlays.Mods protected override void PopOut() => this.FadeOut(transition_duration, Easing.OutQuint); public void Move(Vector2 pos) => Position = pos; - - private partial class ModPresetRow : FillFlowContainer - { - public ModPresetRow(Mod mod) - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Direction = FillDirection.Vertical; - Spacing = new Vector2(4); - InternalChildren = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(7), - Children = new Drawable[] - { - new ModSwitchTiny(mod) - { - Active = { Value = true }, - Scale = new Vector2(0.6f), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }, - new OsuSpriteText - { - Text = mod.Name, - Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Bottom = 2 } - } - } - } - }; - - if (!string.IsNullOrEmpty(mod.SettingDescription)) - { - AddInternal(new OsuTextFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 14 }, - Text = mod.SettingDescription - }); - } - } - } } } From 1cd565193e3a546e0e6d8e603e4eebc09688a0fd Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sat, 11 Mar 2023 11:39:35 +0900 Subject: [PATCH 22/80] public CheckCurrentModCanBeSave --- osu.Game/Overlays/Mods/ModPresetPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index f3fb5b5bb0..d01981d18c 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -100,7 +100,7 @@ namespace osu.Game.Overlays.Mods new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset))), }; - if (checkCurrentModCanBeSave()) + if (CheckCurrentModCanBeSave()) { menu.Insert(1, new OsuMenuItem("Use Current Mods", MenuItemType.Destructive, () => SaveCurrentMod())); } @@ -113,7 +113,7 @@ namespace osu.Game.Overlays.Mods public bool SaveCurrentMod() { - if (!checkCurrentModCanBeSave()) + if (!CheckCurrentModCanBeSave()) return false; Preset.PerformWrite(s => @@ -123,7 +123,7 @@ namespace osu.Game.Overlays.Mods return true; } - private bool checkCurrentModCanBeSave() => (!Active.Value && selectedMods.Value.Any()); + public bool CheckCurrentModCanBeSave() => (!Active.Value && selectedMods.Value.Any()); protected override void Dispose(bool isDisposing) { From 15f11bb1e8ff7ea4d5ed8defa45ea0069fbd303f Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Sat, 11 Mar 2023 12:31:33 +0900 Subject: [PATCH 23/80] scorll container and save mod after popover hidden Requires manual handling of many visual effects --- .../UserInterface/TestSceneModPresetColumn.cs | 12 ++- osu.Game/Overlays/Mods/EditPresetPopover.cs | 76 +++++++++++++++++-- osu.Game/Overlays/Mods/ModPresetPanel.cs | 12 ++- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 13 ++-- 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index eaa3664623..68efb2a5f4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -353,7 +353,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddWaitStep("wait some", 3); AddUntilStep("popover not closed", () => this.ChildrenOfType().Any()); AddAssert("present mod not changed", () => - !new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); + !new HashSet(this.ChildrenOfType().First().Mods.Value).SetEquals(mods)); AddStep("select mods", () => SelectedMods.Value = mods); AddStep("right click first panel", () => @@ -378,8 +378,16 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); AddWaitStep("wait some", 3); - AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); + AddUntilStep("popover not closed", () => this.ChildrenOfType().Any()); AddAssert("present mod is changed", () => + new HashSet(this.ChildrenOfType().First().Mods.Value).SetEquals(mods)); + + AddStep("click edit", () => + { + InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(1)); + InputManager.Click(MouseButton.Left); + }); + AddAssert("present mod in realm is changed", () => new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); } diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index a5c562914b..92860e96a6 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -1,12 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Extensions; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; @@ -23,8 +27,19 @@ namespace osu.Game.Overlays.Mods private readonly LabelledTextBox descriptionTextBox; private readonly ShearedButton useCurrentModButton; private readonly ShearedButton createButton; + private readonly FillFlowContainer scrollContent; private readonly ModPreset preset; + private List saveModAfterClosed = new List(); + + [Resolved] + private Bindable> selectedMods { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; public EditPresetPopover(ModPresetPanel modPresetPanel) { @@ -53,6 +68,19 @@ namespace osu.Game.Overlays.Mods Label = CommonStrings.Description, TabbableContentContainer = this }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.X, + Height = 100, + Padding = new MarginPadding(7), + Child = scrollContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(7), + Spacing = new Vector2(7), + } + }, new FillFlowContainer { Anchor = Anchor.TopCentre, @@ -83,7 +111,7 @@ namespace osu.Game.Overlays.Mods } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuColour colours) + private void load() { Body.BorderThickness = 3; Body.BorderColour = colours.Orange1; @@ -95,17 +123,40 @@ namespace osu.Game.Overlays.Mods createButton.LighterColour = colours.Orange0; createButton.TextColour = colourProvider.Background6; - useCurrentModButton.DarkerColour = colours.Blue1; - useCurrentModButton.LighterColour = colours.Blue0; - useCurrentModButton.TextColour = colourProvider.Background6; + selectedMods.BindValueChanged(_ => updateActiveState(), true); + + scrollContent.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); } private void trySaveCurrentMod() { - if (button.SaveCurrentMod()) + if (!button.CheckCurrentModCanBeSave()) + { + Body.Shake(); return; + } - Body.Shake(); + saveModAfterClosed = selectedMods.Value.ToList(); + scrollContent.Clear(); + scrollContent.ChildrenEnumerable = saveModAfterClosed.Select(mod => new ModPresetRow(mod)); + button.Mods.Value = saveModAfterClosed; + updateActiveState(); + } + + private void updateActiveState() + { + if (button.CheckCurrentModCanBeSave()) + { + useCurrentModButton.DarkerColour = colours.Blue1; + useCurrentModButton.LighterColour = colours.Blue0; + useCurrentModButton.TextColour = colourProvider.Background6; + } + else + { + useCurrentModButton.DarkerColour = colours.Blue3; + useCurrentModButton.LighterColour = colours.Blue4; + useCurrentModButton.TextColour = colourProvider.Background2; + } } protected override void LoadComplete() @@ -131,5 +182,18 @@ namespace osu.Game.Overlays.Mods this.HidePopover(); } + + protected override void UpdateState(ValueChangedEvent state) + { + base.UpdateState(state); + + if (state.NewValue == Visibility.Hidden && saveModAfterClosed.Any()) + { + button.Preset.PerformWrite(s => + { + s.Mods = saveModAfterClosed; + }); + } + } } } diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index d01981d18c..49437c5bb1 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -18,10 +18,12 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public partial class ModPresetPanel : ModSelectPanel, IHasCustomTooltip, IHasContextMenu, IHasPopover + public partial class ModPresetPanel : ModSelectPanel, IHasCustomTooltip>, IHasContextMenu, IHasPopover { public readonly Live Preset; + public readonly Bindable> Mods = new Bindable>(); + public override BindableBool Active { get; } = new BindableBool(); [Resolved] @@ -35,6 +37,7 @@ namespace osu.Game.Overlays.Mods public ModPresetPanel(Live preset) { Preset = preset; + Mods.Value = preset.Value.Mods.ToList(); Title = preset.Value.Name; Description = preset.Value.Description; @@ -51,6 +54,7 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); selectedMods.BindValueChanged(_ => selectedModsChanged(), true); + Mods.BindValueChanged(_ => updateActiveState(), true); } protected override void Select() @@ -78,13 +82,13 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { - Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value); + Active.Value = new HashSet(Mods.Value).SetEquals(selectedMods.Value); } #region IHasCustomTooltip - public ModPreset TooltipContent => Preset.Value; - public ITooltip GetCustomTooltip() => new ModPresetTooltip(ColourProvider); + public List TooltipContent => Mods.Value; + public ITooltip> GetCustomTooltip() => new ModPresetTooltip(ColourProvider); #endregion diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 8e8259de45..0b31a97064 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,7 +12,7 @@ using osuTK; namespace osu.Game.Overlays.Mods { - public partial class ModPresetTooltip : VisibilityContainer, ITooltip + public partial class ModPresetTooltip : VisibilityContainer, ITooltip> { protected override Container Content { get; } @@ -42,15 +43,15 @@ namespace osu.Game.Overlays.Mods }; } - private ModPreset? lastPreset; + private List? lastPreset; - public void SetContent(ModPreset preset) + public void SetContent(List mods) { - if (ReferenceEquals(preset, lastPreset)) + if (ReferenceEquals(mods, lastPreset)) return; - lastPreset = preset; - Content.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); + lastPreset = mods; + Content.ChildrenEnumerable = mods.Select(mod => new ModPresetRow(mod)); } protected override void PopIn() => this.FadeIn(transition_duration, Easing.OutQuint); From 8e8dda3ac0ad1d49043198681fcac2b092c9cdda Mon Sep 17 00:00:00 2001 From: Terochi Date: Sat, 11 Mar 2023 23:29:36 +0100 Subject: [PATCH 24/80] Big simplifying --- osu.Game.Tests/Mods/ModSettingsTest.cs | 83 +++---------------- .../TestSceneModSelectOverlay.cs | 42 ---------- osu.Game/Rulesets/Mods/Mod.cs | 61 ++------------ 3 files changed, 19 insertions(+), 167 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index e2e050870e..d943f0ffb1 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -1,7 +1,6 @@ // 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 NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Localisation; @@ -42,16 +41,20 @@ namespace osu.Game.Tests.Mods { const double setting_change = 50.4; - var modDouble = new TestNonMatchingSettingTypeModDouble(); - var modBool = new TestNonMatchingSettingTypeModBool(); + var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } }; + var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change } }; - modDouble.TestSetting.Value = setting_change; - modBool.TestSetting.Value = !modBool.TestSetting.Default; modDouble.CopySharedSettings(modBool); + modDouble.CopySharedSettings(modInt); modBool.CopySharedSettings(modDouble); + modBool.CopySharedSettings(modInt); + modInt.CopySharedSettings(modDouble); + modInt.CopySharedSettings(modBool); Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); + Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change)); } [Test] @@ -70,75 +73,11 @@ namespace osu.Game.Tests.Mods public void TestValueResetsToDefaultWhenCopied() { var modDouble = new TestNonMatchingSettingTypeModDouble(); - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = 1 } }; + var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - modInt.CopySharedSettings(modDouble); + modBool.CopySharedSettings(modDouble); - Assert.That(modInt.TestSetting.Value, Is.EqualTo(modInt.TestSetting.Default)); - } - - [Test] - public void TestRelativelyScaleWithClampedRangeWhenCopied() - { - const double setting_change = 50.4; - - var modDouble100 = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 100, MinValue = 0, Value = setting_change } }; - var modDouble200 = new TestNonMatchingSettingTypeModDouble { TestSetting = { MaxValue = 200, MinValue = 0 } }; - - modDouble200.CopySharedSettings(modDouble100); - - Assert.That(modDouble200.TestSetting.Value, Is.EqualTo(setting_change * 2)); - } - - [Test] - public void TestCopyDoubleToIntWithDefaultRange() - { - const double setting_change = 50.4; - - var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } }; - var modInt = new TestNonMatchingSettingTypeModInt(); - - modInt.CopySharedSettings(modDouble); - - Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change))); - } - - [Test] - public void TestCopyDoubleToIntWithOutOfBoundsRange() - { - const double setting_change = 50.4; - - var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MinValue = int.MinValue - 1d, Value = setting_change } }; - // make RangeConstrainedBindable.HasDefinedRange return true - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MinValue = int.MinValue + 1 } }; - - modInt.CopySharedSettings(modDouble); - - Assert.That(modInt.TestSetting.Value, Is.EqualTo(Convert.ToInt32(setting_change))); - } - - [Test] - public void TestCopyDoubleToIntWithOutOfBoundsValue() - { - var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { MinValue = int.MinValue + 1, Value = int.MaxValue + 1d } }; - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { MinValue = int.MinValue + 1 } }; - - modInt.CopySharedSettings(modDouble); - - Assert.That(modInt.TestSetting.Value, Is.EqualTo(int.MaxValue)); - } - - [Test] - public void TestCopyIntToDoubleWithDefaultRange() - { - const int setting_change = 50; - - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = setting_change } }; - var modDouble = new TestNonMatchingSettingTypeModDouble(); - - modDouble.CopySharedSettings(modInt); - - Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); + Assert.That(modBool.TestSetting.Value, Is.EqualTo(modBool.TestSetting.Default)); } private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 75efc07d43..b4b5052f23 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -18,7 +18,6 @@ using osu.Game.Overlays.Mods; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch.Mods; -using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -417,47 +416,6 @@ namespace osu.Game.Tests.Visual.UserInterface }); } - [Test] - public void TestKeepSharedSettingsRatio() - { - const float setting_change = 1.8f; - - createScreen(); - changeRuleset(0); - - AddStep("select flashlight mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod()! }); - - changeRuleset(0); - AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModFlashlight); - - AddStep("change mod settings", () => - { - var osuMod = getMod(); - - // range <0.5 - 2> - osuMod.SizeMultiplier.Value = setting_change; - }); - - changeRuleset(3); - AddAssert("mania variant selected", () => SelectedMods.Value.SingleOrDefault() is ManiaModFlashlight); - - AddAssert("shared settings changed to closest ratio", () => - { - var maniaMod = getMod(); - - // range <0.5 - 3> - // converted value based on ratio = (setting_change - 0.5) / (2 - 0.5) * (3 - 0.5) + 0.5 = 2.66 - return maniaMod.SizeMultiplier.Value == 2.7f; // taking precision into account - }); - - AddAssert("other settings unchanged", () => - { - var maniaMod = getMod(); - - return maniaMod.ComboBasedSize.IsDefault; - }); - } - [Test] public void TestExternallySetCustomizedMod() { diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 85014d555d..623a734b82 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -165,8 +165,6 @@ namespace osu.Game.Rulesets.Mods /// The mod to copy properties from. internal void CopySharedSettings(Mod source) { - const string min_value = nameof(BindableNumber.MinValue); - const string max_value = nameof(BindableNumber.MaxValue); const string value = nameof(Bindable.Value); Dictionary sourceSettings = new Dictionary(); @@ -190,65 +188,22 @@ namespace osu.Game.Rulesets.Mods continue; } + bool hasSameGenericArgument = getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == + getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single(); + + if (!hasSameGenericArgument) + continue; + Type? targetBindableNumberType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); Type? sourceBindableNumberType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); if (targetBindableNumberType == null || sourceBindableNumberType == null) { - if (getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == - getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single()) - { - // change settings only if the type is the same - setValue(targetSetting, value, getValue(sourceSetting, value)); - } - + setValue(targetSetting, value, getValue(sourceSetting, value)); continue; } - bool rangeOutOfBounds = false; - - Type targetGenericType = targetBindableNumberType.GenericTypeArguments.Single(); - Type sourceGenericType = sourceBindableNumberType.GenericTypeArguments.Single(); - - if (!Convert.ToBoolean(getValue(targetSetting, nameof(RangeConstrainedBindable.HasDefinedRange))) || - !Convert.ToBoolean(getValue(sourceSetting, nameof(RangeConstrainedBindable.HasDefinedRange)))) - // check if we have a range to rescale from and a range to rescale to - // if not, copy the raw value - rangeOutOfBounds = true; - - double allowedMin = Math.Max( - Convert.ToDouble(targetGenericType.GetField("MinValue")!.GetValue(null)), - Convert.ToDouble(sourceGenericType.GetField("MinValue")!.GetValue(null)) - ); - - double allowedMax = Math.Min( - Convert.ToDouble(targetGenericType.GetField("MaxValue")!.GetValue(null)), - Convert.ToDouble(sourceGenericType.GetField("MaxValue")!.GetValue(null)) - ); - - double targetMin = getValueDouble(targetSetting, min_value); - double targetMax = getValueDouble(targetSetting, max_value); - double sourceMin = getValueDouble(sourceSetting, min_value); - double sourceMax = getValueDouble(sourceSetting, max_value); - double sourceValue = Math.Clamp(getValueDouble(sourceSetting, value), allowedMin, allowedMax); - - double targetValue = rangeOutOfBounds - // keep raw value - ? sourceValue - // convert value to same ratio - : (sourceValue - sourceMin) / (sourceMax - sourceMin) * (targetMax - targetMin) + targetMin; - - setValue(targetSetting, value, Convert.ChangeType(targetValue, targetBindableNumberType.GenericTypeArguments.Single())); - - double getValueDouble(object setting, string name) - { - double settingValue = Convert.ToDouble(getValue(setting, name)!); - - if (settingValue < allowedMin || settingValue > allowedMax) - rangeOutOfBounds = true; - - return settingValue; - } + setValue(targetSetting, value, Convert.ChangeType(getValue(sourceSetting, value), targetBindableNumberType.GenericTypeArguments.Single())); } object? getValue(object setting, string name) => From 42bcc8bafc01005ad91035dde8d80d027b3552f5 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 16 Mar 2023 19:15:50 +0900 Subject: [PATCH 25/80] revert mod store --- osu.Game/Overlays/Mods/ModPresetPanel.cs | 12 ++++-------- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 13 ++++++------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 49437c5bb1..d01981d18c 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -18,12 +18,10 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public partial class ModPresetPanel : ModSelectPanel, IHasCustomTooltip>, IHasContextMenu, IHasPopover + public partial class ModPresetPanel : ModSelectPanel, IHasCustomTooltip, IHasContextMenu, IHasPopover { public readonly Live Preset; - public readonly Bindable> Mods = new Bindable>(); - public override BindableBool Active { get; } = new BindableBool(); [Resolved] @@ -37,7 +35,6 @@ namespace osu.Game.Overlays.Mods public ModPresetPanel(Live preset) { Preset = preset; - Mods.Value = preset.Value.Mods.ToList(); Title = preset.Value.Name; Description = preset.Value.Description; @@ -54,7 +51,6 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); selectedMods.BindValueChanged(_ => selectedModsChanged(), true); - Mods.BindValueChanged(_ => updateActiveState(), true); } protected override void Select() @@ -82,13 +78,13 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { - Active.Value = new HashSet(Mods.Value).SetEquals(selectedMods.Value); + Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value); } #region IHasCustomTooltip - public List TooltipContent => Mods.Value; - public ITooltip> GetCustomTooltip() => new ModPresetTooltip(ColourProvider); + public ModPreset TooltipContent => Preset.Value; + public ITooltip GetCustomTooltip() => new ModPresetTooltip(ColourProvider); #endregion diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 0b31a97064..8e8259de45 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,7 +11,7 @@ using osuTK; namespace osu.Game.Overlays.Mods { - public partial class ModPresetTooltip : VisibilityContainer, ITooltip> + public partial class ModPresetTooltip : VisibilityContainer, ITooltip { protected override Container Content { get; } @@ -43,15 +42,15 @@ namespace osu.Game.Overlays.Mods }; } - private List? lastPreset; + private ModPreset? lastPreset; - public void SetContent(List mods) + public void SetContent(ModPreset preset) { - if (ReferenceEquals(mods, lastPreset)) + if (ReferenceEquals(preset, lastPreset)) return; - lastPreset = mods; - Content.ChildrenEnumerable = mods.Select(mod => new ModPresetRow(mod)); + lastPreset = preset; + Content.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); } protected override void PopIn() => this.FadeIn(transition_duration, Easing.OutQuint); From d025c441ca2249489b6d0c523d55b44ef1da6108 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 16 Mar 2023 19:38:52 +0900 Subject: [PATCH 26/80] delay mod save after click save or not popover hidden --- .../UserInterface/TestSceneModPresetColumn.cs | 20 ++++++----- osu.Game/Overlays/Mods/EditPresetPopover.cs | 36 ++++++++++--------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 68efb2a5f4..c4c5584c5e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -319,6 +319,7 @@ namespace osu.Game.Tests.Visual.UserInterface { ModPresetColumn modPresetColumn = null!; var mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }; + List previousMod = null!; AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn @@ -332,6 +333,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("right click first panel", () => { var panel = this.ChildrenOfType().First(); + previousMod = panel.Preset.Value.Mods.ToList(); InputManager.MoveMouseTo(panel); InputManager.Click(MouseButton.Right); }); @@ -350,10 +352,14 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(0)); InputManager.Click(MouseButton.Left); }); + AddStep("attempt preset edit", () => + { + InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(1)); + InputManager.Click(MouseButton.Left); + }); AddWaitStep("wait some", 3); - AddUntilStep("popover not closed", () => this.ChildrenOfType().Any()); AddAssert("present mod not changed", () => - !new HashSet(this.ChildrenOfType().First().Mods.Value).SetEquals(mods)); + new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(previousMod)); AddStep("select mods", () => SelectedMods.Value = mods); AddStep("right click first panel", () => @@ -377,17 +383,13 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(0)); InputManager.Click(MouseButton.Left); }); - AddWaitStep("wait some", 3); - AddUntilStep("popover not closed", () => this.ChildrenOfType().Any()); - AddAssert("present mod is changed", () => - new HashSet(this.ChildrenOfType().First().Mods.Value).SetEquals(mods)); - - AddStep("click edit", () => + AddStep("attempt preset edit", () => { InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Left); }); - AddAssert("present mod in realm is changed", () => + AddWaitStep("wait some", 3); + AddAssert("present mod is changed", () => new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); } diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 92860e96a6..0e21260493 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -130,7 +130,7 @@ namespace osu.Game.Overlays.Mods private void trySaveCurrentMod() { - if (!button.CheckCurrentModCanBeSave()) + if (!checkCanBeSave()) { Body.Shake(); return; @@ -139,13 +139,12 @@ namespace osu.Game.Overlays.Mods saveModAfterClosed = selectedMods.Value.ToList(); scrollContent.Clear(); scrollContent.ChildrenEnumerable = saveModAfterClosed.Select(mod => new ModPresetRow(mod)); - button.Mods.Value = saveModAfterClosed; updateActiveState(); } private void updateActiveState() { - if (button.CheckCurrentModCanBeSave()) + if (checkCanBeSave()) { useCurrentModButton.DarkerColour = colours.Blue1; useCurrentModButton.LighterColour = colours.Blue0; @@ -159,6 +158,19 @@ namespace osu.Game.Overlays.Mods } } + private bool checkCanBeSave() + { + if (!selectedMods.Value.Any()) + return false; + + if (saveModAfterClosed.Any()) + { + return !new HashSet(saveModAfterClosed).SetEquals(selectedMods.Value); + } + + return button.CheckCurrentModCanBeSave(); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -178,22 +190,14 @@ namespace osu.Game.Overlays.Mods { s.Name = nameTextBox.Current.Value; s.Description = descriptionTextBox.Current.Value; + + if (saveModAfterClosed.Any()) + { + s.Mods = saveModAfterClosed; + } }); this.HidePopover(); } - - protected override void UpdateState(ValueChangedEvent state) - { - base.UpdateState(state); - - if (state.NewValue == Visibility.Hidden && saveModAfterClosed.Any()) - { - button.Preset.PerformWrite(s => - { - s.Mods = saveModAfterClosed; - }); - } - } } } From 8518d15b8daf8cd2cbb047054e8b2286c0f63493 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 23 Mar 2023 00:13:24 +0900 Subject: [PATCH 27/80] use endpoint --- .../Visual/Online/TestSceneChatOverlay.cs | 33 +++++++++++++++- .../Online/API/Requests/ChatReportRequest.cs | 38 +++++++++++++++++++ osu.Game/Overlays/Chat/ChatLine.cs | 2 +- osu.Game/Overlays/Chat/DrawableUsername.cs | 24 ++++++++++-- 4 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Online/API/Requests/ChatReportRequest.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 48db5691ce..08337fbff9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Collections.Generic; using System.Net; using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; @@ -32,6 +33,7 @@ using osu.Game.Overlays.Chat.ChannelList; using osuTK; using osuTK.Input; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays.Comments; namespace osu.Game.Tests.Visual.Online { @@ -55,6 +57,9 @@ namespace osu.Game.Tests.Visual.Online private int currentMessageId; + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + private readonly ManualResetEventSlim requestLock = new ManualResetEventSlim(); + [SetUp] public void SetUp() => Schedule(() => { @@ -581,6 +586,8 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestChatReport() { + ChatReportRequest request = null; + AddStep("Show overlay with channel", () => { chatOverlay.Show(); @@ -590,6 +597,26 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); waitForChannel1Visible(); + AddStep("Setup request handling", () => + { + requestLock.Reset(); + + dummyAPI.HandleRequest = r => + { + if (!(r is ChatReportRequest req)) + return false; + + Task.Run(() => + { + request = req; + requestLock.Wait(10000); + req.TriggerSuccess(); + }); + + return true; + }; + }); + AddStep("Show report popover", () => this.ChildrenOfType().First().ShowPopover()); AddStep("Try to report", () => @@ -599,7 +626,11 @@ namespace osu.Game.Tests.Visual.Online InputManager.Click(MouseButton.Left); }); - AddAssert("Report message sended", () => channelManager.CurrentChannel.Value.Messages.Any(x => x.Content.Contains("!report"))); + AddWaitStep("Wait", 3); + + AddAssert("Overlay closed", () => !this.ChildrenOfType().Any()); + AddStep("Complete request", () => requestLock.Set()); + AddUntilStep("Request sent", () => request != null); } private void joinTestChannel(int i) diff --git a/osu.Game/Online/API/Requests/ChatReportRequest.cs b/osu.Game/Online/API/Requests/ChatReportRequest.cs new file mode 100644 index 0000000000..00d314be68 --- /dev/null +++ b/osu.Game/Online/API/Requests/ChatReportRequest.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Online.Chat; +using osu.Game.Overlays.Chat; + +namespace osu.Game.Online.API.Requests; + +public class ChatReportRequest : APIRequest +{ + public readonly long? MessageId; + public readonly ChatReportReason Reason; + public readonly string Comment; + + public ChatReportRequest(long? id, ChatReportReason reason, string comment) + { + MessageId = id; + Reason = reason; + Comment = comment; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + + req.AddParameter(@"reportable_type", @"message"); + req.AddParameter(@"reportable_id", $"{MessageId}"); + req.AddParameter(@"reason", Reason.ToString()); + req.AddParameter(@"comments", Comment); + + return req; + } + + protected override string Target => @"reports"; +} diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 70c3bf181c..0e69501ae9 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Chat Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), AlwaysPresent = true, }, - drawableUsername = new DrawableUsername(message.Sender) + drawableUsername = new DrawableUsername(message) { Width = UsernameWidth, FontSize = FontSize, diff --git a/osu.Game/Overlays/Chat/DrawableUsername.cs b/osu.Game/Overlays/Chat/DrawableUsername.cs index 640ce2eca6..c9b0b018e3 100644 --- a/osu.Game/Overlays/Chat/DrawableUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableUsername.cs @@ -21,6 +21,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; @@ -72,13 +73,15 @@ namespace osu.Game.Overlays.Chat private Bindable? currentChannel { get; set; } private readonly APIUser user; + private readonly Message message; private readonly OsuSpriteText drawableText; private readonly Drawable colouredDrawable; - public DrawableUsername(APIUser user) + public DrawableUsername(Message message) { - this.user = user; + this.message = message; + user = message.Sender; Action = openUserProfile; @@ -171,7 +174,8 @@ namespace osu.Game.Overlays.Chat })); } - items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, this.ShowPopover)); + if (!user.Equals(api.LocalUser.Value)) + items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, this.ShowPopover)); return items.ToArray(); } @@ -179,7 +183,19 @@ namespace osu.Game.Overlays.Chat private void report(ChatReportReason reason, string comments) { - chatManager?.PostMessage($"!report {user.Username} ({reason.GetDescription()}): {comments}"); + var request = new ChatReportRequest(message.Id, reason, comments); + + request.Failure += _ => Schedule(() => + { + currentChannel?.Value?.AddNewMessages(new ErrorMessage("Report failed to send, please retry")); + }); + + request.Success += () => Schedule(() => + { + currentChannel?.Value?.AddNewMessages(new InfoMessage("Report has been sent")); + }); + + api.Queue(request); } private void openUserChannel() From 85a924f0782ee04f48aae86224f09343f7b37979 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 23 Mar 2023 01:02:17 +0900 Subject: [PATCH 28/80] why merge problem? --- osu.Game/Overlays/Chat/DrawableUsername.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableUsername.cs b/osu.Game/Overlays/Chat/DrawableUsername.cs index c9b0b018e3..25967003e9 100644 --- a/osu.Game/Overlays/Chat/DrawableUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableUsername.cs @@ -163,8 +163,7 @@ namespace osu.Game.Overlays.Chat }; if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem("Start Chat", MenuItemType.Standard, openUserChannel)); - items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); + items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); if (currentChannel?.Value != null) { From 26f2d9047d3238b4790613102e2d0500a18d69d9 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 23 Mar 2023 01:11:44 +0900 Subject: [PATCH 29/80] code style fix --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 1 - osu.Game/Online/API/Requests/ChatReportRequest.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 08337fbff9..5845ba3783 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -33,7 +33,6 @@ using osu.Game.Overlays.Chat.ChannelList; using osuTK; using osuTK.Input; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Overlays.Comments; namespace osu.Game.Tests.Visual.Online { diff --git a/osu.Game/Online/API/Requests/ChatReportRequest.cs b/osu.Game/Online/API/Requests/ChatReportRequest.cs index 00d314be68..01cb455183 100644 --- a/osu.Game/Online/API/Requests/ChatReportRequest.cs +++ b/osu.Game/Online/API/Requests/ChatReportRequest.cs @@ -3,7 +3,6 @@ using System.Net.Http; using osu.Framework.IO.Network; -using osu.Game.Online.Chat; using osu.Game.Overlays.Chat; namespace osu.Game.Online.API.Requests; From b065689cf89b0771c7d117d67c36f9400942d3d7 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Mon, 27 Mar 2023 23:04:45 +0900 Subject: [PATCH 30/80] Limit the area of popover to DrawableChannel popover may be blocked by textBar --- osu.Game/Overlays/ChatOverlay.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 332ffa110a..96dbfe31f3 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -123,7 +123,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Y, Width = side_bar_width, }, - new PopoverContainer + new Container { RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopRight, @@ -135,9 +135,13 @@ namespace osu.Game.Overlays }, Children = new Drawable[] { - currentChannelContainer = new Container + new PopoverContainer { RelativeSizeAxes = Axes.Both, + Child = currentChannelContainer = new Container + { + RelativeSizeAxes = Axes.Both, + } }, loading = new LoadingLayer(true), channelListing = new ChannelListing From 31df626f0ebe022884f2b5bd2bce49898661ec1d Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Tue, 28 Mar 2023 21:32:28 +0900 Subject: [PATCH 31/80] disable button when select other reason --- .../Graphics/UserInterfaceV2/ReportPopover.cs | 24 ++++++++++++------- osu.Game/Overlays/Chat/ReportChatPopover.cs | 6 ++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs index 44af108ab1..46006402ad 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs @@ -24,8 +24,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 private OsuEnumDropdown reasonDropdown = null!; private OsuTextBox commentsTextBox = null!; private RoundedButton submitButton = null!; - - public bool CanSubmitEmptyReason = false; public LocalisableString Header; protected ReportPopover(LocalisableString headerString) @@ -102,13 +100,21 @@ namespace osu.Game.Graphics.UserInterfaceV2 } }; - if (!CanSubmitEmptyReason) - { - commentsTextBox.Current.BindValueChanged(e => - { - submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(e.NewValue); - }, true); - } + commentsTextBox.Current.BindValueChanged(_ => updateStatus()); + + reasonDropdown.Current.BindValueChanged(_ => updateStatus()); + + updateStatus(); + } + + private void updateStatus() + { + submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(commentsTextBox.Current.Value) || CheckCanSubmitEmptyComment(reasonDropdown.Current.Value); + } + + protected virtual bool CheckCanSubmitEmptyComment(T reason) + { + return false; } } } diff --git a/osu.Game/Overlays/Chat/ReportChatPopover.cs b/osu.Game/Overlays/Chat/ReportChatPopover.cs index 79c3922795..de5f3268b8 100644 --- a/osu.Game/Overlays/Chat/ReportChatPopover.cs +++ b/osu.Game/Overlays/Chat/ReportChatPopover.cs @@ -12,7 +12,11 @@ namespace osu.Game.Overlays.Chat public ReportChatPopover(APIUser? user) : base(ReportStrings.UserTitle(user?.Username ?? @"Someone")) { - CanSubmitEmptyReason = true; + } + + protected override bool CheckCanSubmitEmptyComment(ChatReportReason reason) + { + return reason != ChatReportReason.Other; } } } From f01247f1ab75dfd7f9dc81200a569b3f114b4296 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Tue, 28 Mar 2023 21:39:42 +0900 Subject: [PATCH 32/80] test --- .../Visual/Online/TestSceneChatOverlay.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 5845ba3783..faf48ef854 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -618,6 +618,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Show report popover", () => this.ChildrenOfType().First().ShowPopover()); + AddStep("Set report reason to other", () => + { + var reason = this.ChildrenOfType>().Single(); + reason.Current.Value = ChatReportReason.Other; + }); + AddStep("Try to report", () => { var btn = this.ChildrenOfType().Single().ChildrenOfType().Single(); @@ -626,7 +632,21 @@ namespace osu.Game.Tests.Visual.Online }); AddWaitStep("Wait", 3); + AddAssert("Nothing happened", () => this.ChildrenOfType().Any()); + AddStep("Set report data", () => + { + var field = this.ChildrenOfType().Single().ChildrenOfType().Single(); + field.Current.Value = "test other"; + }); + AddStep("Try to report", () => + { + var btn = this.ChildrenOfType().Single().ChildrenOfType().Single(); + InputManager.MoveMouseTo(btn); + InputManager.Click(MouseButton.Left); + }); + + AddWaitStep("Wait", 3); AddAssert("Overlay closed", () => !this.ChildrenOfType().Any()); AddStep("Complete request", () => requestLock.Set()); AddUntilStep("Request sent", () => request != null); From 0841e73a39ba4c383a62e8897ed55b653f66be1a Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 25 Apr 2023 21:05:40 +0200 Subject: [PATCH 33/80] Improved readability and sounds --- osu.Game.Tests/Mods/ModSettingsTest.cs | 29 +++----- osu.Game/OsuGameBase.cs | 35 ++++++---- osu.Game/Rulesets/Mods/Mod.cs | 95 +++++++++++--------------- 3 files changed, 69 insertions(+), 90 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index d943f0ffb1..b3f6f8da7d 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -43,18 +43,18 @@ namespace osu.Game.Tests.Mods var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } }; var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change } }; + var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change / 2 } }; - modDouble.CopySharedSettings(modBool); - modDouble.CopySharedSettings(modInt); - modBool.CopySharedSettings(modDouble); - modBool.CopySharedSettings(modInt); - modInt.CopySharedSettings(modDouble); - modInt.CopySharedSettings(modBool); + modDouble.CopyCommonSettings(modBool); + modDouble.CopyCommonSettings(modInt); + modBool.CopyCommonSettings(modDouble); + modBool.CopyCommonSettings(modInt); + modInt.CopyCommonSettings(modDouble); + modInt.CopyCommonSettings(modBool); Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); - Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change)); + Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change / 2)); } [Test] @@ -63,23 +63,12 @@ namespace osu.Game.Tests.Mods var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } }; var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - modBoolFalse.CopySharedSettings(modBoolTrue); + modBoolFalse.CopyCommonSettings(modBoolTrue); Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false)); Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value)); } - [Test] - public void TestValueResetsToDefaultWhenCopied() - { - var modDouble = new TestNonMatchingSettingTypeModDouble(); - var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - - modBool.CopySharedSettings(modDouble); - - Assert.That(modBool.TestSetting.Value, Is.EqualTo(modBool.TestSetting.Default)); - } - private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod { public override string Acronym => "NMD"; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index de1f2e810c..f6cda62cc3 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -58,7 +58,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Utils; -using File = System.IO.File; using RuntimeInfo = osu.Framework.RuntimeInfo; namespace osu.Game @@ -71,7 +70,7 @@ namespace osu.Game [Cached(typeof(OsuGameBase))] public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider { - public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" }; + public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" }; public const string OSU_PROTOCOL = "osu://"; @@ -626,20 +625,30 @@ namespace osu.Game return; } + var previouslySelectedCommonMods = new List(SelectedMods.Value.Count); + var convertedCommonMods = new List(SelectedMods.Value.Count); + + foreach (var mod in SelectedMods.Value) + { + var convertedMod = instance.CreateModFromAcronym(mod.Acronym); + + if (convertedMod == null) + continue; + + previouslySelectedCommonMods.Add(mod); + + convertedMod.CopyCommonSettings(mod); + convertedCommonMods.Add(convertedMod); + } + + if (!SelectedMods.Disabled) + // Select common mods to play the deselect samples for other mods + SelectedMods.Value = previouslySelectedCommonMods; + AvailableMods.Value = dict; if (!SelectedMods.Disabled) - { - //converting mods from one ruleset to the other, while also keeping their shared settings unchanged - SelectedMods.Value = SelectedMods.Value.Select(oldMod => - { - Mod newMod = instance.CreateModFromAcronym(oldMod.Acronym); - - newMod?.CopySharedSettings(oldMod); - - return newMod; - }).Where(m => m != null).ToArray(); - } + SelectedMods.Value = convertedCommonMods; void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First(); } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 623a734b82..97434ce493 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -115,21 +115,25 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual Type[] IncompatibleMods => Array.Empty(); - private IReadOnlyList? settingsBacking; + private IReadOnlyDictionary? settingsBacking; /// - /// A list of the all settings within this mod. + /// All settings within this mod. /// - internal IReadOnlyList Settings => + internal IEnumerable SettingsBindables => Settings.Values; + + /// + /// Provides mapping of names to s of all settings within this mod. + /// + internal IReadOnlyDictionary Settings => settingsBacking ??= this.GetSettingsSourceProperties() - .Select(p => p.Item2.GetValue(this)) - .Cast() - .ToList(); + .Select(p => p.Item2) + .ToDictionary(property => property.Name.ToSnakeCase(), property => (IBindable)property.GetValue(this)!); /// /// Whether all settings in this mod are set to their default state. /// - protected virtual bool UsesDefaultConfiguration => Settings.All(s => s.IsDefault); + protected virtual bool UsesDefaultConfiguration => SettingsBindables.All(s => s.IsDefault); /// /// Creates a copy of this initialised to a default state. @@ -160,60 +164,37 @@ namespace osu.Game.Rulesets.Mods } /// - /// Copies all mod setting values sharing same from into this instance. + /// When converting mods from one ruleset to the other, this method makes sure + /// to also copy the values of all settings sharing same between the two instances. /// - /// The mod to copy properties from. - internal void CopySharedSettings(Mod source) + /// Copied values are unchanged, even if they have different clamping ranges. + /// The mod to extract settings from. + public void CopyCommonSettings(Mod source) { - const string value = nameof(Bindable.Value); + if (source.UsesDefaultConfiguration) + return; - Dictionary sourceSettings = new Dictionary(); - - foreach (var (_, sourceProperty) in source.GetSettingsSourceProperties()) + foreach (var (name, targetSetting) in Settings) { - sourceSettings.Add(sourceProperty.Name.ToSnakeCase(), sourceProperty.GetValue(source)!); + if (!source.Settings.TryGetValue(name, out IBindable? sourceSetting)) + continue; + + if (sourceSetting.IsDefault) + continue; + + if (getBindableGenericType(targetSetting) != getBindableGenericType(sourceSetting)) + continue; + + // TODO: special case for handling number types + + PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable.Value))!; + property.SetValue(targetSetting, property.GetValue(sourceSetting)); } - foreach (var (_, targetProperty) in this.GetSettingsSourceProperties()) - { - object targetSetting = targetProperty.GetValue(this)!; - - if (!sourceSettings.TryGetValue(targetProperty.Name.ToSnakeCase(), out object? sourceSetting)) - continue; - - if (((IBindable)sourceSetting).IsDefault) - { - // reset to default value if the source is default - targetSetting.GetType().GetMethod(nameof(Bindable.SetDefault))!.Invoke(targetSetting, null); - continue; - } - - bool hasSameGenericArgument = getGenericBaseType(targetSetting, typeof(Bindable<>))!.GenericTypeArguments.Single() == - getGenericBaseType(sourceSetting, typeof(Bindable<>))!.GenericTypeArguments.Single(); - - if (!hasSameGenericArgument) - continue; - - Type? targetBindableNumberType = getGenericBaseType(targetSetting, typeof(BindableNumber<>)); - Type? sourceBindableNumberType = getGenericBaseType(sourceSetting, typeof(BindableNumber<>)); - - if (targetBindableNumberType == null || sourceBindableNumberType == null) - { - setValue(targetSetting, value, getValue(sourceSetting, value)); - continue; - } - - setValue(targetSetting, value, Convert.ChangeType(getValue(sourceSetting, value), targetBindableNumberType.GenericTypeArguments.Single())); - } - - object? getValue(object setting, string name) => - setting.GetType().GetProperty(name)!.GetValue(setting); - - void setValue(object setting, string name, object? newValue) => - setting.GetType().GetProperty(name)!.SetValue(setting, newValue); - - Type? getGenericBaseType(object setting, Type genericType) => - setting.GetType().GetTypeInheritance().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType); + Type getBindableGenericType(IBindable setting) => + setting.GetType().GetTypeInheritance() + .First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Bindable<>)) + .GenericTypeArguments.Single(); } /// @@ -250,7 +231,7 @@ namespace osu.Game.Rulesets.Mods if (ReferenceEquals(this, other)) return true; return GetType() == other.GetType() && - Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default); + SettingsBindables.SequenceEqual(other.SettingsBindables, ModSettingsEqualityComparer.Default); } public override int GetHashCode() @@ -259,7 +240,7 @@ namespace osu.Game.Rulesets.Mods hashCode.Add(GetType()); - foreach (var setting in Settings) + foreach (var setting in SettingsBindables) hashCode.Add(setting.GetUnderlyingSettingValue()); return hashCode.ToHashCode(); From 8e297dc60a18a9d7057d4b7525ec8cd6189bb504 Mon Sep 17 00:00:00 2001 From: Terochi Date: Tue, 25 Apr 2023 21:28:03 +0200 Subject: [PATCH 34/80] Added safety measures for invalid mod combinations --- osu.Game/OsuGameBase.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index f6cda62cc3..0473f2665b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -641,6 +641,16 @@ namespace osu.Game convertedCommonMods.Add(convertedMod); } + if (!ModUtils.CheckValidForGameplay(convertedCommonMods, out var invalid)) + { + invalid.ForEach(mod => + { + int index = convertedCommonMods.IndexOf(mod); + convertedCommonMods.RemoveAt(index); + previouslySelectedCommonMods.RemoveAt(index); + }); + } + if (!SelectedMods.Disabled) // Select common mods to play the deselect samples for other mods SelectedMods.Value = previouslySelectedCommonMods; From 332c199dc2b0cdabe76e56dc8696d6ee52f58fa9 Mon Sep 17 00:00:00 2001 From: Terochi Date: Wed, 26 Apr 2023 10:46:29 +0200 Subject: [PATCH 35/80] further simplification --- osu.Game/Rulesets/Mods/Mod.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 97434ce493..0af8717264 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using AutoMapper.Internal; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; @@ -182,7 +181,10 @@ namespace osu.Game.Rulesets.Mods if (sourceSetting.IsDefault) continue; - if (getBindableGenericType(targetSetting) != getBindableGenericType(sourceSetting)) + var targetType = targetSetting.GetType(); + var sourceType = sourceSetting.GetType(); + + if (!targetType.IsAssignableFrom(sourceType) && !sourceType.IsAssignableFrom(targetType)) continue; // TODO: special case for handling number types @@ -190,11 +192,6 @@ namespace osu.Game.Rulesets.Mods PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable.Value))!; property.SetValue(targetSetting, property.GetValue(sourceSetting)); } - - Type getBindableGenericType(IBindable setting) => - setting.GetType().GetTypeInheritance() - .First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Bindable<>)) - .GenericTypeArguments.Single(); } /// From b28d7b1a928cbda8ee6da8a682683ea9dbd7376f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 May 2023 18:51:02 +0900 Subject: [PATCH 36/80] Fix non-block namespace --- .../Online/API/Requests/ChatReportRequest.cs | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/osu.Game/Online/API/Requests/ChatReportRequest.cs b/osu.Game/Online/API/Requests/ChatReportRequest.cs index 01cb455183..85e5559e01 100644 --- a/osu.Game/Online/API/Requests/ChatReportRequest.cs +++ b/osu.Game/Online/API/Requests/ChatReportRequest.cs @@ -5,33 +5,34 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Overlays.Chat; -namespace osu.Game.Online.API.Requests; - -public class ChatReportRequest : APIRequest +namespace osu.Game.Online.API.Requests { - public readonly long? MessageId; - public readonly ChatReportReason Reason; - public readonly string Comment; - - public ChatReportRequest(long? id, ChatReportReason reason, string comment) + public class ChatReportRequest : APIRequest { - MessageId = id; - Reason = reason; - Comment = comment; + public readonly long? MessageId; + public readonly ChatReportReason Reason; + public readonly string Comment; + + public ChatReportRequest(long? id, ChatReportReason reason, string comment) + { + MessageId = id; + Reason = reason; + Comment = comment; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + + req.AddParameter(@"reportable_type", @"message"); + req.AddParameter(@"reportable_id", $"{MessageId}"); + req.AddParameter(@"reason", Reason.ToString()); + req.AddParameter(@"comments", Comment); + + return req; + } + + protected override string Target => @"reports"; } - - protected override WebRequest CreateWebRequest() - { - var req = base.CreateWebRequest(); - req.Method = HttpMethod.Post; - - req.AddParameter(@"reportable_type", @"message"); - req.AddParameter(@"reportable_id", $"{MessageId}"); - req.AddParameter(@"reason", Reason.ToString()); - req.AddParameter(@"comments", Comment); - - return req; - } - - protected override string Target => @"reports"; } From b932e4d98690f9935aa1d50df4dd8757f9380928 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 May 2023 18:53:22 +0900 Subject: [PATCH 37/80] Rename `DrawableUsername` to `DrawableChatUsername` and only pass message ID --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 2 +- osu.Game/Overlays/Chat/ChatLine.cs | 4 ++-- .../{DrawableUsername.cs => DrawableChatUsername.cs} | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) rename osu.Game/Overlays/Chat/{DrawableUsername.cs => DrawableChatUsername.cs} (96%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index faf48ef854..e9e2cc63c2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -616,7 +616,7 @@ namespace osu.Game.Tests.Visual.Online }; }); - AddStep("Show report popover", () => this.ChildrenOfType().First().ShowPopover()); + AddStep("Show report popover", () => this.ChildrenOfType().First().ShowPopover()); AddStep("Set report reason to other", () => { diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 0e69501ae9..dca34128d1 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Chat private readonly OsuSpriteText drawableTimestamp; - private readonly DrawableUsername drawableUsername; + private readonly DrawableChatUsername drawableUsername; private readonly LinkFlowContainer drawableContentFlow; @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Chat Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), AlwaysPresent = true, }, - drawableUsername = new DrawableUsername(message) + drawableUsername = new DrawableChatUsername(message.Sender, message.Id) { Width = UsernameWidth, FontSize = FontSize, diff --git a/osu.Game/Overlays/Chat/DrawableUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs similarity index 96% rename from osu.Game/Overlays/Chat/DrawableUsername.cs rename to osu.Game/Overlays/Chat/DrawableChatUsername.cs index 25967003e9..4ba5c9769a 100644 --- a/osu.Game/Overlays/Chat/DrawableUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -31,7 +31,7 @@ using ChatStrings = osu.Game.Localisation.ChatStrings; namespace osu.Game.Overlays.Chat { - public partial class DrawableUsername : OsuClickableContainer, IHasContextMenu, IHasPopover + public partial class DrawableChatUsername : OsuClickableContainer, IHasContextMenu, IHasPopover { public Color4 AccentColour { get; } @@ -73,15 +73,15 @@ namespace osu.Game.Overlays.Chat private Bindable? currentChannel { get; set; } private readonly APIUser user; - private readonly Message message; + private readonly long? messageId; private readonly OsuSpriteText drawableText; private readonly Drawable colouredDrawable; - public DrawableUsername(Message message) + public DrawableChatUsername(APIUser user, long? messageId) { - this.message = message; - user = message.Sender; + this.user = user; + this.messageId = messageId; Action = openUserProfile; @@ -182,7 +182,7 @@ namespace osu.Game.Overlays.Chat private void report(ChatReportReason reason, string comments) { - var request = new ChatReportRequest(message.Id, reason, comments); + var request = new ChatReportRequest(messageId, reason, comments); request.Failure += _ => Schedule(() => { From be15d07b1734b853d1424c996b3e0de10a94c213 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 May 2023 18:59:39 +0900 Subject: [PATCH 38/80] Tidy up various implementation details --- osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs | 7 ++++--- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 10 +++++----- osu.Game/Overlays/Comments/CommentReportButton.cs | 10 +++++----- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs index 46006402ad..a5c60bb9e6 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs @@ -24,11 +24,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 private OsuEnumDropdown reasonDropdown = null!; private OsuTextBox commentsTextBox = null!; private RoundedButton submitButton = null!; - public LocalisableString Header; + + private readonly LocalisableString header; protected ReportPopover(LocalisableString headerString) { - Header = headerString; + header = headerString; } [BackgroundDependencyLoader] @@ -53,7 +54,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, - Text = Header, + Text = header, Font = OsuFont.Torus.With(size: 25), Margin = new MarginPadding { Bottom = 10 } }, diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 4ba5c9769a..b3bbc74be7 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -180,6 +180,11 @@ namespace osu.Game.Overlays.Chat } } + public Popover GetPopover() => new ReportChatPopover(user) + { + Action = report + }; + private void report(ChatReportReason reason, string comments) { var request = new ChatReportRequest(messageId, reason, comments); @@ -260,10 +265,5 @@ namespace osu.Game.Overlays.Chat Color4Extensions.FromHex("812a96"), Color4Extensions.FromHex("992861"), }; - - public Popover GetPopover() => new ReportChatPopover(user) - { - Action = report - }; } } diff --git a/osu.Game/Overlays/Comments/CommentReportButton.cs b/osu.Game/Overlays/Comments/CommentReportButton.cs index ba5319094b..e4d4d671da 100644 --- a/osu.Game/Overlays/Comments/CommentReportButton.cs +++ b/osu.Game/Overlays/Comments/CommentReportButton.cs @@ -57,6 +57,11 @@ namespace osu.Game.Overlays.Comments link.AddLink(ReportStrings.CommentButton.ToLower(), this.ShowPopover); } + public Popover GetPopover() => new ReportCommentPopover(comment) + { + Action = report + }; + private void report(CommentReportReason reason, string comments) { var request = new CommentReportRequest(comment.Id, reason, comments); @@ -83,10 +88,5 @@ namespace osu.Game.Overlays.Comments api.Queue(request); } - - public Popover GetPopover() => new ReportCommentPopover(comment) - { - Action = report - }; } } From 25bf4e68ec32b5fa68f40280aed8246cefcd42e8 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 3 May 2023 22:53:13 +0900 Subject: [PATCH 39/80] remove useless wait --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index e9e2cc63c2..041556a5a6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -631,7 +631,6 @@ namespace osu.Game.Tests.Visual.Online InputManager.Click(MouseButton.Left); }); - AddWaitStep("Wait", 3); AddAssert("Nothing happened", () => this.ChildrenOfType().Any()); AddStep("Set report data", () => { From 3a15783a3c53e610b86ff71c891df125d3651bb1 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 3 May 2023 22:56:47 +0900 Subject: [PATCH 40/80] remove wait, use AddUntilStep --- .../Visual/UserInterface/TestSceneModPresetColumn.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index c4c5584c5e..55fe142d9f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -301,7 +301,6 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); - AddWaitStep("wait some", 3); AddAssert("present is not changed", () => panel.Preset.Value.Name == presetName); AddUntilStep("popover is unchanged", () => this.ChildrenOfType().FirstOrDefault() == popover); AddStep("edit preset name", () => popover.ChildrenOfType().First().Current.Value = "something new"); @@ -357,8 +356,8 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Left); }); - AddWaitStep("wait some", 3); - AddAssert("present mod not changed", () => + + AddUntilStep("present mod not changed", () => new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(previousMod)); AddStep("select mods", () => SelectedMods.Value = mods); @@ -388,8 +387,8 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.MoveMouseTo(popover.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Left); }); - AddWaitStep("wait some", 3); - AddAssert("present mod is changed", () => + + AddUntilStep("present mod is changed", () => new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); } From c609e6345cf6c3b41b57917fe1dac841b1be9a4c Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 3 May 2023 23:01:31 +0900 Subject: [PATCH 41/80] remove `Use Current Mods` menu item --- .../UserInterface/TestSceneModPresetColumn.cs | 44 ------------------- osu.Game/Overlays/Mods/ModPresetPanel.cs | 21 ++------- 2 files changed, 4 insertions(+), 61 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 55fe142d9f..0cbbe00311 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -392,50 +392,6 @@ namespace osu.Game.Tests.Visual.UserInterface new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); } - [Test] - public void TestEditPresetModInContextMenu() - { - ModPresetColumn modPresetColumn = null!; - var mods = new Mod[] { new OsuModHidden() }; - - AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); - AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - - AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); - - AddStep("right click first panel", () => - { - var panel = this.ChildrenOfType().First(); - InputManager.MoveMouseTo(panel); - InputManager.Click(MouseButton.Right); - }); - AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); - AddAssert("No Use Current Mods", () => this.ChildrenOfType().Count() == 2); - - AddStep("select mods", () => SelectedMods.Value = mods); - AddStep("right click second panel", () => - { - var panel = this.ChildrenOfType().ElementAt(1); - InputManager.MoveMouseTo(panel); - InputManager.Click(MouseButton.Right); - }); - - AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); - AddAssert("Have Use Current Mods", () => this.ChildrenOfType().Count() == 3); - AddStep("Click Use Current Mods", () => - { - var editItem = this.ChildrenOfType().ElementAt(1); - InputManager.MoveMouseTo(editItem); - InputManager.Click(MouseButton.Left); - }); - AddAssert("present mod is changed", () => - new HashSet(this.ChildrenOfType().ElementAt(1).Preset.Value.Mods).SetEquals(mods)); - } - private ICollection createTestPresets() => new[] { new ModPreset diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index d01981d18c..6999f2cede 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -90,24 +90,11 @@ namespace osu.Game.Overlays.Mods #region IHasContextMenu - public MenuItem[] ContextMenuItems + public MenuItem[] ContextMenuItems => new MenuItem[] { - get - { - var menu = new List - { - new OsuMenuItem(CommonStrings.ButtonsEdit, MenuItemType.Highlighted, this.ShowPopover), - new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset))), - }; - - if (CheckCurrentModCanBeSave()) - { - menu.Insert(1, new OsuMenuItem("Use Current Mods", MenuItemType.Destructive, () => SaveCurrentMod())); - } - - return menu.ToArray(); - } - } + new OsuMenuItem(CommonStrings.ButtonsEdit, MenuItemType.Highlighted, this.ShowPopover), + new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset))), + }; #endregion From aa5a026c676c0503f6110c0d4fa610b3669bc9fd Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 3 May 2023 23:14:24 +0900 Subject: [PATCH 42/80] remove local button handle --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 27 ++++++--------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 0e21260493..77980ea629 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = "Use Current Mods", - Action = trySaveCurrentMod + Action = saveCurrentMod }, createButton = new ShearedButton { @@ -123,19 +123,17 @@ namespace osu.Game.Overlays.Mods createButton.LighterColour = colours.Orange0; createButton.TextColour = colourProvider.Background6; + useCurrentModButton.DarkerColour = colours.Blue1; + useCurrentModButton.LighterColour = colours.Blue0; + useCurrentModButton.TextColour = colourProvider.Background6; + selectedMods.BindValueChanged(_ => updateActiveState(), true); scrollContent.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); } - private void trySaveCurrentMod() + private void saveCurrentMod() { - if (!checkCanBeSave()) - { - Body.Shake(); - return; - } - saveModAfterClosed = selectedMods.Value.ToList(); scrollContent.Clear(); scrollContent.ChildrenEnumerable = saveModAfterClosed.Select(mod => new ModPresetRow(mod)); @@ -144,18 +142,7 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { - if (checkCanBeSave()) - { - useCurrentModButton.DarkerColour = colours.Blue1; - useCurrentModButton.LighterColour = colours.Blue0; - useCurrentModButton.TextColour = colourProvider.Background6; - } - else - { - useCurrentModButton.DarkerColour = colours.Blue3; - useCurrentModButton.LighterColour = colours.Blue4; - useCurrentModButton.TextColour = colourProvider.Background2; - } + useCurrentModButton.Enabled.Value = checkCanBeSave(); } private bool checkCanBeSave() From f4b1264cc9859acd275e0f99bf68c2b6cd59306b Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 3 May 2023 23:22:46 +0900 Subject: [PATCH 43/80] use button `Enable` status to ensure preset name is not null --- osu.Game/Overlays/Mods/AddPresetPopover.cs | 15 ++++++------- osu.Game/Overlays/Mods/EditPresetPopover.cs | 25 ++++++++++----------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index 33d72ff383..6fd4231f2c 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = ModSelectOverlayStrings.AddPreset, - Action = tryCreatePreset + Action = createPreset } } }; @@ -89,16 +89,15 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); + + nameTextBox.Current.BindValueChanged(s => + { + createButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue); + }, true); } - private void tryCreatePreset() + private void createPreset() { - if (string.IsNullOrWhiteSpace(nameTextBox.Current.Value)) - { - Body.Shake(); - return; - } - realm.Write(r => r.Add(new ModPreset { Name = nameTextBox.Current.Value, diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 77980ea629..451c1ec712 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Mods private readonly LabelledTextBox nameTextBox; private readonly LabelledTextBox descriptionTextBox; private readonly ShearedButton useCurrentModButton; - private readonly ShearedButton createButton; + private readonly ShearedButton editButton; private readonly FillFlowContainer scrollContent; private readonly ModPreset preset; @@ -97,12 +97,12 @@ namespace osu.Game.Overlays.Mods Text = "Use Current Mods", Action = saveCurrentMod }, - createButton = new ShearedButton + editButton = new ShearedButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = Resources.Localisation.Web.CommonStrings.ButtonsSave, - Action = tryEditPreset + Action = editPreset }, } } @@ -119,9 +119,9 @@ namespace osu.Game.Overlays.Mods nameTextBox.Current.Value = preset.Name; descriptionTextBox.Current.Value = preset.Description; - createButton.DarkerColour = colours.Orange1; - createButton.LighterColour = colours.Orange0; - createButton.TextColour = colourProvider.Background6; + editButton.DarkerColour = colours.Orange1; + editButton.LighterColour = colours.Orange0; + editButton.TextColour = colourProvider.Background6; useCurrentModButton.DarkerColour = colours.Blue1; useCurrentModButton.LighterColour = colours.Blue0; @@ -130,6 +130,11 @@ namespace osu.Game.Overlays.Mods selectedMods.BindValueChanged(_ => updateActiveState(), true); scrollContent.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); + + nameTextBox.Current.BindValueChanged(s => + { + editButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue); + }, true); } private void saveCurrentMod() @@ -165,14 +170,8 @@ namespace osu.Game.Overlays.Mods ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); } - private void tryEditPreset() + private void editPreset() { - if (string.IsNullOrWhiteSpace(nameTextBox.Current.Value)) - { - Body.Shake(); - return; - } - button.Preset.PerformWrite(s => { s.Name = nameTextBox.Current.Value; From debbd376bd6e8155612ea8a2b6e2fe1db2a9c884 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 3 May 2023 23:24:14 +0900 Subject: [PATCH 44/80] move `scrollContent` update logic to `updateActiveState()` --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 451c1ec712..e96168d8ff 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -129,8 +129,6 @@ namespace osu.Game.Overlays.Mods selectedMods.BindValueChanged(_ => updateActiveState(), true); - scrollContent.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); - nameTextBox.Current.BindValueChanged(s => { editButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue); @@ -141,12 +139,12 @@ namespace osu.Game.Overlays.Mods { saveModAfterClosed = selectedMods.Value.ToList(); scrollContent.Clear(); - scrollContent.ChildrenEnumerable = saveModAfterClosed.Select(mod => new ModPresetRow(mod)); updateActiveState(); } private void updateActiveState() { + scrollContent.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); useCurrentModButton.Enabled.Value = checkCanBeSave(); } From 967e801f9c77cc4b745fd32625c674fd708d25e9 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Wed, 3 May 2023 23:26:58 +0900 Subject: [PATCH 45/80] code inspect --- osu.Game/Overlays/Mods/AddPresetPopover.cs | 1 - osu.Game/Overlays/Mods/EditPresetPopover.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index 6fd4231f2c..d9e350e560 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -9,7 +9,6 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index e96168d8ff..7f7b9472a7 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -8,7 +8,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; From d1d4b54c640df278987ff8ba8ccad28519e59e44 Mon Sep 17 00:00:00 2001 From: Terochi Date: Wed, 3 May 2023 18:31:35 +0200 Subject: [PATCH 46/80] Simplified --- osu.Game/OsuGameBase.cs | 46 +++++++++++++---------------------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e6d6deabe9..076c1213ad 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -625,40 +625,22 @@ namespace osu.Game return; } - var previouslySelectedCommonMods = new List(SelectedMods.Value.Count); - var convertedCommonMods = new List(SelectedMods.Value.Count); - - foreach (var mod in SelectedMods.Value) - { - var convertedMod = instance.CreateModFromAcronym(mod.Acronym); - - if (convertedMod == null) - continue; - - previouslySelectedCommonMods.Add(mod); - - convertedMod.CopyCommonSettings(mod); - convertedCommonMods.Add(convertedMod); - } - - if (!ModUtils.CheckValidForGameplay(convertedCommonMods, out var invalid)) - { - invalid.ForEach(mod => - { - int index = convertedCommonMods.IndexOf(mod); - convertedCommonMods.RemoveAt(index); - previouslySelectedCommonMods.RemoveAt(index); - }); - } - - if (!SelectedMods.Disabled) - // Select common mods to play the deselect samples for other mods - SelectedMods.Value = previouslySelectedCommonMods; - AvailableMods.Value = dict; - if (!SelectedMods.Disabled) - SelectedMods.Value = convertedCommonMods; + if (SelectedMods.Disabled) + return; + + var convertedMods = SelectedMods.Value.Select(mod => + { + var newMod = instance.CreateModFromAcronym(mod.Acronym); + newMod?.CopyCommonSettings(mod); + return newMod; + }).Where(newMod => newMod != null).ToList(); + + if (!ModUtils.CheckValidForGameplay(convertedMods, out var invalid)) + invalid.ForEach(newMod => convertedMods.Remove(newMod)); + + SelectedMods.Value = convertedMods; void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First(); } From 0a584f065235d3317067b9ed2860a686fe419122 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 May 2023 11:07:09 +0900 Subject: [PATCH 47/80] Simplify check logic and improve variable naming --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 7f7b9472a7..9015218910 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -29,7 +29,8 @@ namespace osu.Game.Overlays.Mods private readonly FillFlowContainer scrollContent; private readonly ModPreset preset; - private List saveModAfterClosed = new List(); + + private HashSet? newMods; [Resolved] private Bindable> selectedMods { get; set; } = null!; @@ -136,7 +137,7 @@ namespace osu.Game.Overlays.Mods private void saveCurrentMod() { - saveModAfterClosed = selectedMods.Value.ToList(); + newMods = selectedMods.Value.ToHashSet(); scrollContent.Clear(); updateActiveState(); } @@ -144,18 +145,16 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { scrollContent.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); - useCurrentModButton.Enabled.Value = checkCanBeSave(); + useCurrentModButton.Enabled.Value = checkSelectedModsDiffersFromSaved(); } - private bool checkCanBeSave() + private bool checkSelectedModsDiffersFromSaved() { if (!selectedMods.Value.Any()) return false; - if (saveModAfterClosed.Any()) - { - return !new HashSet(saveModAfterClosed).SetEquals(selectedMods.Value); - } + if (newMods?.SetEquals(selectedMods.Value) == false) + return true; return button.CheckCurrentModCanBeSave(); } @@ -174,10 +173,8 @@ namespace osu.Game.Overlays.Mods s.Name = nameTextBox.Current.Value; s.Description = descriptionTextBox.Current.Value; - if (saveModAfterClosed.Any()) - { - s.Mods = saveModAfterClosed; - } + if (newMods != null) + s.Mods = newMods; }); this.HidePopover(); From b7abab6d8aff61e2e79c17f7fd367999f72857de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 May 2023 11:10:05 +0900 Subject: [PATCH 48/80] More variable improvements --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 9015218910..91beea8e38 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -24,8 +24,8 @@ namespace osu.Game.Overlays.Mods private readonly LabelledTextBox nameTextBox; private readonly LabelledTextBox descriptionTextBox; - private readonly ShearedButton useCurrentModButton; - private readonly ShearedButton editButton; + private readonly ShearedButton useCurrentModsButton; + private readonly ShearedButton saveButton; private readonly FillFlowContainer scrollContent; private readonly ModPreset preset; @@ -90,19 +90,19 @@ namespace osu.Game.Overlays.Mods Spacing = new Vector2(7), Children = new Drawable[] { - useCurrentModButton = new ShearedButton + useCurrentModsButton = new ShearedButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = "Use Current Mods", - Action = saveCurrentMod + Action = useCurrentMods }, - editButton = new ShearedButton + saveButton = new ShearedButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = Resources.Localisation.Web.CommonStrings.ButtonsSave, - Action = editPreset + Action = save }, } } @@ -119,23 +119,23 @@ namespace osu.Game.Overlays.Mods nameTextBox.Current.Value = preset.Name; descriptionTextBox.Current.Value = preset.Description; - editButton.DarkerColour = colours.Orange1; - editButton.LighterColour = colours.Orange0; - editButton.TextColour = colourProvider.Background6; + saveButton.DarkerColour = colours.Orange1; + saveButton.LighterColour = colours.Orange0; + saveButton.TextColour = colourProvider.Background6; - useCurrentModButton.DarkerColour = colours.Blue1; - useCurrentModButton.LighterColour = colours.Blue0; - useCurrentModButton.TextColour = colourProvider.Background6; + useCurrentModsButton.DarkerColour = colours.Blue1; + useCurrentModsButton.LighterColour = colours.Blue0; + useCurrentModsButton.TextColour = colourProvider.Background6; selectedMods.BindValueChanged(_ => updateActiveState(), true); nameTextBox.Current.BindValueChanged(s => { - editButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue); + saveButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue); }, true); } - private void saveCurrentMod() + private void useCurrentMods() { newMods = selectedMods.Value.ToHashSet(); scrollContent.Clear(); @@ -145,7 +145,7 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { scrollContent.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); - useCurrentModButton.Enabled.Value = checkSelectedModsDiffersFromSaved(); + useCurrentModsButton.Enabled.Value = checkSelectedModsDiffersFromSaved(); } private bool checkSelectedModsDiffersFromSaved() @@ -166,7 +166,7 @@ namespace osu.Game.Overlays.Mods ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); } - private void editPreset() + private void save() { button.Preset.PerformWrite(s => { From be995f1359e19de4d9573fe1ad76645400c0949b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 May 2023 11:11:19 +0900 Subject: [PATCH 49/80] Add localisation support for new button string --- osu.Game/Localisation/ModSelectOverlayStrings.cs | 5 +++++ osu.Game/Overlays/Mods/EditPresetPopover.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs index d6a01c4794..f11c52ee20 100644 --- a/osu.Game/Localisation/ModSelectOverlayStrings.cs +++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs @@ -34,6 +34,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AddPreset => new TranslatableString(getKey(@"add_preset"), @"Add preset"); + /// + /// "Use current mods" + /// + public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 91beea8e38..b826bdf944 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -94,7 +94,7 @@ namespace osu.Game.Overlays.Mods { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "Use Current Mods", + Text = ModSelectOverlayStrings.UseCurrentMods, Action = useCurrentMods }, saveButton = new ShearedButton From 99d2616c34d51b8e4027543f01b73a8efe499475 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 May 2023 11:14:51 +0900 Subject: [PATCH 50/80] Move drawable construction to `load` for simplicity --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 58 ++++++++++----------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index b826bdf944..f02c200654 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -20,13 +20,13 @@ namespace osu.Game.Overlays.Mods { internal partial class EditPresetPopover : OsuPopover { - private readonly ModPresetPanel button; + private readonly ModPresetPanel presetPanel; - private readonly LabelledTextBox nameTextBox; - private readonly LabelledTextBox descriptionTextBox; - private readonly ShearedButton useCurrentModsButton; - private readonly ShearedButton saveButton; - private readonly FillFlowContainer scrollContent; + private LabelledTextBox nameTextBox = null!; + private LabelledTextBox descriptionTextBox = null!; + private ShearedButton useCurrentModsButton = null!; + private ShearedButton saveButton = null!; + private FillFlowContainer scrollContent = null!; private readonly ModPreset preset; @@ -41,11 +41,15 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public EditPresetPopover(ModPresetPanel modPresetPanel) + public EditPresetPopover(ModPresetPanel presetPanel) { - button = modPresetPanel; - preset = button.Preset.Value; + this.presetPanel = presetPanel; + preset = presetPanel.Preset.Value; + } + [BackgroundDependencyLoader] + private void load() + { Child = new FillFlowContainer { Width = 300, @@ -59,14 +63,16 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Label = CommonStrings.Name, - TabbableContentContainer = this + TabbableContentContainer = this, + Current = { Value = preset.Name }, }, descriptionTextBox = new LabelledTextBox { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Label = CommonStrings.Description, - TabbableContentContainer = this + TabbableContentContainer = this, + Current = { Value = preset.Description }, }, new OsuScrollContainer { @@ -95,40 +101,30 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = ModSelectOverlayStrings.UseCurrentMods, - Action = useCurrentMods + DarkerColour = colours.Blue1, + LighterColour = colours.Blue0, + TextColour = colourProvider.Background6, + Action = useCurrentMods, }, saveButton = new ShearedButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = Resources.Localisation.Web.CommonStrings.ButtonsSave, - Action = save + DarkerColour = colours.Orange1, + LighterColour = colours.Orange0, + TextColour = colourProvider.Background6, + Action = save, }, } } } }; - } - [BackgroundDependencyLoader] - private void load() - { Body.BorderThickness = 3; Body.BorderColour = colours.Orange1; - nameTextBox.Current.Value = preset.Name; - descriptionTextBox.Current.Value = preset.Description; - - saveButton.DarkerColour = colours.Orange1; - saveButton.LighterColour = colours.Orange0; - saveButton.TextColour = colourProvider.Background6; - - useCurrentModsButton.DarkerColour = colours.Blue1; - useCurrentModsButton.LighterColour = colours.Blue0; - useCurrentModsButton.TextColour = colourProvider.Background6; - selectedMods.BindValueChanged(_ => updateActiveState(), true); - nameTextBox.Current.BindValueChanged(s => { saveButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue); @@ -156,7 +152,7 @@ namespace osu.Game.Overlays.Mods if (newMods?.SetEquals(selectedMods.Value) == false) return true; - return button.CheckCurrentModCanBeSave(); + return presetPanel.CheckCurrentModCanBeSave(); } protected override void LoadComplete() @@ -168,7 +164,7 @@ namespace osu.Game.Overlays.Mods private void save() { - button.Preset.PerformWrite(s => + presetPanel.Preset.PerformWrite(s => { s.Name = nameTextBox.Current.Value; s.Description = descriptionTextBox.Current.Value; From b12d139317b55d47046eb0eaaf50756aaf07fb67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 May 2023 11:16:52 +0900 Subject: [PATCH 51/80] Remove dead code --- osu.Game/Overlays/Mods/ModPresetPanel.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 6999f2cede..3f5a764ab1 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -98,18 +98,6 @@ namespace osu.Game.Overlays.Mods #endregion - public bool SaveCurrentMod() - { - if (!CheckCurrentModCanBeSave()) - return false; - - Preset.PerformWrite(s => - { - s.Mods = selectedMods.Value.ToArray(); - }); - return true; - } - public bool CheckCurrentModCanBeSave() => (!Active.Value && selectedMods.Value.Any()); protected override void Dispose(bool isDisposing) From 49fb5da1a237c30768f753d98d793733750bea5a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 May 2023 11:19:09 +0900 Subject: [PATCH 52/80] Stop passing preset panel to popover --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 23 ++++++++------------- osu.Game/Overlays/Mods/ModPresetPanel.cs | 4 +--- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index f02c200654..3f5eca8ad0 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -20,15 +21,13 @@ namespace osu.Game.Overlays.Mods { internal partial class EditPresetPopover : OsuPopover { - private readonly ModPresetPanel presetPanel; - private LabelledTextBox nameTextBox = null!; private LabelledTextBox descriptionTextBox = null!; private ShearedButton useCurrentModsButton = null!; private ShearedButton saveButton = null!; private FillFlowContainer scrollContent = null!; - private readonly ModPreset preset; + private readonly Live preset; private HashSet? newMods; @@ -41,10 +40,9 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public EditPresetPopover(ModPresetPanel presetPanel) + public EditPresetPopover(Live preset) { - this.presetPanel = presetPanel; - preset = presetPanel.Preset.Value; + this.preset = preset; } [BackgroundDependencyLoader] @@ -64,7 +62,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.TopCentre, Label = CommonStrings.Name, TabbableContentContainer = this, - Current = { Value = preset.Name }, + Current = { Value = preset.PerformRead(p => p.Name) }, }, descriptionTextBox = new LabelledTextBox { @@ -72,7 +70,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.TopCentre, Label = CommonStrings.Description, TabbableContentContainer = this, - Current = { Value = preset.Description }, + Current = { Value = preset.PerformRead(p => p.Description) }, }, new OsuScrollContainer { @@ -140,7 +138,7 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { - scrollContent.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod)); + scrollContent.ChildrenEnumerable = preset.PerformRead(p => p.Mods.Select(mod => new ModPresetRow(mod))); useCurrentModsButton.Enabled.Value = checkSelectedModsDiffersFromSaved(); } @@ -149,10 +147,7 @@ namespace osu.Game.Overlays.Mods if (!selectedMods.Value.Any()) return false; - if (newMods?.SetEquals(selectedMods.Value) == false) - return true; - - return presetPanel.CheckCurrentModCanBeSave(); + return newMods?.SetEquals(selectedMods.Value) == false; } protected override void LoadComplete() @@ -164,7 +159,7 @@ namespace osu.Game.Overlays.Mods private void save() { - presetPanel.Preset.PerformWrite(s => + preset.PerformWrite(s => { s.Name = nameTextBox.Current.Value; s.Description = descriptionTextBox.Current.Value; diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 3f5a764ab1..8bcb5e4e4e 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -98,8 +98,6 @@ namespace osu.Game.Overlays.Mods #endregion - public bool CheckCurrentModCanBeSave() => (!Active.Value && selectedMods.Value.Any()); - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -107,6 +105,6 @@ namespace osu.Game.Overlays.Mods settingChangeTracker?.Dispose(); } - public Popover GetPopover() => new EditPresetPopover(this); + public Popover GetPopover() => new EditPresetPopover(Preset); } } From 26b8c5b852d31b1e0d340d0926821dfcd0910fa5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 May 2023 11:23:51 +0900 Subject: [PATCH 53/80] Fix mod list display not updating after clicking "use current" This is not a regression from my changes. It was broken before them. --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 3f5eca8ad0..82d0bdb51e 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Mods private readonly Live preset; - private HashSet? newMods; + private HashSet saveableMods; [Resolved] private Bindable> selectedMods { get; set; } = null!; @@ -43,6 +43,7 @@ namespace osu.Game.Overlays.Mods public EditPresetPopover(Live preset) { this.preset = preset; + saveableMods = preset.PerformRead(p => p.Mods).ToHashSet(); } [BackgroundDependencyLoader] @@ -122,7 +123,7 @@ namespace osu.Game.Overlays.Mods Body.BorderThickness = 3; Body.BorderColour = colours.Orange1; - selectedMods.BindValueChanged(_ => updateActiveState(), true); + selectedMods.BindValueChanged(_ => updateState(), true); nameTextBox.Current.BindValueChanged(s => { saveButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue); @@ -131,14 +132,14 @@ namespace osu.Game.Overlays.Mods private void useCurrentMods() { - newMods = selectedMods.Value.ToHashSet(); + saveableMods = selectedMods.Value.ToHashSet(); scrollContent.Clear(); - updateActiveState(); + updateState(); } - private void updateActiveState() + private void updateState() { - scrollContent.ChildrenEnumerable = preset.PerformRead(p => p.Mods.Select(mod => new ModPresetRow(mod))); + scrollContent.ChildrenEnumerable = saveableMods.Select(mod => new ModPresetRow(mod)); useCurrentModsButton.Enabled.Value = checkSelectedModsDiffersFromSaved(); } @@ -147,7 +148,7 @@ namespace osu.Game.Overlays.Mods if (!selectedMods.Value.Any()) return false; - return newMods?.SetEquals(selectedMods.Value) == false; + return !saveableMods.SetEquals(selectedMods.Value); } protected override void LoadComplete() @@ -163,9 +164,7 @@ namespace osu.Game.Overlays.Mods { s.Name = nameTextBox.Current.Value; s.Description = descriptionTextBox.Current.Value; - - if (newMods != null) - s.Mods = newMods; + s.Mods = saveableMods; }); this.HidePopover(); From 0034124d79ee949f00dc5ec7fcd98fe8780d250e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 May 2023 11:25:10 +0900 Subject: [PATCH 54/80] Remove unnecessary content clear --- osu.Game/Overlays/Mods/EditPresetPopover.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 82d0bdb51e..5220f6a391 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -133,7 +133,6 @@ namespace osu.Game.Overlays.Mods private void useCurrentMods() { saveableMods = selectedMods.Value.ToHashSet(); - scrollContent.Clear(); updateState(); } From 2cb6642b0d8f805317f1458078e545c685c0a148 Mon Sep 17 00:00:00 2001 From: cdwcgt Date: Thu, 4 May 2023 13:18:25 +0900 Subject: [PATCH 55/80] use AddUntilStep or not wait --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 041556a5a6..339d861d3b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -645,8 +645,7 @@ namespace osu.Game.Tests.Visual.Online InputManager.Click(MouseButton.Left); }); - AddWaitStep("Wait", 3); - AddAssert("Overlay closed", () => !this.ChildrenOfType().Any()); + AddUntilStep("Overlay closed", () => !this.ChildrenOfType().Any()); AddStep("Complete request", () => requestLock.Set()); AddUntilStep("Request sent", () => request != null); } From 0d2396c5575cd34864e42974b7c99218e4074c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 18:15:12 +0200 Subject: [PATCH 56/80] Rename method to better indicate directionality --- osu.Game.Tests/Mods/ModSettingsTest.cs | 14 +++++++------- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index b3f6f8da7d..4dcc4511db 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -45,12 +45,12 @@ namespace osu.Game.Tests.Mods var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change / 2 } }; - modDouble.CopyCommonSettings(modBool); - modDouble.CopyCommonSettings(modInt); - modBool.CopyCommonSettings(modDouble); - modBool.CopyCommonSettings(modInt); - modInt.CopyCommonSettings(modDouble); - modInt.CopyCommonSettings(modBool); + modDouble.CopyCommonSettingsFrom(modBool); + modDouble.CopyCommonSettingsFrom(modInt); + modBool.CopyCommonSettingsFrom(modDouble); + modBool.CopyCommonSettingsFrom(modInt); + modInt.CopyCommonSettingsFrom(modDouble); + modInt.CopyCommonSettingsFrom(modBool); Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Mods var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } }; var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } }; - modBoolFalse.CopyCommonSettings(modBoolTrue); + modBoolFalse.CopyCommonSettingsFrom(modBoolTrue); Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false)); Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value)); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 076c1213ad..c55b6c249f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -633,7 +633,7 @@ namespace osu.Game var convertedMods = SelectedMods.Value.Select(mod => { var newMod = instance.CreateModFromAcronym(mod.Acronym); - newMod?.CopyCommonSettings(mod); + newMod?.CopyCommonSettingsFrom(mod); return newMod; }).Where(newMod => newMod != null).ToList(); diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 0af8717264..cf5a1c6fcc 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Mods /// /// Copied values are unchanged, even if they have different clamping ranges. /// The mod to extract settings from. - public void CopyCommonSettings(Mod source) + public void CopyCommonSettingsFrom(Mod source) { if (source.UsesDefaultConfiguration) return; From 26337dbcd8c7ca314cf0a8034ee2a6cdad9058b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 18:26:45 +0200 Subject: [PATCH 57/80] Do not rely on unspecified `Dictionary.Values` ordering --- osu.Game/Rulesets/Mods/Mod.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index cf5a1c6fcc..db7e7ce8e3 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -119,7 +119,11 @@ namespace osu.Game.Rulesets.Mods /// /// All settings within this mod. /// - internal IEnumerable SettingsBindables => Settings.Values; + /// + /// The settings are returned in ascending key order as per . + /// The ordering is intentionally enforced manually, as ordering of is unspecified. + /// + internal IEnumerable SettingsBindables => Settings.OrderBy(pair => pair.Key).Select(pair => pair.Value); /// /// Provides mapping of names to s of all settings within this mod. From 88e77ad390fad33752395905260f97ed95f86def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 18:30:41 +0200 Subject: [PATCH 58/80] Rename `Settings{-> Map}` --- osu.Game/Rulesets/Mods/Mod.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index db7e7ce8e3..cd89b45a5e 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -120,15 +120,15 @@ namespace osu.Game.Rulesets.Mods /// All settings within this mod. /// /// - /// The settings are returned in ascending key order as per . + /// The settings are returned in ascending key order as per . /// The ordering is intentionally enforced manually, as ordering of is unspecified. /// - internal IEnumerable SettingsBindables => Settings.OrderBy(pair => pair.Key).Select(pair => pair.Value); + internal IEnumerable SettingsBindables => SettingsMap.OrderBy(pair => pair.Key).Select(pair => pair.Value); /// /// Provides mapping of names to s of all settings within this mod. /// - internal IReadOnlyDictionary Settings => + internal IReadOnlyDictionary SettingsMap => settingsBacking ??= this.GetSettingsSourceProperties() .Select(p => p.Item2) .ToDictionary(property => property.Name.ToSnakeCase(), property => (IBindable)property.GetValue(this)!); @@ -177,9 +177,9 @@ namespace osu.Game.Rulesets.Mods if (source.UsesDefaultConfiguration) return; - foreach (var (name, targetSetting) in Settings) + foreach (var (name, targetSetting) in SettingsMap) { - if (!source.Settings.TryGetValue(name, out IBindable? sourceSetting)) + if (!source.SettingsMap.TryGetValue(name, out IBindable? sourceSetting)) continue; if (sourceSetting.IsDefault) From 1f1342b099752b99de109a2fc6a6fcbaa9b37bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 18:33:49 +0200 Subject: [PATCH 59/80] Reword xmldoc --- osu.Game/Rulesets/Mods/Mod.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index cd89b45a5e..f5ccde01cf 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -167,10 +167,13 @@ namespace osu.Game.Rulesets.Mods } /// - /// When converting mods from one ruleset to the other, this method makes sure - /// to also copy the values of all settings sharing same between the two instances. + /// This method copies the values of all settings from that share the same names with this mod instance. + /// The most frequent use of this is when switching rulesets, in order to preserve values of common settings during the switch. /// - /// Copied values are unchanged, even if they have different clamping ranges. + /// + /// The values are copied directly, without adjusting for possibly different allowed ranges of values. + /// If the value of a setting is not valid for this instance due to not falling inside of the allowed range, it will be clamped accordingly. + /// /// The mod to extract settings from. public void CopyCommonSettingsFrom(Mod source) { From e43fc23606e4b579fdf46ee54aa3fb144bb85d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 18:46:36 +0200 Subject: [PATCH 60/80] Fix typos in test --- .../Visual/UserInterface/TestSceneModPresetColumn.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 0cbbe00311..3efdba8754 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -262,7 +262,7 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestEditPresentName() + public void TestEditPresetName() { ModPresetColumn modPresetColumn = null!; string presetName = null!; @@ -301,7 +301,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); - AddAssert("present is not changed", () => panel.Preset.Value.Name == presetName); + AddAssert("preset is not changed", () => panel.Preset.Value.Name == presetName); AddUntilStep("popover is unchanged", () => this.ChildrenOfType().FirstOrDefault() == popover); AddStep("edit preset name", () => popover.ChildrenOfType().First().Current.Value = "something new"); AddStep("attempt preset edit", () => @@ -310,7 +310,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); - AddAssert("present is changed", () => panel.Preset.Value.Name != presetName); + AddAssert("preset is changed", () => panel.Preset.Value.Name != presetName); } [Test] @@ -357,7 +357,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); - AddUntilStep("present mod not changed", () => + AddUntilStep("preset mod not changed", () => new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(previousMod)); AddStep("select mods", () => SelectedMods.Value = mods); @@ -388,7 +388,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); - AddUntilStep("present mod is changed", () => + AddUntilStep("preset mod is changed", () => new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); } From 485b7282dce5a7c220fa534e2f5f9655935270e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 19:06:07 +0200 Subject: [PATCH 61/80] Clarify type check --- osu.Game/Rulesets/Mods/Mod.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f5ccde01cf..787da926ea 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -188,10 +188,13 @@ namespace osu.Game.Rulesets.Mods if (sourceSetting.IsDefault) continue; - var targetType = targetSetting.GetType(); - var sourceType = sourceSetting.GetType(); + var targetBindableType = targetSetting.GetType(); + var sourceBindableType = sourceSetting.GetType(); - if (!targetType.IsAssignableFrom(sourceType) && !sourceType.IsAssignableFrom(targetType)) + // if either the target is assignable to the source or the source is assignable to the target, + // then we presume that the data types contained in both bindables are compatible and we can proceed with the copy. + // this handles cases like `Bindable` and `BindableInt`. + if (!targetBindableType.IsAssignableFrom(sourceBindableType) && !sourceBindableType.IsAssignableFrom(targetBindableType)) continue; // TODO: special case for handling number types From aa7885ab973a258c350ad26c19bc51d89befdf51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 19:11:51 +0200 Subject: [PATCH 62/80] Use better test step names --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index b4b5052f23..f079b65e23 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -399,7 +399,7 @@ namespace osu.Game.Tests.Visual.UserInterface changeRuleset(1); AddAssert("taiko variant selected", () => SelectedMods.Value.SingleOrDefault() is TaikoModDifficultyAdjust); - AddAssert("shared settings didn't change", () => + AddAssert("shared settings preserved", () => { var taikoMod = getMod(); @@ -408,7 +408,7 @@ namespace osu.Game.Tests.Visual.UserInterface taikoMod.OverallDifficulty.Value == setting_change; }); - AddAssert("non-shared settings unchanged", () => + AddAssert("non-shared settings remain default", () => { var taikoMod = getMod(); From 99e8b2ce70276a34d648960556373917b50d0e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 19:12:22 +0200 Subject: [PATCH 63/80] Make `getMod()` method generally better --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index f079b65e23..9993893a99 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -387,7 +387,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("change mod settings", () => { - var osuMod = getMod(); + var osuMod = getSelectedMod(); osuMod.ExtendedLimits.Value = true; osuMod.CircleSize.Value = setting_change; @@ -401,7 +401,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("shared settings preserved", () => { - var taikoMod = getMod(); + var taikoMod = getSelectedMod(); return taikoMod.ExtendedLimits.Value && taikoMod.DrainRate.Value == setting_change && @@ -410,7 +410,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("non-shared settings remain default", () => { - var taikoMod = getMod(); + var taikoMod = getSelectedMod(); return taikoMod.ScrollSpeed.IsDefault; }); @@ -626,7 +626,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Value == active); } - private T getMod() where T : Mod => (T)SelectedMods.Value.Single(); + private T getSelectedMod() where T : Mod => SelectedMods.Value.OfType().Single(); private ModPanel getPanelForMod(Type modType) => modSelectOverlay.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType); From a1106d0a4e0a2522122f945d283f92368714f4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 19:14:20 +0200 Subject: [PATCH 64/80] Be explicit in test --- osu.Game.Tests/Mods/ModSettingsTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs index 4dcc4511db..5ec9629dc2 100644 --- a/osu.Game.Tests/Mods/ModSettingsTest.cs +++ b/osu.Game.Tests/Mods/ModSettingsTest.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Mods modInt.CopyCommonSettingsFrom(modBool); Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change)); - Assert.That(modBool.TestSetting.Value, Is.EqualTo(!modBool.TestSetting.Default)); + Assert.That(modBool.TestSetting.Value, Is.EqualTo(true)); Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change / 2)); } From dcd2abae6d5f7a196fae601854f08a794999c470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 May 2023 20:46:27 +0200 Subject: [PATCH 65/80] Add test coverage for mod equality with multiple settings --- .../Mods/ModSettingsEqualityComparison.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs index cd6879cf01..a03b29f7bc 100644 --- a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs +++ b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs @@ -49,5 +49,31 @@ namespace osu.Game.Tests.Mods Assert.That(mod3, Is.EqualTo(mod2)); Assert.That(doubleConvertedMod3, Is.EqualTo(doubleConvertedMod2)); } + + [Test] + public void TestModWithMultipleSettings() + { + var ruleset = new OsuRuleset(); + + var mod1 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 0 } }; + var mod2 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 6 } }; + var mod3 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 6 } }; + + var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset); + var doubleConvertedMod2 = new APIMod(mod2).ToMod(ruleset); + var doubleConvertedMod3 = new APIMod(mod3).ToMod(ruleset); + + Assert.That(mod1, Is.Not.EqualTo(mod2)); + Assert.That(doubleConvertedMod1, Is.Not.EqualTo(doubleConvertedMod2)); + + Assert.That(mod2, Is.EqualTo(mod2)); + Assert.That(doubleConvertedMod2, Is.EqualTo(doubleConvertedMod2)); + + Assert.That(mod2, Is.EqualTo(mod3)); + Assert.That(doubleConvertedMod2, Is.EqualTo(doubleConvertedMod3)); + + Assert.That(mod3, Is.EqualTo(mod2)); + Assert.That(doubleConvertedMod3, Is.EqualTo(doubleConvertedMod2)); + } } } From 8391e2a538c7ca7777e2c04a9cdf90a9f191abf7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 May 2023 13:58:13 +0900 Subject: [PATCH 66/80] Move reporting code out of `DrawableChatUsername` into more correct locations --- .../Visual/Online/TestSceneChatOverlay.cs | 2 +- osu.Game/Overlays/Chat/ChatLine.cs | 14 +++++--- .../Overlays/Chat/DrawableChatUsername.cs | 34 +++---------------- osu.Game/Overlays/Chat/ReportChatPopover.cs | 31 +++++++++++++++-- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 339d861d3b..99ce837665 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -616,7 +616,7 @@ namespace osu.Game.Tests.Visual.Online }; }); - AddStep("Show report popover", () => this.ChildrenOfType().First().ShowPopover()); + AddStep("Show report popover", () => this.ChildrenOfType().First().ShowPopover()); AddStep("Set report reason to other", () => { diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index dca34128d1..2f4c175ac4 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -1,25 +1,28 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Chat { - public partial class ChatLine : CompositeDrawable + public partial class ChatLine : CompositeDrawable, IHasPopover { private Message message = null!; @@ -92,7 +95,7 @@ namespace osu.Game.Overlays.Chat Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), AlwaysPresent = true, }, - drawableUsername = new DrawableChatUsername(message.Sender, message.Id) + drawableUsername = new DrawableChatUsername(message.Sender) { Width = UsernameWidth, FontSize = FontSize, @@ -100,6 +103,7 @@ namespace osu.Game.Overlays.Chat Origin = Anchor.TopRight, Anchor = Anchor.TopRight, Margin = new MarginPadding { Horizontal = Spacing }, + ReportRequested = this.ShowPopover, }, drawableContentFlow = new LinkFlowContainer(styleMessageContent) { @@ -128,6 +132,8 @@ namespace osu.Game.Overlays.Chat FinishTransforms(true); } + public Popover GetPopover() => new ReportChatPopover(message); + /// /// Performs a highlight animation on this . /// diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index b3bbc74be7..4b4afc204c 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -21,7 +20,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; @@ -31,8 +29,10 @@ using ChatStrings = osu.Game.Localisation.ChatStrings; namespace osu.Game.Overlays.Chat { - public partial class DrawableChatUsername : OsuClickableContainer, IHasContextMenu, IHasPopover + public partial class DrawableChatUsername : OsuClickableContainer, IHasContextMenu { + public Action? ReportRequested; + public Color4 AccentColour { get; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => @@ -73,15 +73,13 @@ namespace osu.Game.Overlays.Chat private Bindable? currentChannel { get; set; } private readonly APIUser user; - private readonly long? messageId; private readonly OsuSpriteText drawableText; private readonly Drawable colouredDrawable; - public DrawableChatUsername(APIUser user, long? messageId) + public DrawableChatUsername(APIUser user) { this.user = user; - this.messageId = messageId; Action = openUserProfile; @@ -174,34 +172,12 @@ namespace osu.Game.Overlays.Chat } if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, this.ShowPopover)); + items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, ReportRequested)); return items.ToArray(); } } - public Popover GetPopover() => new ReportChatPopover(user) - { - Action = report - }; - - private void report(ChatReportReason reason, string comments) - { - var request = new ChatReportRequest(messageId, reason, comments); - - request.Failure += _ => Schedule(() => - { - currentChannel?.Value?.AddNewMessages(new ErrorMessage("Report failed to send, please retry")); - }); - - request.Success += () => Schedule(() => - { - currentChannel?.Value?.AddNewMessages(new InfoMessage("Report has been sent")); - }); - - api.Queue(request); - } - private void openUserChannel() { chatManager?.OpenPrivateChannel(user); diff --git a/osu.Game/Overlays/Chat/ReportChatPopover.cs b/osu.Game/Overlays/Chat/ReportChatPopover.cs index de5f3268b8..f02e52eb2e 100644 --- a/osu.Game/Overlays/Chat/ReportChatPopover.cs +++ b/osu.Game/Overlays/Chat/ReportChatPopover.cs @@ -1,22 +1,47 @@ // 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.Game.Graphics.UserInterfaceV2; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Chat { public partial class ReportChatPopover : ReportPopover { - public ReportChatPopover(APIUser? user) - : base(ReportStrings.UserTitle(user?.Username ?? @"Someone")) + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private ChannelManager channelManager { get; set; } = null!; + + private readonly Message message; + + public ReportChatPopover(Message message) + : base(ReportStrings.UserTitle(message.Sender?.Username ?? @"Someone")) { + this.message = message; + Action = report; } protected override bool CheckCanSubmitEmptyComment(ChatReportReason reason) { return reason != ChatReportReason.Other; } + + private void report(ChatReportReason reason, string comments) + { + var request = new ChatReportRequest(message.Id, reason, comments); + + request.Success += () => Schedule(() => + { + channelManager.CurrentChannel.Value.AddNewMessages(new InfoMessage(UsersStrings.ReportThanks.ToString())); + }); + + api.Queue(request); + } } } From 560f71ef5366a673cd0f7e79ba55ef16c4c73610 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 May 2023 14:29:03 +0900 Subject: [PATCH 67/80] Adjust `BlurredIcon` expansion ratio to fix glow getting cut off at lower resolutions Closes https://github.com/ppy/osu/issues/23210. Ballparked the fix to work down to the lowest resolution we support. The previous number (`2.5f`) was also likely ballparked so the fix seems in line with expectations. I don't want to put too much thought into this because the design of this screen is likely going to change in the mean time anyway. --- osu.Game/Screens/Play/Break/BlurredIcon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Break/BlurredIcon.cs b/osu.Game/Screens/Play/Break/BlurredIcon.cs index cd38390324..6ce1c2e686 100644 --- a/osu.Game/Screens/Play/Break/BlurredIcon.cs +++ b/osu.Game/Screens/Play/Break/BlurredIcon.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play.Break set { icon.Size = value; - base.Size = value + BlurSigma * 2.5f; + base.Size = value + BlurSigma * 5; ForceRedraw(); } get => base.Size; From 76b2f0e6dd57f50c956cc38ede5565aa6bda522e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 May 2023 15:15:18 +0900 Subject: [PATCH 68/80] Show slider velocity in hit object inspector --- osu.Game/Rulesets/Edit/HitObjectInspector.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Rulesets/Edit/HitObjectInspector.cs b/osu.Game/Rulesets/Edit/HitObjectInspector.cs index 977d00ede2..7eeee7a6e9 100644 --- a/osu.Game/Rulesets/Edit/HitObjectInspector.cs +++ b/osu.Game/Rulesets/Edit/HitObjectInspector.cs @@ -98,6 +98,12 @@ namespace osu.Game.Rulesets.Edit addValue($"{distance.Distance:#,0.##}px"); } + if (selected is IHasSliderVelocity sliderVelocity) + { + addHeader("Slider Velocity"); + addValue($"{sliderVelocity.SliderVelocity:#,0.00}x"); + } + if (selected is IHasRepeats repeats) { addHeader("Repeats"); From 4663057060ee71e79aba79bfd99594ba24ebad73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 May 2023 15:40:34 +0900 Subject: [PATCH 69/80] Split out `EditorInspector` implementation for reuse --- osu.Game/Rulesets/Edit/EditorInspector.cs | 50 ++++++++++ osu.Game/Rulesets/Edit/HitObjectInspector.cs | 99 ++++++-------------- 2 files changed, 79 insertions(+), 70 deletions(-) create mode 100644 osu.Game/Rulesets/Edit/EditorInspector.cs diff --git a/osu.Game/Rulesets/Edit/EditorInspector.cs b/osu.Game/Rulesets/Edit/EditorInspector.cs new file mode 100644 index 0000000000..ed7f305a28 --- /dev/null +++ b/osu.Game/Rulesets/Edit/EditorInspector.cs @@ -0,0 +1,50 @@ +// 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.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Screens.Edit; + +namespace osu.Game.Rulesets.Edit +{ + internal partial class EditorInspector : CompositeDrawable + { + protected OsuTextFlowContainer InspectorText = null!; + + [Resolved] + protected EditorBeatmap EditorBeatmap { get; private set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + + InternalChild = InspectorText = new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + } + + protected void AddHeader(string header) => InspectorText.AddParagraph($"{header}: ", s => + { + s.Padding = new MarginPadding { Top = 2 }; + s.Font = s.Font.With(size: 12); + s.Colour = colourProvider.Content2; + }); + + protected void AddValue(string value) => InspectorText.AddParagraph(value, s => + { + s.Font = s.Font.With(weight: FontWeight.SemiBold); + s.Colour = colourProvider.Content1; + }); + } +} diff --git a/osu.Game/Rulesets/Edit/HitObjectInspector.cs b/osu.Game/Rulesets/Edit/HitObjectInspector.cs index 7eeee7a6e9..d13190e484 100644 --- a/osu.Game/Rulesets/Edit/HitObjectInspector.cs +++ b/osu.Game/Rulesets/Edit/HitObjectInspector.cs @@ -2,43 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Extensions.TypeExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Threading; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Overlays; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Edit { - internal partial class HitObjectInspector : CompositeDrawable + internal partial class HitObjectInspector : EditorInspector { - private OsuTextFlowContainer inspectorText = null!; - - [Resolved] - protected EditorBeatmap EditorBeatmap { get; private set; } = null!; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - [BackgroundDependencyLoader] - private void load() - { - AutoSizeAxes = Axes.Y; - RelativeSizeAxes = Axes.X; - - InternalChild = inspectorText = new OsuTextFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }; - } - protected override void LoadComplete() { base.LoadComplete(); @@ -53,69 +25,69 @@ namespace osu.Game.Rulesets.Edit private void updateInspectorText() { - inspectorText.Clear(); + InspectorText.Clear(); rollingTextUpdate?.Cancel(); rollingTextUpdate = null; switch (EditorBeatmap.SelectedHitObjects.Count) { case 0: - addValue("No selection"); + AddValue("No selection"); break; case 1: var selected = EditorBeatmap.SelectedHitObjects.Single(); - addHeader("Type"); - addValue($"{selected.GetType().ReadableName()}"); + AddHeader("Type"); + AddValue($"{selected.GetType().ReadableName()}"); - addHeader("Time"); - addValue($"{selected.StartTime:#,0.##}ms"); + AddHeader("Time"); + AddValue($"{selected.StartTime:#,0.##}ms"); switch (selected) { case IHasPosition pos: - addHeader("Position"); - addValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}"); + AddHeader("Position"); + AddValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}"); break; case IHasXPosition x: - addHeader("Position"); + AddHeader("Position"); - addValue($"x:{x.X:#,0.##} "); + AddValue($"x:{x.X:#,0.##} "); break; case IHasYPosition y: - addHeader("Position"); + AddHeader("Position"); - addValue($"y:{y.Y:#,0.##}"); + AddValue($"y:{y.Y:#,0.##}"); break; } if (selected is IHasDistance distance) { - addHeader("Distance"); - addValue($"{distance.Distance:#,0.##}px"); + AddHeader("Distance"); + AddValue($"{distance.Distance:#,0.##}px"); } if (selected is IHasSliderVelocity sliderVelocity) { - addHeader("Slider Velocity"); - addValue($"{sliderVelocity.SliderVelocity:#,0.00}x"); + AddHeader("Slider Velocity"); + AddValue($"{sliderVelocity.SliderVelocity:#,0.00}x"); } if (selected is IHasRepeats repeats) { - addHeader("Repeats"); - addValue($"{repeats.RepeatCount:#,0.##}"); + AddHeader("Repeats"); + AddValue($"{repeats.RepeatCount:#,0.##}"); } if (selected is IHasDuration duration) { - addHeader("End Time"); - addValue($"{duration.EndTime:#,0.##}ms"); - addHeader("Duration"); - addValue($"{duration.Duration:#,0.##}ms"); + AddHeader("End Time"); + AddValue($"{duration.EndTime:#,0.##}ms"); + AddHeader("Duration"); + AddValue($"{duration.Duration:#,0.##}ms"); } // I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes. @@ -124,29 +96,16 @@ namespace osu.Game.Rulesets.Edit break; default: - addHeader("Selected Objects"); - addValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}"); + AddHeader("Selected Objects"); + AddValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}"); - addHeader("Start Time"); - addValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms"); + AddHeader("Start Time"); + AddValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms"); - addHeader("End Time"); - addValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms"); + AddHeader("End Time"); + AddValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms"); break; } - - void addHeader(string header) => inspectorText.AddParagraph($"{header}: ", s => - { - s.Padding = new MarginPadding { Top = 2 }; - s.Font = s.Font.With(size: 12); - s.Colour = colourProvider.Content2; - }); - - void addValue(string value) => inspectorText.AddParagraph(value, s => - { - s.Font = s.Font.With(weight: FontWeight.SemiBold); - s.Colour = colourProvider.Content1; - }); } } } From cc70d89bf94269d34b3e32cfc24a66884d26f6dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 May 2023 15:42:34 +0900 Subject: [PATCH 70/80] Move editor inspector classes out of ruleset namespace --- .../Edit/Compose/Components}/EditorInspector.cs | 3 +-- .../Edit/Compose/Components}/HitObjectInspector.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) rename osu.Game/{Rulesets/Edit => Screens/Edit/Compose/Components}/EditorInspector.cs (96%) rename osu.Game/{Rulesets/Edit => Screens/Edit/Compose/Components}/HitObjectInspector.cs (98%) diff --git a/osu.Game/Rulesets/Edit/EditorInspector.cs b/osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs similarity index 96% rename from osu.Game/Rulesets/Edit/EditorInspector.cs rename to osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs index ed7f305a28..442454f97a 100644 --- a/osu.Game/Rulesets/Edit/EditorInspector.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs @@ -7,9 +7,8 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays; -using osu.Game.Screens.Edit; -namespace osu.Game.Rulesets.Edit +namespace osu.Game.Screens.Edit.Compose.Components { internal partial class EditorInspector : CompositeDrawable { diff --git a/osu.Game/Rulesets/Edit/HitObjectInspector.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs similarity index 98% rename from osu.Game/Rulesets/Edit/HitObjectInspector.cs rename to osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs index d13190e484..597925e3e2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectInspector.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs @@ -7,7 +7,7 @@ using osu.Framework.Threading; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; -namespace osu.Game.Rulesets.Edit +namespace osu.Game.Screens.Edit.Compose.Components { internal partial class HitObjectInspector : EditorInspector { From b7a287869a5867c6ed5c2388e29ddff36ea94084 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 May 2023 15:54:27 +0900 Subject: [PATCH 71/80] Add display of beatmap slider velocity when adjusting --- .../Timeline/DifficultyPointPiece.cs | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 4741b75641..9192913fa0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -95,7 +95,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Text = "Hold shift while dragging the end of an object to adjust velocity while snapping." - } + }, + new SliderVelocityInspector(), } } }; @@ -105,7 +106,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).Where(o => o is IHasSliderVelocity).ToArray(); // even if there are multiple objects selected, we can still display a value if they all have the same value. - var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1 ? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable : null; + var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1 + ? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable + : null; if (selectedPointBindable != null) { @@ -139,4 +142,37 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } } + + internal partial class SliderVelocityInspector : EditorInspector + { + [BackgroundDependencyLoader] + private void load() + { + EditorBeatmap.TransactionBegan += updateInspectorText; + EditorBeatmap.TransactionEnded += updateInspectorText; + updateInspectorText(); + } + + private void updateInspectorText() + { + InspectorText.Clear(); + + double[] sliderVelocities = EditorBeatmap.HitObjects.OfType().Select(sv => sv.SliderVelocity).OrderBy(v => v).ToArray(); + + if (sliderVelocities.Length < 2) + return; + + double? modeSliderVelocity = sliderVelocities.GroupBy(v => v).MaxBy(v => v.Count())?.Key; + double? medianSliderVelocity = sliderVelocities[sliderVelocities.Length / 2]; + + AddHeader("Beatmap average velocity"); + AddValue($"{medianSliderVelocity:#,0.00}x"); + + AddHeader("Most used velocity"); + AddValue($"{modeSliderVelocity:#,0.00}x"); + + AddHeader("Velocity range"); + AddValue($"{sliderVelocities.First():#,0.00}x - {sliderVelocities.Last():#,0.00}x"); + } + } } From 3d55a0291f2c1378b0a28e360afd9a6189a4a5cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 May 2023 15:40:43 +0900 Subject: [PATCH 72/80] Prefer block scoped namespaces --- .editorconfig | 2 ++ osu.sln.DotSettings | 1 + 2 files changed, 3 insertions(+) diff --git a/.editorconfig b/.editorconfig index c0ea55f4c8..67c47000d3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -191,6 +191,8 @@ csharp_style_prefer_index_operator = false:silent csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none +csharp_style_namespace_declarations = block_scoped:warning + [*.{yaml,yml}] insert_final_newline = true indent_style = space diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 367dfccb71..d7486273fb 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -277,6 +277,7 @@ Explicit ExpressionBody BlockBody + BlockScoped ExplicitlyTyped True NEXT_LINE From 490df8073ca1f0950f7f22dda30317ac1ae82003 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 May 2023 21:15:26 +0900 Subject: [PATCH 73/80] Fix one incorrect namespace syntax file --- .../Objects/Types/IHasSliderVelocity.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs b/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs index c0ac5036ee..80fd8dd8dc 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasSliderVelocity.cs @@ -3,17 +3,18 @@ using osu.Framework.Bindables; -namespace osu.Game.Rulesets.Objects.Types; - -/// -/// A HitObject that has a slider velocity multiplier. -/// -public interface IHasSliderVelocity +namespace osu.Game.Rulesets.Objects.Types { /// - /// The slider velocity multiplier. + /// A HitObject that has a slider velocity multiplier. /// - double SliderVelocity { get; set; } + public interface IHasSliderVelocity + { + /// + /// The slider velocity multiplier. + /// + double SliderVelocity { get; set; } - BindableNumber SliderVelocityBindable { get; } + BindableNumber SliderVelocityBindable { get; } + } } From f536238554873f27cd8424e0a3547032b3c1e737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 5 May 2023 20:30:50 +0200 Subject: [PATCH 74/80] Use shorter copy --- .../Edit/Compose/Components/Timeline/DifficultyPointPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 9192913fa0..e737d6b0ce 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double? modeSliderVelocity = sliderVelocities.GroupBy(v => v).MaxBy(v => v.Count())?.Key; double? medianSliderVelocity = sliderVelocities[sliderVelocities.Length / 2]; - AddHeader("Beatmap average velocity"); + AddHeader("Average velocity"); AddValue($"{medianSliderVelocity:#,0.00}x"); AddHeader("Most used velocity"); From e808a47811d0027f82037cbc5d58b0c7117cffba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 5 May 2023 20:33:27 +0200 Subject: [PATCH 75/80] Fix delegate leak --- .../Compose/Components/Timeline/DifficultyPointPiece.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index e737d6b0ce..13a1c30cfe 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -174,5 +174,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AddHeader("Velocity range"); AddValue($"{sliderVelocities.First():#,0.00}x - {sliderVelocities.Last():#,0.00}x"); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + EditorBeatmap.TransactionBegan -= updateInspectorText; + EditorBeatmap.TransactionEnded -= updateInspectorText; + } } } From 52e5835be6e8ae544ec9917d67409a2493b04c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 6 May 2023 14:52:33 +0200 Subject: [PATCH 76/80] Document and retouch `ReportPopover` --- .../Graphics/UserInterfaceV2/ReportPopover.cs | 32 +++++++++++++------ osu.Game/Overlays/Chat/ReportChatPopover.cs | 5 +-- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs index a5c60bb9e6..393cdef76d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs @@ -16,17 +16,29 @@ using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 { - public abstract partial class ReportPopover : OsuPopover - where T : struct, Enum + /// + /// A generic popover for sending an online report about something. + /// + /// An enumeration type with all valid reasons for the report. + public abstract partial class ReportPopover : OsuPopover + where TReportReason : struct, Enum { - public Action? Action; + /// + /// The action to run when the report is finalised. + /// The arguments to this action are: the reason for the report, and an optional additional comment. + /// + public Action? Action; - private OsuEnumDropdown reasonDropdown = null!; + private OsuEnumDropdown reasonDropdown = null!; private OsuTextBox commentsTextBox = null!; private RoundedButton submitButton = null!; private readonly LocalisableString header; + /// + /// Creates a new . + /// + /// The text to display in the header of the popover. protected ReportPopover(LocalisableString headerString) { header = headerString; @@ -68,7 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { RelativeSizeAxes = Axes.X, Height = 40, - Child = reasonDropdown = new OsuEnumDropdown + Child = reasonDropdown = new OsuEnumDropdown { RelativeSizeAxes = Axes.X } @@ -110,12 +122,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void updateStatus() { - submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(commentsTextBox.Current.Value) || CheckCanSubmitEmptyComment(reasonDropdown.Current.Value); + submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(commentsTextBox.Current.Value) || IsCommentRequired(reasonDropdown.Current.Value); } - protected virtual bool CheckCanSubmitEmptyComment(T reason) - { - return false; - } + /// + /// Determines whether an additional comment is required for submitting the report with the supplied . + /// + protected virtual bool IsCommentRequired(TReportReason reason) => false; } } diff --git a/osu.Game/Overlays/Chat/ReportChatPopover.cs b/osu.Game/Overlays/Chat/ReportChatPopover.cs index f02e52eb2e..625f44505b 100644 --- a/osu.Game/Overlays/Chat/ReportChatPopover.cs +++ b/osu.Game/Overlays/Chat/ReportChatPopover.cs @@ -27,10 +27,7 @@ namespace osu.Game.Overlays.Chat Action = report; } - protected override bool CheckCanSubmitEmptyComment(ChatReportReason reason) - { - return reason != ChatReportReason.Other; - } + protected override bool IsCommentRequired(ChatReportReason reason) => reason != ChatReportReason.Other; private void report(ChatReportReason reason, string comments) { From 6b4050ea5253bd70c1a4c2d691d7c49d2c53fbab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 6 May 2023 15:01:57 +0200 Subject: [PATCH 77/80] Remove invalid report reason (and add osu-web references) --- osu.Game/Overlays/Chat/ChatReportReason.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatReportReason.cs b/osu.Game/Overlays/Chat/ChatReportReason.cs index 55593d29ad..5fda11b61e 100644 --- a/osu.Game/Overlays/Chat/ChatReportReason.cs +++ b/osu.Game/Overlays/Chat/ChatReportReason.cs @@ -7,6 +7,11 @@ using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Chat { + /// + /// References: + /// https://github.com/ppy/osu-web/blob/0a41b13acf5f47bb0d2b08bab42a9646b7ab5821/app/Models/UserReport.php#L50 + /// https://github.com/ppy/osu-web/blob/0a41b13acf5f47bb0d2b08bab42a9646b7ab5821/app/Models/UserReport.php#L39 + /// public enum ChatReportReason { [Description("Insulting People")] @@ -17,10 +22,6 @@ namespace osu.Game.Overlays.Chat [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsSpam))] Spam, - [Description("Cheating")] - [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsCheating))] - FoulPlay, - [Description("Unwanted Content")] [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsUnwantedContent))] UnwantedContent, From fba3c587cfd11278247840de290463dbc19e1201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 6 May 2023 15:26:29 +0200 Subject: [PATCH 78/80] Add test coverage for info message failing to display --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 99ce837665..55e6b54af7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -648,6 +648,7 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("Overlay closed", () => !this.ChildrenOfType().Any()); AddStep("Complete request", () => requestLock.Set()); AddUntilStep("Request sent", () => request != null); + AddUntilStep("Info message displayed", () => channelManager.CurrentChannel.Value.Messages.Last(), () => Is.InstanceOf(typeof(InfoMessage))); } private void joinTestChannel(int i) From 1da5e69d5a6aeb573d0d538e84257ecbf6752455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 6 May 2023 15:27:27 +0200 Subject: [PATCH 79/80] Fix API success callback scheduling to dead drawable --- osu.Game/Overlays/Chat/ReportChatPopover.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/ReportChatPopover.cs b/osu.Game/Overlays/Chat/ReportChatPopover.cs index 625f44505b..999c76ca10 100644 --- a/osu.Game/Overlays/Chat/ReportChatPopover.cs +++ b/osu.Game/Overlays/Chat/ReportChatPopover.cs @@ -33,10 +33,7 @@ namespace osu.Game.Overlays.Chat { var request = new ChatReportRequest(message.Id, reason, comments); - request.Success += () => Schedule(() => - { - channelManager.CurrentChannel.Value.AddNewMessages(new InfoMessage(UsersStrings.ReportThanks.ToString())); - }); + request.Success += () => channelManager.CurrentChannel.Value.AddNewMessages(new InfoMessage(UsersStrings.ReportThanks.ToString())); api.Queue(request); } From 3942281d13a4a23b3d8efbd2aed96e155831753d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 6 May 2023 15:34:55 +0200 Subject: [PATCH 80/80] Fix back-to-front logic --- osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs | 4 ++-- osu.Game/Overlays/Chat/ReportChatPopover.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs index 393cdef76d..7b3c32d60d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs @@ -122,12 +122,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void updateStatus() { - submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(commentsTextBox.Current.Value) || IsCommentRequired(reasonDropdown.Current.Value); + submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(commentsTextBox.Current.Value) || !IsCommentRequired(reasonDropdown.Current.Value); } /// /// Determines whether an additional comment is required for submitting the report with the supplied . /// - protected virtual bool IsCommentRequired(TReportReason reason) => false; + protected virtual bool IsCommentRequired(TReportReason reason) => true; } } diff --git a/osu.Game/Overlays/Chat/ReportChatPopover.cs b/osu.Game/Overlays/Chat/ReportChatPopover.cs index 999c76ca10..265a17c799 100644 --- a/osu.Game/Overlays/Chat/ReportChatPopover.cs +++ b/osu.Game/Overlays/Chat/ReportChatPopover.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Chat Action = report; } - protected override bool IsCommentRequired(ChatReportReason reason) => reason != ChatReportReason.Other; + protected override bool IsCommentRequired(ChatReportReason reason) => reason == ChatReportReason.Other; private void report(ChatReportReason reason, string comments) {