From 6e812ebd566c1598303fb2e7ccf5a3cd13f4458e Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 00:45:33 +0100 Subject: [PATCH 001/670] Reimplement chat settings from stable --- osu.Game/Configuration/OsuConfigManager.cs | 12 ++++++- .../Online/AlertsAndPrivacySettings.cs | 32 +++++++++++++++++++ .../Sections/Online/InGameChatSettings.cs | 29 +++++++++++++++++ .../Settings/Sections/OnlineSection.cs | 4 ++- 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index e26021d930..ed562637d4 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -49,6 +49,12 @@ namespace osu.Game.Configuration Set(OsuSetting.ExternalLinkWarning, true); + Set(OsuSetting.ChatHighlightName, true); + Set(OsuSetting.ChatMessageNotification, true); + + Set(OsuSetting.HighlightWords, string.Empty); + Set(OsuSetting.IgnoreList, string.Empty); + // Audio Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); @@ -180,6 +186,10 @@ namespace osu.Game.Configuration ScalingSizeX, ScalingSizeY, UIScale, - IntroSequence + IntroSequence, + ChatHighlightName, + ChatMessageNotification, + HighlightWords, + IgnoreList } } diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs new file mode 100644 index 0000000000..d84bf4eb3f --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs @@ -0,0 +1,32 @@ +// 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.Game.Configuration; + +namespace osu.Game.Overlays.Settings.Sections.Online +{ + public class AlertsAndPrivacySettings : SettingsSubsection + { + protected override string Header => "Alerts and Privacy"; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = "Show a notification popup when someone says your name", + Bindable = config.GetBindable(OsuSetting.ChatHighlightName) + }, + new SettingsCheckbox + { + LabelText = "Show chat message notifications", + Bindable = config.GetBindable(OsuSetting.ChatMessageNotification) + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs new file mode 100644 index 0000000000..e9cb1477ad --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs @@ -0,0 +1,29 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Configuration; + +namespace osu.Game.Overlays.Settings.Sections.Online +{ + public class InGameChatSettings : SettingsSubsection + { + protected override string Header => "In-Game Chat"; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsTextBox + { + LabelText = "Chat ignore list (space-separated list)", + Bindable = config.GetBindable(OsuSetting.IgnoreList) + }, + new SettingsTextBox + { + LabelText = "Chat highlight words (space-separated list)", + Bindable = config.GetBindable(OsuSetting.HighlightWords) + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs index 80295690c0..67a2e881d0 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs @@ -16,7 +16,9 @@ namespace osu.Game.Overlays.Settings.Sections { Children = new Drawable[] { - new WebSettings() + new WebSettings(), + new AlertsAndPrivacySettings(), + new InGameChatSettings() }; } } From e8180ab153901844621d0877918dd918a43c9c73 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 00:45:55 +0100 Subject: [PATCH 002/670] Add ToString() method to message for better debugging --- osu.Game/Online/Chat/Message.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index 2e41038a59..3b0507eb0c 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -63,5 +63,7 @@ namespace osu.Game.Online.Chat // ReSharper disable once ImpureMethodCallOnReadonlyValueField public override int GetHashCode() => Id.GetHashCode(); + + public override string ToString() => $"[{ChannelId}] ({Id}) {Sender}: {Content}"; } } From 8dfc8929f11ed8b4be09bca362ce8e6bf83ad62b Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 00:48:22 +0100 Subject: [PATCH 003/670] Add chat and notification logic to DrawableChannel with alongside multiple helper methods --- osu.Game/Overlays/Chat/DrawableChannel.cs | 167 +++++++++++++++++++++- 1 file changed, 163 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index f831266b1b..8c5a2e68ef 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -12,6 +12,14 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Online.Chat; +using osu.Game.Overlays.Notifications; +using osu.Game.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Colour; +using osu.Game.Online.API; +using osu.Game.Configuration; +using osu.Framework.Bindables; +using osu.Game.Users; namespace osu.Game.Overlays.Chat { @@ -20,6 +28,22 @@ namespace osu.Game.Overlays.Chat public readonly Channel Channel; protected ChatLineContainer ChatLineFlow; private OsuScrollContainer scroll; + public ColourInfo HighlightColour { get; set; } + + [Resolved(CanBeNull = true)] + private NotificationOverlay notificationOverlay { get; set; } + + [Resolved(CanBeNull = true)] + private ChatOverlay chatOverlay { get; set; } + + [Resolved(CanBeNull = true)] + private ChannelManager channelManager { get; set; } + + private Bindable notifyOnMention; + private Bindable notifyOnChat; + private Bindable highlightWords; + private Bindable ignoreList; + private Bindable localUser; public DrawableChannel(Channel channel) { @@ -28,8 +52,15 @@ namespace osu.Game.Overlays.Chat } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours, OsuConfigManager config, IAPIProvider api) { + notifyOnMention = config.GetBindable(OsuSetting.ChatHighlightName); + notifyOnChat = config.GetBindable(OsuSetting.ChatMessageNotification); + highlightWords = config.GetBindable(OsuSetting.HighlightWords); + ignoreList = config.GetBindable(OsuSetting.IgnoreList); + localUser = api.LocalUser; + HighlightColour = colours.Blue; + Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, @@ -77,10 +108,14 @@ namespace osu.Game.Overlays.Chat private void newMessagesArrived(IEnumerable newMessages) { // Add up to last Channel.MAX_HISTORY messages - var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MaxHistory)); + var ignoredWords = getWords(ignoreList.Value); + var displayMessages = newMessages.Where(m => hasCaseInsensitive(getWords(m.Content), ignoredWords) == null); + displayMessages = displayMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MaxHistory)); ChatLineFlow.AddRange(displayMessages.Select(CreateChatLine)); + checkForMentions(displayMessages); + if (scroll.IsScrolledToEnd(10) || !ChatLineFlow.Children.Any() || newMessages.Any(m => m is LocalMessage)) scrollToEnd(); @@ -96,6 +131,63 @@ namespace osu.Game.Overlays.Chat } } + private void checkForMentions(IEnumerable messages) + { + // only send notifications when chat overlay is **closed** + if (chatOverlay?.IsPresent == true && channelManager?.CurrentChannel.Value == Channel) + return; + + foreach (var message in messages) + { + var words = getWords(message.Content); + var username = localUser.Value.Username; + + if (message.Sender.Username == username) + continue; + + if (notifyOnChat.Value && Channel.Type == ChannelType.PM) + { + var notification = new MentionNotification(Channel, message.Sender.Username, () => + { + channelManager.CurrentChannel.Value = Channel; + HighlightMessage(message); + }, true); + + notificationOverlay?.Post(notification); + continue; + } + + if (notifyOnMention.Value && anyCaseInsensitive(words, username)) + { + var notification = new MentionNotification(Channel, message.Sender.Username, () => + { + channelManager.CurrentChannel.Value = Channel; + HighlightMessage(message); + }, false); + + notificationOverlay?.Post(notification); + continue; + } + + if (!string.IsNullOrWhiteSpace(highlightWords.Value)) + { + var matchedWord = hasCaseInsensitive(words, getWords(highlightWords.Value)); + + if (matchedWord != null) + { + var notification = new MentionNotification(Channel, message.Sender.Username, matchedWord, () => + { + channelManager.CurrentChannel.Value = Channel; + HighlightMessage(message); + }); + + notificationOverlay?.Post(notification); + continue; + } + } + } + } + private void pendingMessageResolved(Message existing, Message updated) { var found = ChatLineFlow.Children.LastOrDefault(c => c.Message == existing); @@ -110,13 +202,31 @@ namespace osu.Game.Overlays.Chat } } - private void messageRemoved(Message removed) + public void HighlightMessage(Message message) { - ChatLineFlow.Children.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); + var chatLine = findChatLine(message); + scroll.ScrollTo(chatLine); + chatLine.FlashColour(HighlightColour, 5000, Easing.InExpo); } + private void messageRemoved(Message removed) + { + findChatLine(removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); + } + + private ChatLine findChatLine(Message message) => ChatLineFlow.Children.FirstOrDefault(c => c.Message == message); + private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd()); + private string[] getWords(string input) => input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + /// + /// Finds the first matching string/word in both and (case-insensitive) + /// + private string hasCaseInsensitive(IEnumerable x, IEnumerable y) => x.FirstOrDefault(x2 => anyCaseInsensitive(y, x2)); + + private bool anyCaseInsensitive(IEnumerable x, string y) => x.Any(x2 => x2.Equals(y, StringComparison.InvariantCultureIgnoreCase)); + protected class ChatLineContainer : FillFlowContainer { protected override int Compare(Drawable x, Drawable y) @@ -127,5 +237,54 @@ namespace osu.Game.Overlays.Chat return xC.Message.CompareTo(yC.Message); } } + + private class MentionNotification : SimpleNotification + { + public MentionNotification(Channel channel, string username, Action onClick, bool isPm) : this(channel, onClick) + { + if (isPm) + { + Icon = FontAwesome.Solid.Envelope; + Text = $"You received a private message from '{username}'. Click to read it!"; + } + else + { + Icon = FontAwesome.Solid.At; + Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; + } + } + + public MentionNotification(Channel channel, string highlighter, string word, Action onClick) : this(channel, onClick) + { + Icon = FontAwesome.Solid.Highlighter; + Text = $"'{word}' was mentioned in chat by '{highlighter}'. Click to find out why!"; + } + + private MentionNotification(Channel channel, Action onClick) + { + Channel = channel; + this.onClick = onClick; + } + + private readonly Action onClick; + + public Channel Channel { get; } + + public override bool IsImportant => false; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) + { + IconBackgound.Colour = colours.PurpleDark; + Activated = delegate + { + notificationOverlay.Hide(); + chatOverlay.Show(); + onClick?.Invoke(); + + return true; + }; + } + } } } From 28d1fb181fae36a2bc35deac327e37a8b8d2e2e6 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 01:14:51 +0100 Subject: [PATCH 004/670] Add missing license header for InGameChatSettings.cs My unit tests fail at a solution filter, let's hope AppVeyor says yes. --- .../Overlays/Settings/Sections/Online/InGameChatSettings.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs index e9cb1477ad..4d8d06e557 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs @@ -1,4 +1,7 @@ -using osu.Framework.Allocation; +// 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.Game.Configuration; From 20670730b99604f6b26d7eb653839873a8632030 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 01:57:07 +0100 Subject: [PATCH 005/670] Resolve code formatting --- osu.Game/Overlays/Chat/DrawableChannel.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 7eec3bf18d..66ba2d1076 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -218,7 +218,6 @@ namespace osu.Game.Overlays.Chat }); notificationOverlay?.Post(notification); - continue; } } } @@ -331,7 +330,8 @@ namespace osu.Game.Overlays.Chat private class MentionNotification : SimpleNotification { - public MentionNotification(Channel channel, string username, Action onClick, bool isPm) : this(channel, onClick) + public MentionNotification(Channel channel, string username, Action onClick, bool isPm) + : this(channel, onClick) { if (isPm) { @@ -345,7 +345,8 @@ namespace osu.Game.Overlays.Chat } } - public MentionNotification(Channel channel, string highlighter, string word, Action onClick) : this(channel, onClick) + public MentionNotification(Channel channel, string highlighter, string word, Action onClick) + : this(channel, onClick) { Icon = FontAwesome.Solid.Highlighter; Text = $"'{word}' was mentioned in chat by '{highlighter}'. Click to find out why!"; @@ -353,13 +354,13 @@ namespace osu.Game.Overlays.Chat private MentionNotification(Channel channel, Action onClick) { - Channel = channel; + this.channel = channel; this.onClick = onClick; } private readonly Action onClick; - public Channel Channel { get; } + private readonly Channel channel; public override bool IsImportant => false; From 8b14090c950df960194e5f1763fea694ba1c7695 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 02:13:26 +0100 Subject: [PATCH 006/670] Remove unused field --- osu.Game/Overlays/Chat/DrawableChannel.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 66ba2d1076..0ca3129d6c 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -183,7 +183,7 @@ namespace osu.Game.Overlays.Chat if (notifyOnChat.Value && Channel.Type == ChannelType.PM) { - var notification = new MentionNotification(Channel, message.Sender.Username, () => + var notification = new MentionNotification(message.Sender.Username, () => { channelManager.CurrentChannel.Value = Channel; HighlightMessage(message); @@ -195,7 +195,7 @@ namespace osu.Game.Overlays.Chat if (notifyOnMention.Value && anyCaseInsensitive(words, username)) { - var notification = new MentionNotification(Channel, message.Sender.Username, () => + var notification = new MentionNotification(message.Sender.Username, () => { channelManager.CurrentChannel.Value = Channel; HighlightMessage(message); @@ -211,7 +211,7 @@ namespace osu.Game.Overlays.Chat if (matchedWord != null) { - var notification = new MentionNotification(Channel, message.Sender.Username, matchedWord, () => + var notification = new MentionNotification(message.Sender.Username, matchedWord, () => { channelManager.CurrentChannel.Value = Channel; HighlightMessage(message); @@ -330,8 +330,8 @@ namespace osu.Game.Overlays.Chat private class MentionNotification : SimpleNotification { - public MentionNotification(Channel channel, string username, Action onClick, bool isPm) - : this(channel, onClick) + public MentionNotification(string username, Action onClick, bool isPm) + : this(onClick) { if (isPm) { @@ -345,23 +345,20 @@ namespace osu.Game.Overlays.Chat } } - public MentionNotification(Channel channel, string highlighter, string word, Action onClick) - : this(channel, onClick) + public MentionNotification(string highlighter, string word, Action onClick) + : this(onClick) { Icon = FontAwesome.Solid.Highlighter; Text = $"'{word}' was mentioned in chat by '{highlighter}'. Click to find out why!"; } - private MentionNotification(Channel channel, Action onClick) + private MentionNotification(Action onClick) { - this.channel = channel; this.onClick = onClick; } private readonly Action onClick; - private readonly Channel channel; - public override bool IsImportant => false; [BackgroundDependencyLoader] From 81d994abeda12ff1e0ddf635c77363f82ae96b4f Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 03:22:14 +0100 Subject: [PATCH 007/670] Change ChatMessageNotification's LabelText --- .../Settings/Sections/Online/AlertsAndPrivacySettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs index d84bf4eb3f..0898ce3b84 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.Online }, new SettingsCheckbox { - LabelText = "Show chat message notifications", + LabelText = "Show private message notifications", Bindable = config.GetBindable(OsuSetting.ChatMessageNotification) }, }; From eb3f851ce27eb3496c8f9d8882f81fc0f246630a Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 03:22:55 +0100 Subject: [PATCH 008/670] Split Notification class into three separate ones --- osu.Game/Overlays/Chat/DrawableChannel.cs | 84 ++++++++++++++++------- 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 0ca3129d6c..bbfdb2dece 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -169,7 +169,7 @@ namespace osu.Game.Overlays.Chat private void checkForMentions(IEnumerable messages) { - // only send notifications when chat overlay is **closed** + // only send notifications when the chat overlay is **closed** and the channel is not visible. if (chatOverlay?.IsPresent == true && channelManager?.CurrentChannel.Value == Channel) return; @@ -183,11 +183,11 @@ namespace osu.Game.Overlays.Chat if (notifyOnChat.Value && Channel.Type == ChannelType.PM) { - var notification = new MentionNotification(message.Sender.Username, () => + var notification = new PrivateMessageNotification(message.Sender.Username, () => { channelManager.CurrentChannel.Value = Channel; HighlightMessage(message); - }, true); + }); notificationOverlay?.Post(notification); continue; @@ -199,7 +199,7 @@ namespace osu.Game.Overlays.Chat { channelManager.CurrentChannel.Value = Channel; HighlightMessage(message); - }, false); + }); notificationOverlay?.Post(notification); continue; @@ -211,7 +211,7 @@ namespace osu.Game.Overlays.Chat if (matchedWord != null) { - var notification = new MentionNotification(message.Sender.Username, matchedWord, () => + var notification = new HighlightNotification(message.Sender.Username, matchedWord, () => { channelManager.CurrentChannel.Value = Channel; HighlightMessage(message); @@ -328,32 +328,68 @@ namespace osu.Game.Overlays.Chat } } - private class MentionNotification : SimpleNotification + private class HighlightNotification : SimpleNotification { - public MentionNotification(string username, Action onClick, bool isPm) - : this(onClick) - { - if (isPm) - { - Icon = FontAwesome.Solid.Envelope; - Text = $"You received a private message from '{username}'. Click to read it!"; - } - else - { - Icon = FontAwesome.Solid.At; - Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; - } - } - - public MentionNotification(string highlighter, string word, Action onClick) - : this(onClick) + public HighlightNotification(string highlighter, string word, Action onClick) { Icon = FontAwesome.Solid.Highlighter; Text = $"'{word}' was mentioned in chat by '{highlighter}'. Click to find out why!"; + this.onClick = onClick; } - private MentionNotification(Action onClick) + private readonly Action onClick; + + public override bool IsImportant => false; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) { + IconBackgound.Colour = colours.PurpleDark; + Activated = delegate + { + notificationOverlay.Hide(); + chatOverlay.Show(); + onClick?.Invoke(); + + return true; + }; + } + } + + private class PrivateMessageNotification : SimpleNotification + { + public PrivateMessageNotification(string username, Action onClick) + { + Icon = FontAwesome.Solid.Envelope; + Text = $"You received a private message from '{username}'. Click to read it!"; + this.onClick = onClick; + } + + private readonly Action onClick; + + public override bool IsImportant => false; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) + { + IconBackgound.Colour = colours.PurpleDark; + Activated = delegate + { + notificationOverlay.Hide(); + chatOverlay.Show(); + onClick?.Invoke(); + + return true; + }; + } + } + + private class MentionNotification : SimpleNotification + { + public MentionNotification(string username, Action onClick) + { + Icon = FontAwesome.Solid.At; + Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; this.onClick = onClick; } From 0225372e8347a4cbafd8aff855ac52e614289691 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 03:24:07 +0100 Subject: [PATCH 009/670] Rename method to ScrollToAndHighlightMessage --- osu.Game/Overlays/Chat/DrawableChannel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index bbfdb2dece..ef4ab25df7 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -186,7 +186,7 @@ namespace osu.Game.Overlays.Chat var notification = new PrivateMessageNotification(message.Sender.Username, () => { channelManager.CurrentChannel.Value = Channel; - HighlightMessage(message); + ScrollToAndHighlightMessage(message); }); notificationOverlay?.Post(notification); @@ -198,7 +198,7 @@ namespace osu.Game.Overlays.Chat var notification = new MentionNotification(message.Sender.Username, () => { channelManager.CurrentChannel.Value = Channel; - HighlightMessage(message); + ScrollToAndHighlightMessage(message); }); notificationOverlay?.Post(notification); @@ -214,7 +214,7 @@ namespace osu.Game.Overlays.Chat var notification = new HighlightNotification(message.Sender.Username, matchedWord, () => { channelManager.CurrentChannel.Value = Channel; - HighlightMessage(message); + ScrollToAndHighlightMessage(message); }); notificationOverlay?.Post(notification); @@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Chat } } - public void HighlightMessage(Message message) + public void ScrollToAndHighlightMessage(Message message) { var chatLine = findChatLine(message); scroll.ScrollTo(chatLine); From 997b51b1f86679239034ff157e72f28cd6cbb118 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 03:26:30 +0100 Subject: [PATCH 010/670] Make messageRemoved use helper method --- osu.Game/Overlays/Chat/DrawableChannel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index ef4ab25df7..6813b3464d 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -246,7 +246,7 @@ namespace osu.Game.Overlays.Chat private void messageRemoved(Message removed) { - chatLines.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); + findChatLine(removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); } private IEnumerable chatLines => ChatLineFlow.Children.OfType(); From 1a1253a4aa66d7bc917322f56d05a61fc627bcc2 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 16 Dec 2019 03:27:19 +0100 Subject: [PATCH 011/670] Add null check to ScrollToAndHighlightMessage --- osu.Game/Overlays/Chat/DrawableChannel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 6813b3464d..1ca65a1da7 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -239,6 +239,9 @@ namespace osu.Game.Overlays.Chat public void ScrollToAndHighlightMessage(Message message) { + if (message is null) + return; + var chatLine = findChatLine(message); scroll.ScrollTo(chatLine); chatLine.FlashColour(HighlightColour, 5000, Easing.InExpo); From bea34e3aab4b37822ee3792b53851a6aba0ffaa5 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 17 Dec 2019 06:55:48 +0100 Subject: [PATCH 012/670] Make it possible to retrieve notifications from NotificationOverlay --- osu.Game/Overlays/NotificationOverlay.cs | 3 +++ osu.Game/Overlays/Notifications/NotificationSection.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 41160d10ec..2ae17b143a 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -13,6 +13,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Threading; +using System.Collections.Generic; namespace osu.Game.Overlays { @@ -22,6 +23,8 @@ namespace osu.Game.Overlays public const float TRANSITION_LENGTH = 600; + public IEnumerable Notifications => sections.Children.SelectMany(s => s.Notifications); + private FlowContainer sections; /// diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 17a2d4cf9f..320c0d6cb1 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -21,6 +21,8 @@ namespace osu.Game.Overlays.Notifications private FlowContainer notifications; + public IEnumerable Notifications => notifications.Children; + public int DisplayedCount => notifications.Count(n => !n.WasClosed); public int UnreadCount => notifications.Count(n => !n.WasClosed && !n.Read); From 02dc70be022a3837f556412d5dbf1b8725a27bee Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 17 Dec 2019 06:56:05 +0100 Subject: [PATCH 013/670] Make it possible to retrieve loaded channel drawables in ChatOverlay --- osu.Game/Overlays/ChatOverlay.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 33bcc4c139..bceb47c484 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -438,6 +438,11 @@ namespace osu.Game.Overlays textbox.Text = string.Empty; } + /// + /// Returns the loaded drawable for a channel. Returns null if not found. + /// + public DrawableChannel GetChannelDrawable(Channel channel) => loadedChannels.Find(drawable => drawable.Channel == channel); + private class TabsArea : Container { // IsHovered is used From b6c31e7764fb339907bcdea63a90e0cbb0f09637 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 17 Dec 2019 06:59:27 +0100 Subject: [PATCH 014/670] Remove ignore list, move code to MessageNotifier and add it to DI This also adds countable private message notifications. --- osu.Game/Configuration/OsuConfigManager.cs | 2 - osu.Game/Online/Chat/ChannelManager.cs | 14 +- osu.Game/Online/Chat/MessageNotifier.cs | 231 ++++++++++++++++++ osu.Game/OsuGame.cs | 3 + osu.Game/Overlays/Chat/DrawableChannel.cs | 177 +------------- .../Sections/Online/InGameChatSettings.cs | 7 +- 6 files changed, 250 insertions(+), 184 deletions(-) create mode 100644 osu.Game/Online/Chat/MessageNotifier.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index cb4feb360c..93d9068a2e 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -53,7 +53,6 @@ namespace osu.Game.Configuration Set(OsuSetting.ChatMessageNotification, true); Set(OsuSetting.HighlightWords, string.Empty); - Set(OsuSetting.IgnoreList, string.Empty); // Audio Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); @@ -198,7 +197,6 @@ namespace osu.Game.Configuration ChatHighlightName, ChatMessageNotification, HighlightWords, - IgnoreList, UIHoldActivationDelay, HitLighting, MenuBackgroundSource diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 1d8c5609d9..937acf2128 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -50,6 +50,9 @@ namespace osu.Game.Online.Chat private IAPIProvider api; + [Resolved] + private MessageNotifier messageNotifier { get; set; } + public readonly BindableBool HighPollRate = new BindableBool(); public ChannelManager() @@ -247,7 +250,16 @@ namespace osu.Game.Online.Chat var channels = JoinedChannels.ToList(); foreach (var group in messages.GroupBy(m => m.ChannelId)) - channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); + { + var channel = channels.Find(c => c.Id == group.Key); + + if (channel == null) + continue; + + var groupArray = group.ToArray(); + channel.AddNewMessages(groupArray); + messageNotifier.HandleMessages(channel, groupArray); + } } private void initializeChannels() diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs new file mode 100644 index 0000000000..61ec7351c4 --- /dev/null +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Users; + +namespace osu.Game.Online.Chat +{ + /// + /// Component that handles creating and posting notifications for incoming messages. + /// + public class MessageNotifier : Component + { + [Resolved(CanBeNull = true)] + private NotificationOverlay notificationOverlay { get; set; } + + [Resolved(CanBeNull = true)] + private ChatOverlay chatOverlay { get; set; } + + [Resolved(CanBeNull = true)] + private ChannelManager channelManager { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + private Bindable notifyOnMention; + private Bindable notifyOnChat; + private Bindable highlightWords; + private Bindable localUser; + + /// + /// Determines if the user is able to see incoming messages. + /// + public bool IsActive => chatOverlay?.IsPresent == true; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OsuConfigManager config, IAPIProvider api) + { + notifyOnMention = config.GetBindable(OsuSetting.ChatHighlightName); + notifyOnChat = config.GetBindable(OsuSetting.ChatMessageNotification); + highlightWords = config.GetBindable(OsuSetting.HighlightWords); + localUser = api.LocalUser; + } + + public void HandleMessages(Channel channel, IEnumerable messages) + { + // don't show if visible or not visible + if (IsActive && channelManager.CurrentChannel.Value == channel) + return; + + var channelDrawable = chatOverlay.GetChannelDrawable(channel); + if (channelDrawable == null) + return; + + foreach (var message in messages) + { + var words = getWords(message.Content); + var localUsername = localUser.Value.Username; + + if (message.Sender.Username == localUsername) + continue; + + void onClick() + { + if (channelManager != null) + channelManager.CurrentChannel.Value = channel; + + channelDrawable.ScrollToAndHighlightMessage(message); + } + + if (notifyOnChat.Value && channel.Type == ChannelType.PM) + { + var username = message.Sender.Username; + var existingNotification = notificationOverlay.Notifications.OfType().FirstOrDefault(n => n.Username == username); + + if (existingNotification == null) + { + var notification = new PrivateMessageNotification(username, onClick); + notificationOverlay?.Post(notification); + } + else + { + existingNotification.MessageCount++; + } + + continue; + } + if (notifyOnMention.Value && anyCaseInsensitive(words, localUsername)) + { + var notification = new MentionNotification(message.Sender.Username, onClick); + notificationOverlay?.Post(notification); + + continue; + } + if (!string.IsNullOrWhiteSpace(highlightWords.Value)) + { + var matchedWord = hasCaseInsensitive(words, getWords(highlightWords.Value)); + + if (matchedWord != null) + { + var notification = new HighlightNotification(message.Sender.Username, matchedWord, onClick); + notificationOverlay?.Post(notification); + } + } + } + } + + private static string[] getWords(string input) => input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + /// + /// Finds the first matching string/word in both and (case-insensitive) + /// + private static string hasCaseInsensitive(IEnumerable x, IEnumerable y) => x.FirstOrDefault(x2 => anyCaseInsensitive(y, x2)); + + private static bool anyCaseInsensitive(IEnumerable x, string y) => x.Any(x2 => x2.Equals(y, StringComparison.InvariantCultureIgnoreCase)); + + private class HighlightNotification : SimpleNotification + { + public HighlightNotification(string highlighter, string word, Action onClick) + { + Icon = FontAwesome.Solid.Highlighter; + Text = $"'{word}' was mentioned in chat by '{highlighter}'. Click to find out why!"; + this.onClick = onClick; + } + + private readonly Action onClick; + + public override bool IsImportant => false; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) + { + IconBackgound.Colour = colours.PurpleDark; + Activated = delegate + { + notificationOverlay.Hide(); + chatOverlay.Show(); + onClick?.Invoke(); + + return true; + }; + } + } + + private class PrivateMessageNotification : SimpleNotification + { + public PrivateMessageNotification(string username, Action onClick) + { + Icon = FontAwesome.Solid.Envelope; + Username = username; + MessageCount = 1; + this.onClick = onClick; + } + + private int messageCount = 0; + + public int MessageCount + { + get => messageCount; + set + { + messageCount = value; + if (messageCount > 1) + { + Text = $"You received {messageCount} private messages from '{Username}'. Click to read it!"; + } + else + { + Text = $"You received a private message from '{Username}'. Click to read it!"; + } + } + } + + public string Username { get; set; } + + private readonly Action onClick; + + public override bool IsImportant => false; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) + { + IconBackgound.Colour = colours.PurpleDark; + Activated = delegate + { + notificationOverlay.Hide(); + chatOverlay.Show(); + onClick?.Invoke(); + + return true; + }; + } + } + + private class MentionNotification : SimpleNotification + { + public MentionNotification(string username, Action onClick) + { + Icon = FontAwesome.Solid.At; + Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; + this.onClick = onClick; + } + + private readonly Action onClick; + + public override bool IsImportant => false; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) + { + IconBackgound.Colour = colours.PurpleDark; + Activated = delegate + { + notificationOverlay.Hide(); + chatOverlay.Show(); + onClick?.Invoke(); + + return true; + }; + } + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c7c746bed3..d89109e9b9 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -58,6 +58,8 @@ namespace osu.Game private ChannelManager channelManager; + private MessageNotifier messageNotifier; + private NotificationOverlay notifications; private DirectOverlay direct; @@ -589,6 +591,7 @@ namespace osu.Game loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true); loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true); loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); + loadComponentSingleFile(messageNotifier = new MessageNotifier(), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(Settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true); var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true); diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 1ca65a1da7..74aac2a7cf 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -33,21 +33,6 @@ namespace osu.Game.Overlays.Chat private OsuScrollContainer scroll; public ColourInfo HighlightColour { get; set; } - [Resolved(CanBeNull = true)] - private NotificationOverlay notificationOverlay { get; set; } - - [Resolved(CanBeNull = true)] - private ChatOverlay chatOverlay { get; set; } - - [Resolved(CanBeNull = true)] - private ChannelManager channelManager { get; set; } - - private Bindable notifyOnMention; - private Bindable notifyOnChat; - private Bindable highlightWords; - private Bindable ignoreList; - private Bindable localUser; - [Resolved] private OsuColour colours { get; set; } @@ -58,13 +43,8 @@ namespace osu.Game.Overlays.Chat } [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager config, IAPIProvider api) + private void load(OsuColour colours) { - notifyOnMention = config.GetBindable(OsuSetting.ChatHighlightName); - notifyOnChat = config.GetBindable(OsuSetting.ChatMessageNotification); - highlightWords = config.GetBindable(OsuSetting.HighlightWords); - ignoreList = config.GetBindable(OsuSetting.IgnoreList); - localUser = api.LocalUser; HighlightColour = colours.Blue; Child = new OsuContextMenuContainer @@ -122,14 +102,10 @@ namespace osu.Game.Overlays.Chat bool shouldScrollToEnd = scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage); // Add up to last Channel.MAX_HISTORY messages - var ignoredWords = getWords(ignoreList.Value); - var displayMessages = newMessages.Where(m => hasCaseInsensitive(getWords(m.Content), ignoredWords) == null); - displayMessages = displayMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY)); + var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY)); Message lastMessage = chatLines.LastOrDefault()?.Message; - checkForMentions(displayMessages); - foreach (var message in displayMessages) { if (lastMessage == null || lastMessage.Timestamp.ToLocalTime().Date != message.Timestamp.ToLocalTime().Date) @@ -167,62 +143,6 @@ namespace osu.Game.Overlays.Chat scrollToEnd(); } - private void checkForMentions(IEnumerable messages) - { - // only send notifications when the chat overlay is **closed** and the channel is not visible. - if (chatOverlay?.IsPresent == true && channelManager?.CurrentChannel.Value == Channel) - return; - - foreach (var message in messages) - { - var words = getWords(message.Content); - var username = localUser.Value.Username; - - if (message.Sender.Username == username) - continue; - - if (notifyOnChat.Value && Channel.Type == ChannelType.PM) - { - var notification = new PrivateMessageNotification(message.Sender.Username, () => - { - channelManager.CurrentChannel.Value = Channel; - ScrollToAndHighlightMessage(message); - }); - - notificationOverlay?.Post(notification); - continue; - } - - if (notifyOnMention.Value && anyCaseInsensitive(words, username)) - { - var notification = new MentionNotification(message.Sender.Username, () => - { - channelManager.CurrentChannel.Value = Channel; - ScrollToAndHighlightMessage(message); - }); - - notificationOverlay?.Post(notification); - continue; - } - - if (!string.IsNullOrWhiteSpace(highlightWords.Value)) - { - var matchedWord = hasCaseInsensitive(words, getWords(highlightWords.Value)); - - if (matchedWord != null) - { - var notification = new HighlightNotification(message.Sender.Username, matchedWord, () => - { - channelManager.CurrentChannel.Value = Channel; - ScrollToAndHighlightMessage(message); - }); - - notificationOverlay?.Post(notification); - } - } - } - } - private void pendingMessageResolved(Message existing, Message updated) { var found = chatLines.LastOrDefault(c => c.Message == existing); @@ -256,15 +176,6 @@ namespace osu.Game.Overlays.Chat private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd()); - private string[] getWords(string input) => input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - - /// - /// Finds the first matching string/word in both and (case-insensitive) - /// - private string hasCaseInsensitive(IEnumerable x, IEnumerable y) => x.FirstOrDefault(x2 => anyCaseInsensitive(y, x2)); - - private bool anyCaseInsensitive(IEnumerable x, string y) => x.Any(x2 => x2.Equals(y, StringComparison.InvariantCultureIgnoreCase)); - private ChatLine findChatLine(Message message) => chatLines.FirstOrDefault(c => c.Message == message); public class DaySeparator : Container @@ -330,89 +241,5 @@ namespace osu.Game.Overlays.Chat }; } } - - private class HighlightNotification : SimpleNotification - { - public HighlightNotification(string highlighter, string word, Action onClick) - { - Icon = FontAwesome.Solid.Highlighter; - Text = $"'{word}' was mentioned in chat by '{highlighter}'. Click to find out why!"; - this.onClick = onClick; - } - - private readonly Action onClick; - - public override bool IsImportant => false; - - [BackgroundDependencyLoader] - private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) - { - IconBackgound.Colour = colours.PurpleDark; - Activated = delegate - { - notificationOverlay.Hide(); - chatOverlay.Show(); - onClick?.Invoke(); - - return true; - }; - } - } - - private class PrivateMessageNotification : SimpleNotification - { - public PrivateMessageNotification(string username, Action onClick) - { - Icon = FontAwesome.Solid.Envelope; - Text = $"You received a private message from '{username}'. Click to read it!"; - this.onClick = onClick; - } - - private readonly Action onClick; - - public override bool IsImportant => false; - - [BackgroundDependencyLoader] - private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) - { - IconBackgound.Colour = colours.PurpleDark; - Activated = delegate - { - notificationOverlay.Hide(); - chatOverlay.Show(); - onClick?.Invoke(); - - return true; - }; - } - } - - private class MentionNotification : SimpleNotification - { - public MentionNotification(string username, Action onClick) - { - Icon = FontAwesome.Solid.At; - Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; - this.onClick = onClick; - } - - private readonly Action onClick; - - public override bool IsImportant => false; - - [BackgroundDependencyLoader] - private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) - { - IconBackgound.Colour = colours.PurpleDark; - Activated = delegate - { - notificationOverlay.Hide(); - chatOverlay.Show(); - onClick?.Invoke(); - - return true; - }; - } - } } } diff --git a/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs index 4d8d06e557..781aa10618 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs @@ -16,16 +16,11 @@ namespace osu.Game.Overlays.Settings.Sections.Online { Children = new Drawable[] { - new SettingsTextBox - { - LabelText = "Chat ignore list (space-separated list)", - Bindable = config.GetBindable(OsuSetting.IgnoreList) - }, new SettingsTextBox { LabelText = "Chat highlight words (space-separated list)", Bindable = config.GetBindable(OsuSetting.HighlightWords) - }, + } }; } } From 7bdfd2e23ce1083cc52db42d021e6a125bba97a5 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 17 Dec 2019 07:04:55 +0100 Subject: [PATCH 015/670] All copyright goes to peppy --- osu.Game/Online/Chat/MessageNotifier.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 61ec7351c4..9ee5e90be8 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; From 0d812bce9f90a8a77cd413386e9a6c4cdea90c44 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 26 Dec 2019 03:32:40 +0100 Subject: [PATCH 016/670] WIP changes for code review --- osu.Game/Online/Chat/Channel.cs | 27 ++++++---- osu.Game/Online/Chat/ChannelManager.cs | 5 -- osu.Game/Online/Chat/MessageNotifier.cs | 65 +++++++++++++++++------ osu.Game/Overlays/Chat/DrawableChannel.cs | 6 +-- osu.Game/Overlays/ChatOverlay.cs | 41 ++++++++++++-- 5 files changed, 105 insertions(+), 39 deletions(-) diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 451174a73c..3e2a247d7f 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -44,7 +44,7 @@ namespace osu.Game.Online.Chat /// /// An event that fires when new messages arrived. /// - public event Action> NewMessagesArrived; + public event Action, bool> NewMessagesArrived; /// /// An event that fires when a pending message gets resolved. @@ -58,6 +58,11 @@ namespace osu.Game.Online.Chat public bool ReadOnly => false; //todo not yet used. + /// + /// Determines if the channel's previous messages have been loaded. + /// + public bool Populated { get; set; } = false; + public override string ToString() => Name; [JsonProperty(@"name")] @@ -105,7 +110,7 @@ namespace osu.Game.Online.Chat pendingMessages.Add(message); Messages.Add(message); - NewMessagesArrived?.Invoke(new[] { message }); + NewMessagesArrived?.Invoke(new[] { message }, Populated); } public bool MessagesLoaded; @@ -118,17 +123,21 @@ namespace osu.Game.Online.Chat { messages = messages.Except(Messages).ToArray(); - if (messages.Length == 0) return; + if (messages.Length != 0) + { + Messages.AddRange(messages); - Messages.AddRange(messages); + var maxMessageId = messages.Max(m => m.Id); + if (maxMessageId > LastMessageId) + LastMessageId = maxMessageId; - var maxMessageId = messages.Max(m => m.Id); - if (maxMessageId > LastMessageId) - LastMessageId = maxMessageId; + purgeOldMessages(); - purgeOldMessages(); + NewMessagesArrived?.Invoke(messages, Populated); + } - NewMessagesArrived?.Invoke(messages); + if (!Populated) + Populated = true; } /// diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 937acf2128..1bee12d8c8 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -49,10 +49,6 @@ namespace osu.Game.Online.Chat public IBindableList AvailableChannels => availableChannels; private IAPIProvider api; - - [Resolved] - private MessageNotifier messageNotifier { get; set; } - public readonly BindableBool HighPollRate = new BindableBool(); public ChannelManager() @@ -258,7 +254,6 @@ namespace osu.Game.Online.Chat var groupArray = group.ToArray(); channel.AddNewMessages(groupArray); - messageNotifier.HandleMessages(channel, groupArray); } } diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 9ee5e90be8..de079ce636 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Online.API; @@ -31,9 +32,6 @@ namespace osu.Game.Online.Chat [Resolved(CanBeNull = true)] private ChannelManager channelManager { get; set; } - [Resolved] - private OsuColour colours { get; set; } - private Bindable notifyOnMention; private Bindable notifyOnChat; private Bindable highlightWords; @@ -45,12 +43,53 @@ namespace osu.Game.Online.Chat public bool IsActive => chatOverlay?.IsPresent == true; [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager config, IAPIProvider api) + private void load(OsuConfigManager config, IAPIProvider api) { notifyOnMention = config.GetBindable(OsuSetting.ChatHighlightName); notifyOnChat = config.GetBindable(OsuSetting.ChatMessageNotification); highlightWords = config.GetBindable(OsuSetting.HighlightWords); localUser = api.LocalUser; + + // Listen for new messages + + channelManager.JoinedChannels.ItemsAdded += (joinedChannels) => + { + foreach (var channel in joinedChannels) + channel.NewMessagesArrived += channel_NewMessagesArrived; + }; + + channelManager.JoinedChannels.ItemsRemoved += (leftChannels) => + { + foreach (var channel in leftChannels) + channel.NewMessagesArrived -= channel_NewMessagesArrived; + }; + } + + private void channel_NewMessagesArrived(IEnumerable messages, bool populated) + { + if (messages == null || !messages.Any()) + return; + + if (!populated) + return; + + HandleMessages(messages.First().ChannelId, messages); + } + + /// + /// Resolves the channel id + /// + public void HandleMessages(long channelId, IEnumerable messages) + { + var channel = channelManager.JoinedChannels.FirstOrDefault(c => c.Id == channelId); + + if (channel == null) + { + Logger.Log($"Couldn't resolve channel id {channelId}", LoggingTarget.Information); + return; + } + + HandleMessages(channel, messages); } public void HandleMessages(Channel channel, IEnumerable messages) @@ -59,10 +98,6 @@ namespace osu.Game.Online.Chat if (IsActive && channelManager.CurrentChannel.Value == channel) return; - var channelDrawable = chatOverlay.GetChannelDrawable(channel); - if (channelDrawable == null) - return; - foreach (var message in messages) { var words = getWords(message.Content); @@ -73,20 +108,17 @@ namespace osu.Game.Online.Chat void onClick() { - if (channelManager != null) - channelManager.CurrentChannel.Value = channel; - - channelDrawable.ScrollToAndHighlightMessage(message); + chatOverlay.ScrollToAndHighlightMessage(channel, message); + chatOverlay.Show(); } if (notifyOnChat.Value && channel.Type == ChannelType.PM) { - var username = message.Sender.Username; - var existingNotification = notificationOverlay.Notifications.OfType().FirstOrDefault(n => n.Username == username); + var existingNotification = notificationOverlay.Notifications.OfType().FirstOrDefault(n => n.Username == message.Sender.Username); if (existingNotification == null) { - var notification = new PrivateMessageNotification(username, onClick); + var notification = new PrivateMessageNotification(message.Sender.Username, onClick); notificationOverlay?.Post(notification); } else @@ -139,13 +171,12 @@ namespace osu.Game.Online.Chat public override bool IsImportant => false; [BackgroundDependencyLoader] - private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) + private void load(OsuColour colours, NotificationOverlay notificationOverlay) { IconBackgound.Colour = colours.PurpleDark; Activated = delegate { notificationOverlay.Hide(); - chatOverlay.Show(); onClick?.Invoke(); return true; diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 74aac2a7cf..57ce7fed7c 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Chat }, }; - newMessagesArrived(Channel.Messages); + newMessagesArrived(Channel.Messages, Channel.Populated); Channel.NewMessagesArrived += newMessagesArrived; Channel.MessageRemoved += messageRemoved; @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Chat Colour = colours.ChatBlue.Lighten(0.7f), }; - private void newMessagesArrived(IEnumerable newMessages) + private void newMessagesArrived(IEnumerable newMessages, bool populated) { bool shouldScrollToEnd = scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage); @@ -164,7 +164,7 @@ namespace osu.Game.Overlays.Chat var chatLine = findChatLine(message); scroll.ScrollTo(chatLine); - chatLine.FlashColour(HighlightColour, 5000, Easing.InExpo); + chatLine.FlashColour(HighlightColour, 7500, Easing.InExpo); } private void messageRemoved(Message removed) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index bceb47c484..ab74439f9a 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -22,6 +22,7 @@ using osu.Game.Overlays.Chat.Selection; using osu.Game.Overlays.Chat.Tabs; using osuTK.Input; using osu.Framework.Graphics.Sprites; +using System; namespace osu.Game.Overlays { @@ -60,6 +61,8 @@ namespace osu.Game.Overlays private Container channelSelectionContainer; protected ChannelSelectionOverlay ChannelSelectionOverlay; + private Message highlightingMessage { get; set; } + public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos) || (ChannelSelectionOverlay.State.Value == Visibility.Visible && ChannelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos)); @@ -252,15 +255,14 @@ namespace osu.Game.Overlays if (ChannelTabControl.Current.Value != e.NewValue) Scheduler.Add(() => ChannelTabControl.Current.Value = e.NewValue); - var loaded = loadedChannels.Find(d => d.Channel == e.NewValue); + var loaded = GetChannelDrawable(e.NewValue); if (loaded == null) { currentChannelContainer.FadeOut(500, Easing.OutQuint); loading.Show(); - loaded = new DrawableChannel(e.NewValue); - loadedChannels.Add(loaded); + loaded = loadChannelDrawable(e.NewValue); LoadComponentAsync(loaded, l => { if (currentChannel.Value != e.NewValue) @@ -271,6 +273,12 @@ namespace osu.Game.Overlays currentChannelContainer.Clear(false); currentChannelContainer.Add(loaded); currentChannelContainer.FadeIn(500, Easing.OutQuint); + + if (highlightingMessage != null && highlightingMessage.ChannelId == e.NewValue.Id) + { + loaded.ScrollToAndHighlightMessage(highlightingMessage); + highlightingMessage = null; + } }); } else @@ -439,9 +447,32 @@ namespace osu.Game.Overlays } /// - /// Returns the loaded drawable for a channel. Returns null if not found. + /// Returns the loaded drawable for a channel. Creates new instance if is true. Otherwise returns null if not found. /// - public DrawableChannel GetChannelDrawable(Channel channel) => loadedChannels.Find(drawable => drawable.Channel == channel); + public DrawableChannel GetChannelDrawable(Channel channel, bool createIfUnloaded = false) + { + var result = loadedChannels.Find(drawable => drawable.Channel == channel); + + if (createIfUnloaded && result == null) + { + result = loadChannelDrawable(channel); + } + + return result; + } + + private DrawableChannel loadChannelDrawable(Channel channel) + { + var loaded = new DrawableChannel(channel); + loadedChannels.Add(loaded); + return loaded; + } + + public void ScrollToAndHighlightMessage(Channel channel, Message message) + { + highlightingMessage = message; + channelManager.CurrentChannel.Value = channel; + } private class TabsArea : Container { From 1b53c0ff7479ec59711882fa21086be4d9cd5cfe Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 16 Jan 2020 23:15:30 +0100 Subject: [PATCH 017/670] Remove populated property, and other changes --- osu.Game/Online/Chat/Channel.cs | 14 +--- osu.Game/Online/Chat/MessageNotifier.cs | 90 ++++++++++++++++------- osu.Game/Overlays/Chat/DrawableChannel.cs | 19 +---- osu.Game/Overlays/ChatOverlay.cs | 43 +---------- 4 files changed, 71 insertions(+), 95 deletions(-) diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 3e2a247d7f..3257774a27 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -44,7 +44,7 @@ namespace osu.Game.Online.Chat /// /// An event that fires when new messages arrived. /// - public event Action, bool> NewMessagesArrived; + public event Action> NewMessagesArrived; /// /// An event that fires when a pending message gets resolved. @@ -58,11 +58,6 @@ namespace osu.Game.Online.Chat public bool ReadOnly => false; //todo not yet used. - /// - /// Determines if the channel's previous messages have been loaded. - /// - public bool Populated { get; set; } = false; - public override string ToString() => Name; [JsonProperty(@"name")] @@ -110,7 +105,7 @@ namespace osu.Game.Online.Chat pendingMessages.Add(message); Messages.Add(message); - NewMessagesArrived?.Invoke(new[] { message }, Populated); + NewMessagesArrived?.Invoke(new[] { message }); } public bool MessagesLoaded; @@ -133,11 +128,8 @@ namespace osu.Game.Online.Chat purgeOldMessages(); - NewMessagesArrived?.Invoke(messages, Populated); + NewMessagesArrived?.Invoke(messages); } - - if (!Populated) - Populated = true; } /// diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index de079ce636..8663cf4793 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -51,7 +51,6 @@ namespace osu.Game.Online.Chat localUser = api.LocalUser; // Listen for new messages - channelManager.JoinedChannels.ItemsAdded += (joinedChannels) => { foreach (var channel in joinedChannels) @@ -65,14 +64,11 @@ namespace osu.Game.Online.Chat }; } - private void channel_NewMessagesArrived(IEnumerable messages, bool populated) + private void channel_NewMessagesArrived(IEnumerable messages) { if (messages == null || !messages.Any()) return; - if (!populated) - return; - HandleMessages(messages.First().ChannelId, messages); } @@ -94,7 +90,7 @@ namespace osu.Game.Online.Chat public void HandleMessages(Channel channel, IEnumerable messages) { - // don't show if visible or not visible + // don't show if the ChatOverlay and the channel is visible. if (IsActive && channelManager.CurrentChannel.Value == channel) return; @@ -108,26 +104,36 @@ namespace osu.Game.Online.Chat void onClick() { - chatOverlay.ScrollToAndHighlightMessage(channel, message); + notificationOverlay.Hide(); chatOverlay.Show(); + channelManager.CurrentChannel.Value = channel; } + + if (notifyOnChat.Value && channel.Type == ChannelType.PM) { - var existingNotification = notificationOverlay.Notifications.OfType().FirstOrDefault(n => n.Username == message.Sender.Username); + // Scheduling because of possible "race-condition" (NotificationOverlay didn't add the notification yet). + Schedule(() => + { + var existingNotification = notificationOverlay.Notifications.OfType() + .FirstOrDefault(n => n.Username == message.Sender.Username); - if (existingNotification == null) - { - var notification = new PrivateMessageNotification(message.Sender.Username, onClick); - notificationOverlay?.Post(notification); - } - else - { - existingNotification.MessageCount++; - } + if (existingNotification == null) + { + var notification = new PrivateMessageNotification(message.Sender.Username, onClick); + notificationOverlay?.Post(notification); + } + else + { + existingNotification.MessageCount++; + } + }); + continue; } + if (notifyOnMention.Value && anyCaseInsensitive(words, localUsername)) { var notification = new MentionNotification(message.Sender.Username, onClick); @@ -135,6 +141,7 @@ namespace osu.Game.Online.Chat continue; } + if (!string.IsNullOrWhiteSpace(highlightWords.Value)) { var matchedWord = hasCaseInsensitive(words, getWords(highlightWords.Value)); @@ -146,6 +153,40 @@ namespace osu.Game.Online.Chat } } } + + //making sure if the notification drawer bugs out, we merge it afterwards again. + Schedule(() => mergeNotifications()); + } + + /// + /// Checks current notifications if they aren't merged, and merges them together again. + /// + private void mergeNotifications() + { + if (notificationOverlay == null) + { + return; + } + + var pmn = notificationOverlay.Notifications.OfType(); + + foreach (var notification in pmn) + { + var duplicates = pmn.Where(n => n.Username == notification.Username); + + if (duplicates.Count() < 2) + continue; + + var first = duplicates.First(); + foreach (var notification2 in duplicates) + { + if (notification2 == first) + continue; + + first.MessageCount += notification2.MessageCount; + notification2.Close(); + } + } } private static string[] getWords(string input) => input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); @@ -171,14 +212,12 @@ namespace osu.Game.Online.Chat public override bool IsImportant => false; [BackgroundDependencyLoader] - private void load(OsuColour colours, NotificationOverlay notificationOverlay) + private void load(OsuColour colours) { IconBackgound.Colour = colours.PurpleDark; Activated = delegate { - notificationOverlay.Hide(); onClick?.Invoke(); - return true; }; } @@ -202,6 +241,7 @@ namespace osu.Game.Online.Chat set { messageCount = value; + if (messageCount > 1) { Text = $"You received {messageCount} private messages from '{Username}'. Click to read it!"; @@ -220,15 +260,12 @@ namespace osu.Game.Online.Chat public override bool IsImportant => false; [BackgroundDependencyLoader] - private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) + private void load(OsuColour colours) { IconBackgound.Colour = colours.PurpleDark; Activated = delegate { - notificationOverlay.Hide(); - chatOverlay.Show(); onClick?.Invoke(); - return true; }; } @@ -248,15 +285,12 @@ namespace osu.Game.Online.Chat public override bool IsImportant => false; [BackgroundDependencyLoader] - private void load(OsuColour colours, NotificationOverlay notificationOverlay, ChatOverlay chatOverlay) + private void load(OsuColour colours) { IconBackgound.Colour = colours.PurpleDark; Activated = delegate { - notificationOverlay.Hide(); - chatOverlay.Show(); onClick?.Invoke(); - return true; }; } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 57ce7fed7c..9c75e89249 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -8,17 +8,12 @@ using System.Linq; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Online.Chat; -using osu.Game.Overlays.Notifications; using osu.Game.Graphics; -using osu.Game.Online.API; -using osu.Game.Configuration; -using osu.Game.Users; using osu.Game.Graphics.Sprites; using osu.Framework.Graphics; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Sprites; @@ -67,7 +62,7 @@ namespace osu.Game.Overlays.Chat }, }; - newMessagesArrived(Channel.Messages, Channel.Populated); + newMessagesArrived(Channel.Messages); Channel.NewMessagesArrived += newMessagesArrived; Channel.MessageRemoved += messageRemoved; @@ -97,7 +92,7 @@ namespace osu.Game.Overlays.Chat Colour = colours.ChatBlue.Lighten(0.7f), }; - private void newMessagesArrived(IEnumerable newMessages, bool populated) + private void newMessagesArrived(IEnumerable newMessages) { bool shouldScrollToEnd = scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage); @@ -157,16 +152,6 @@ namespace osu.Game.Overlays.Chat } } - public void ScrollToAndHighlightMessage(Message message) - { - if (message is null) - return; - - var chatLine = findChatLine(message); - scroll.ScrollTo(chatLine); - chatLine.FlashColour(HighlightColour, 7500, Easing.InExpo); - } - private void messageRemoved(Message removed) { findChatLine(removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index ab74439f9a..c2716cd585 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -22,7 +22,6 @@ using osu.Game.Overlays.Chat.Selection; using osu.Game.Overlays.Chat.Tabs; using osuTK.Input; using osu.Framework.Graphics.Sprites; -using System; namespace osu.Game.Overlays { @@ -61,8 +60,6 @@ namespace osu.Game.Overlays private Container channelSelectionContainer; protected ChannelSelectionOverlay ChannelSelectionOverlay; - private Message highlightingMessage { get; set; } - public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos) || (ChannelSelectionOverlay.State.Value == Visibility.Visible && ChannelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos)); @@ -255,14 +252,16 @@ namespace osu.Game.Overlays if (ChannelTabControl.Current.Value != e.NewValue) Scheduler.Add(() => ChannelTabControl.Current.Value = e.NewValue); - var loaded = GetChannelDrawable(e.NewValue); + var loaded = loadedChannels.Find(drawable => drawable.Channel == e.NewValue); if (loaded == null) { currentChannelContainer.FadeOut(500, Easing.OutQuint); loading.Show(); - loaded = loadChannelDrawable(e.NewValue); + loaded = new DrawableChannel(e.NewValue); + loadedChannels.Add(loaded); + LoadComponentAsync(loaded, l => { if (currentChannel.Value != e.NewValue) @@ -273,12 +272,6 @@ namespace osu.Game.Overlays currentChannelContainer.Clear(false); currentChannelContainer.Add(loaded); currentChannelContainer.FadeIn(500, Easing.OutQuint); - - if (highlightingMessage != null && highlightingMessage.ChannelId == e.NewValue.Id) - { - loaded.ScrollToAndHighlightMessage(highlightingMessage); - highlightingMessage = null; - } }); } else @@ -446,34 +439,6 @@ namespace osu.Game.Overlays textbox.Text = string.Empty; } - /// - /// Returns the loaded drawable for a channel. Creates new instance if is true. Otherwise returns null if not found. - /// - public DrawableChannel GetChannelDrawable(Channel channel, bool createIfUnloaded = false) - { - var result = loadedChannels.Find(drawable => drawable.Channel == channel); - - if (createIfUnloaded && result == null) - { - result = loadChannelDrawable(channel); - } - - return result; - } - - private DrawableChannel loadChannelDrawable(Channel channel) - { - var loaded = new DrawableChannel(channel); - loadedChannels.Add(loaded); - return loaded; - } - - public void ScrollToAndHighlightMessage(Channel channel, Message message) - { - highlightingMessage = message; - channelManager.CurrentChannel.Value = channel; - } - private class TabsArea : Container { // IsHovered is used From 5d244f48f7553a862b7f9435c7f62941eb7ec53b Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 17 Jan 2020 00:00:10 +0100 Subject: [PATCH 018/670] Use instance list instead of exposing NotifcationOverlay's notifications --- osu.Game/Online/Chat/MessageNotifier.cs | 31 ++++++++++++------- osu.Game/Overlays/NotificationOverlay.cs | 2 -- .../Notifications/NotificationSection.cs | 2 -- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 8663cf4793..c850fb4519 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -42,6 +42,8 @@ namespace osu.Game.Online.Chat /// public bool IsActive => chatOverlay?.IsPresent == true; + private List privateMessageNotifications = new List(); + [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { @@ -96,12 +98,17 @@ namespace osu.Game.Online.Chat foreach (var message in messages) { - var words = getWords(message.Content); + // ignore messages that already have been read + if (message.Id < channel.LastReadId) + return; + var localUsername = localUser.Value.Username; if (message.Sender.Username == localUsername) continue; + var words = getWords(message.Content); + void onClick() { notificationOverlay.Hide(); @@ -109,20 +116,19 @@ namespace osu.Game.Online.Chat channelManager.CurrentChannel.Value = channel; } - - if (notifyOnChat.Value && channel.Type == ChannelType.PM) { // Scheduling because of possible "race-condition" (NotificationOverlay didn't add the notification yet). Schedule(() => { - var existingNotification = notificationOverlay.Notifications.OfType() - .FirstOrDefault(n => n.Username == message.Sender.Username); + var existingNotification = privateMessageNotifications.OfType() + .FirstOrDefault(n => n.Username == message.Sender.Username); if (existingNotification == null) { var notification = new PrivateMessageNotification(message.Sender.Username, onClick); notificationOverlay?.Post(notification); + privateMessageNotifications.Add(notification); } else { @@ -130,7 +136,6 @@ namespace osu.Game.Online.Chat } }); - continue; } @@ -196,9 +201,9 @@ namespace osu.Game.Online.Chat /// private static string hasCaseInsensitive(IEnumerable x, IEnumerable y) => x.FirstOrDefault(x2 => anyCaseInsensitive(y, x2)); - private static bool anyCaseInsensitive(IEnumerable x, string y) => x.Any(x2 => x2.Equals(y, StringComparison.InvariantCultureIgnoreCase)); + private static bool anyCaseInsensitive(IEnumerable x, string y) => x.Any(x2 => x2.Equals(y, StringComparison.OrdinalIgnoreCase)); - private class HighlightNotification : SimpleNotification + public class HighlightNotification : SimpleNotification { public HighlightNotification(string highlighter, string word, Action onClick) { @@ -223,7 +228,7 @@ namespace osu.Game.Online.Chat } } - private class PrivateMessageNotification : SimpleNotification + public class PrivateMessageNotification : SimpleNotification { public PrivateMessageNotification(string username, Action onClick) { @@ -260,18 +265,22 @@ namespace osu.Game.Online.Chat public override bool IsImportant => false; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, MessageNotifier notifier) { IconBackgound.Colour = colours.PurpleDark; Activated = delegate { onClick?.Invoke(); + + if (notifier.privateMessageNotifications.Contains(this)) + notifier.privateMessageNotifications.Remove(this); + return true; }; } } - private class MentionNotification : SimpleNotification + public class MentionNotification : SimpleNotification { public MentionNotification(string username, Action onClick) { diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 2ae17b143a..f36c13ab70 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -23,8 +23,6 @@ namespace osu.Game.Overlays public const float TRANSITION_LENGTH = 600; - public IEnumerable Notifications => sections.Children.SelectMany(s => s.Notifications); - private FlowContainer sections; /// diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 320c0d6cb1..17a2d4cf9f 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -21,8 +21,6 @@ namespace osu.Game.Overlays.Notifications private FlowContainer notifications; - public IEnumerable Notifications => notifications.Children; - public int DisplayedCount => notifications.Count(n => !n.WasClosed); public int UnreadCount => notifications.Count(n => !n.WasClosed && !n.Read); From f55cf03bd0f09c770e84c85f8d856c85a65bc255 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 18 Jan 2020 14:17:26 +0100 Subject: [PATCH 019/670] Remove unnecessary changes after rework --- osu.Game/Online/Chat/MessageNotifier.cs | 64 +++++-------------------- 1 file changed, 13 insertions(+), 51 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index c850fb4519..2715c42a95 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -118,24 +118,20 @@ namespace osu.Game.Online.Chat if (notifyOnChat.Value && channel.Type == ChannelType.PM) { - // Scheduling because of possible "race-condition" (NotificationOverlay didn't add the notification yet). - Schedule(() => - { - var existingNotification = privateMessageNotifications.OfType() - .FirstOrDefault(n => n.Username == message.Sender.Username); + var existingNotification = privateMessageNotifications.OfType() + .FirstOrDefault(n => n.Username == message.Sender.Username); + + if (existingNotification == null) + { + var notification = new PrivateMessageNotification(message.Sender.Username, onClick); + notificationOverlay?.Post(notification); + privateMessageNotifications.Add(notification); + } + else + { + existingNotification.MessageCount++; + } - if (existingNotification == null) - { - var notification = new PrivateMessageNotification(message.Sender.Username, onClick); - notificationOverlay?.Post(notification); - privateMessageNotifications.Add(notification); - } - else - { - existingNotification.MessageCount++; - } - }); - continue; } @@ -158,40 +154,6 @@ namespace osu.Game.Online.Chat } } } - - //making sure if the notification drawer bugs out, we merge it afterwards again. - Schedule(() => mergeNotifications()); - } - - /// - /// Checks current notifications if they aren't merged, and merges them together again. - /// - private void mergeNotifications() - { - if (notificationOverlay == null) - { - return; - } - - var pmn = notificationOverlay.Notifications.OfType(); - - foreach (var notification in pmn) - { - var duplicates = pmn.Where(n => n.Username == notification.Username); - - if (duplicates.Count() < 2) - continue; - - var first = duplicates.First(); - foreach (var notification2 in duplicates) - { - if (notification2 == first) - continue; - - first.MessageCount += notification2.MessageCount; - notification2.Close(); - } - } } private static string[] getWords(string input) => input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); From bc6f71fe97d150e2dc1911efececb14edce39de0 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 18 Jan 2020 15:27:55 +0100 Subject: [PATCH 020/670] Preserve current channel if ChatOverlay is being loaded in --- osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs | 7 ++++--- osu.Game/Overlays/ChatOverlay.cs | 8 +++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs index 4b1d595b44..e30c1678d5 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -59,15 +59,16 @@ namespace osu.Game.Overlays.Chat.Tabs /// /// Adds a channel to the ChannelTabControl. - /// The first channel added will automaticly selected. + /// The first channel added will automaticly selected if is true. /// /// The channel that is going to be added. - public void AddChannel(Channel channel) + /// If the current channel should be changed if none was selected before + public void AddChannel(Channel channel, bool setChannel = true) { if (!Items.Contains(channel)) AddItem(channel); - if (Current.Value == null) + if (Current.Value == null && setChannel) Current.Value = channel; } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 44772da3c1..4e69e4c9fc 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -221,8 +221,14 @@ namespace osu.Game.Overlays // TODO: consider scheduling bindable callbacks to not perform when overlay is not present. channelManager.JoinedChannels.ItemsAdded += onChannelAddedToJoinedChannels; channelManager.JoinedChannels.ItemsRemoved += onChannelRemovedFromJoinedChannels; + + bool channelSelected = channelManager.CurrentChannel.Value != null; + foreach (Channel channel in channelManager.JoinedChannels) - ChannelTabControl.AddChannel(channel); + ChannelTabControl.AddChannel(channel, !channelSelected); + + if (channelSelected) + ChannelTabControl.Current.Value = channelManager.CurrentChannel.Value; channelManager.AvailableChannels.ItemsAdded += availableChannelsChanged; channelManager.AvailableChannels.ItemsRemoved += availableChannelsChanged; From 8ddd36596e5cfd4b3933abbad5d5fc59179f6506 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 18 Jan 2020 15:40:55 +0100 Subject: [PATCH 021/670] Revert useless changes varying from properties, naming changes etc. --- osu.Game/Online/Chat/Channel.cs | 17 ++++++++--------- osu.Game/Online/Chat/ChannelManager.cs | 11 ++--------- osu.Game/Overlays/Chat/DrawableChannel.cs | 3 --- osu.Game/Overlays/ChatOverlay.cs | 3 +-- osu.Game/Overlays/NotificationOverlay.cs | 1 - 5 files changed, 11 insertions(+), 24 deletions(-) diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 1dea38f422..6f67a95f53 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -126,18 +126,17 @@ namespace osu.Game.Online.Chat { messages = messages.Except(Messages).ToArray(); - if (messages.Length != 0) - { - Messages.AddRange(messages); + if (messages.Length == 0) return; - var maxMessageId = messages.Max(m => m.Id); - if (maxMessageId > LastMessageId) - LastMessageId = maxMessageId; + Messages.AddRange(messages); - purgeOldMessages(); + var maxMessageId = messages.Max(m => m.Id); + if (maxMessageId > LastMessageId) + LastMessageId = maxMessageId; - NewMessagesArrived?.Invoke(messages); - } + purgeOldMessages(); + + NewMessagesArrived?.Invoke(messages); } /// diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 45c0df0677..4b5ec1cad0 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -49,6 +49,7 @@ namespace osu.Game.Online.Chat public IBindableList AvailableChannels => availableChannels; private IAPIProvider api; + public readonly BindableBool HighPollRate = new BindableBool(); public ChannelManager() @@ -246,15 +247,7 @@ namespace osu.Game.Online.Chat var channels = JoinedChannels.ToList(); foreach (var group in messages.GroupBy(m => m.ChannelId)) - { - var channel = channels.Find(c => c.Id == group.Key); - - if (channel == null) - continue; - - var groupArray = group.ToArray(); - channel.AddNewMessages(groupArray); - } + channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); } private void initializeChannels() diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 9c75e89249..a85b157175 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -26,7 +26,6 @@ namespace osu.Game.Overlays.Chat public readonly Channel Channel; protected FillFlowContainer ChatLineFlow; private OsuScrollContainer scroll; - public ColourInfo HighlightColour { get; set; } [Resolved] private OsuColour colours { get; set; } @@ -40,8 +39,6 @@ namespace osu.Game.Overlays.Chat [BackgroundDependencyLoader] private void load(OsuColour colours) { - HighlightColour = colours.Blue; - Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 4e69e4c9fc..9bd9f89665 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -259,7 +259,7 @@ namespace osu.Game.Overlays if (ChannelTabControl.Current.Value != e.NewValue) Scheduler.Add(() => ChannelTabControl.Current.Value = e.NewValue); - var loaded = loadedChannels.Find(drawable => drawable.Channel == e.NewValue); + var loaded = loadedChannels.Find(d => d.Channel == e.NewValue); if (loaded == null) { @@ -268,7 +268,6 @@ namespace osu.Game.Overlays loaded = new DrawableChannel(e.NewValue); loadedChannels.Add(loaded); - LoadComponentAsync(loaded, l => { if (currentChannel.Value != e.NewValue) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index f36c13ab70..41160d10ec 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -13,7 +13,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Threading; -using System.Collections.Generic; namespace osu.Game.Overlays { From 64fe9692ed2abd4411affa6fe39d5525085dbe8a Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 18 Jan 2020 15:57:51 +0100 Subject: [PATCH 022/670] Resolve CA errors --- osu.Game/Online/Chat/MessageNotifier.cs | 26 +++++++------------------ osu.Game/OsuGame.cs | 4 +--- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 2715c42a95..a2d6759863 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -53,13 +53,13 @@ namespace osu.Game.Online.Chat localUser = api.LocalUser; // Listen for new messages - channelManager.JoinedChannels.ItemsAdded += (joinedChannels) => + channelManager.JoinedChannels.ItemsAdded += joinedChannels => { foreach (var channel in joinedChannels) channel.NewMessagesArrived += channel_NewMessagesArrived; }; - channelManager.JoinedChannels.ItemsRemoved += (leftChannels) => + channelManager.JoinedChannels.ItemsRemoved += leftChannels => { foreach (var channel in leftChannels) channel.NewMessagesArrived -= channel_NewMessagesArrived; @@ -92,7 +92,7 @@ namespace osu.Game.Online.Chat public void HandleMessages(Channel channel, IEnumerable messages) { - // don't show if the ChatOverlay and the channel is visible. + // don't show if the ChatOverlay and the target channel is visible. if (IsActive && channelManager.CurrentChannel.Value == channel) return; @@ -118,8 +118,7 @@ namespace osu.Game.Online.Chat if (notifyOnChat.Value && channel.Type == ChannelType.PM) { - var existingNotification = privateMessageNotifications.OfType() - .FirstOrDefault(n => n.Username == message.Sender.Username); + var existingNotification = privateMessageNotifications.FirstOrDefault(n => n.Username == message.Sender.Username); if (existingNotification == null) { @@ -200,24 +199,13 @@ namespace osu.Game.Online.Chat this.onClick = onClick; } - private int messageCount = 0; + private int messageCount; public int MessageCount { get => messageCount; - set - { - messageCount = value; - - if (messageCount > 1) - { - Text = $"You received {messageCount} private messages from '{Username}'. Click to read it!"; - } - else - { - Text = $"You received a private message from '{Username}'. Click to read it!"; - } - } + set => Text = (messageCount = value) > 1 ? $"You received {messageCount} private messages from '{Username}'. Click to read it!" + : $"You received a private message from '{Username}'. Click to read it!"; } public string Username { get; set; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ff23375556..40b65b50e6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -59,8 +59,6 @@ namespace osu.Game private ChannelManager channelManager; - private MessageNotifier messageNotifier; - private NotificationOverlay notifications; private NowPlayingOverlay nowPlaying; @@ -615,7 +613,7 @@ namespace osu.Game loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true); loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true); loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); - loadComponentSingleFile(messageNotifier = new MessageNotifier(), AddInternal, true); + loadComponentSingleFile(new MessageNotifier(), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(Settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true); var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true); From 8a9c90c5e61b6b16b1f96c9f3fd225b58e923226 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 18 Jan 2020 16:18:17 +0100 Subject: [PATCH 023/670] Resolve CA errors #2 --- osu.Game/Online/Chat/MessageNotifier.cs | 5 ++--- osu.Game/Overlays/Chat/DrawableChannel.cs | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index a2d6759863..1637d2c2fe 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online.Chat /// public bool IsActive => chatOverlay?.IsPresent == true; - private List privateMessageNotifications = new List(); + private readonly List privateMessageNotifications = new List(); [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) @@ -204,8 +204,7 @@ namespace osu.Game.Online.Chat public int MessageCount { get => messageCount; - set => Text = (messageCount = value) > 1 ? $"You received {messageCount} private messages from '{Username}'. Click to read it!" - : $"You received a private message from '{Username}'. Click to read it!"; + set => Text = (messageCount = value) > 1 ? $"You received {messageCount} private messages from '{Username}'. Click to read it!" : $"You received a private message from '{Username}'. Click to read it!"; } public string Username { get; set; } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index a85b157175..b6c5a05c62 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -15,7 +15,6 @@ using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Shapes; From 32c20235171b7a873e7fbe7fe4f8c9fc4738da73 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 19 Jan 2020 17:20:54 +0100 Subject: [PATCH 024/670] Remove refactor in DrawableChannel --- osu.Game/Overlays/Chat/DrawableChannel.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index b6c5a05c62..d5f4d6c6d6 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -150,15 +150,13 @@ namespace osu.Game.Overlays.Chat private void messageRemoved(Message removed) { - findChatLine(removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); + chatLines.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); } private IEnumerable chatLines => ChatLineFlow.Children.OfType(); private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd()); - private ChatLine findChatLine(Message message) => chatLines.FirstOrDefault(c => c.Message == message); - public class DaySeparator : Container { public float TextSize From dd5478fe1ff3fe39bbc353e5eebff1aa00f6f460 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 19 Jan 2020 17:26:43 +0100 Subject: [PATCH 025/670] Remove highlighted/mentioned words --- osu.Game/Configuration/OsuConfigManager.cs | 2 -- osu.Game/Online/Chat/MessageNotifier.cs | 34 ------------------- .../Sections/Online/InGameChatSettings.cs | 27 --------------- .../Settings/Sections/OnlineSection.cs | 3 +- 4 files changed, 1 insertion(+), 65 deletions(-) delete mode 100644 osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 93d9068a2e..2968dadb40 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -52,8 +52,6 @@ namespace osu.Game.Configuration Set(OsuSetting.ChatHighlightName, true); Set(OsuSetting.ChatMessageNotification, true); - Set(OsuSetting.HighlightWords, string.Empty); - // Audio Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 1637d2c2fe..0e6da54e8d 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -141,17 +141,6 @@ namespace osu.Game.Online.Chat continue; } - - if (!string.IsNullOrWhiteSpace(highlightWords.Value)) - { - var matchedWord = hasCaseInsensitive(words, getWords(highlightWords.Value)); - - if (matchedWord != null) - { - var notification = new HighlightNotification(message.Sender.Username, matchedWord, onClick); - notificationOverlay?.Post(notification); - } - } } } @@ -164,30 +153,7 @@ namespace osu.Game.Online.Chat private static bool anyCaseInsensitive(IEnumerable x, string y) => x.Any(x2 => x2.Equals(y, StringComparison.OrdinalIgnoreCase)); - public class HighlightNotification : SimpleNotification - { - public HighlightNotification(string highlighter, string word, Action onClick) - { - Icon = FontAwesome.Solid.Highlighter; - Text = $"'{word}' was mentioned in chat by '{highlighter}'. Click to find out why!"; - this.onClick = onClick; - } - private readonly Action onClick; - - public override bool IsImportant => false; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - IconBackgound.Colour = colours.PurpleDark; - Activated = delegate - { - onClick?.Invoke(); - return true; - }; - } - } public class PrivateMessageNotification : SimpleNotification { diff --git a/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs deleted file mode 100644 index 781aa10618..0000000000 --- a/osu.Game/Overlays/Settings/Sections/Online/InGameChatSettings.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Game.Configuration; - -namespace osu.Game.Overlays.Settings.Sections.Online -{ - public class InGameChatSettings : SettingsSubsection - { - protected override string Header => "In-Game Chat"; - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - Children = new Drawable[] - { - new SettingsTextBox - { - LabelText = "Chat highlight words (space-separated list)", - Bindable = config.GetBindable(OsuSetting.HighlightWords) - } - }; - } - } -} diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs index 67a2e881d0..77aa81b429 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs @@ -17,8 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections Children = new Drawable[] { new WebSettings(), - new AlertsAndPrivacySettings(), - new InGameChatSettings() + new AlertsAndPrivacySettings() }; } } From 86ecaf223d4c92369330c525067b5328df08eb58 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 19 Jan 2020 17:36:38 +0100 Subject: [PATCH 026/670] Improve getWords() --- osu.Game/Online/Chat/MessageNotifier.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 0e6da54e8d..606882507f 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -144,7 +145,7 @@ namespace osu.Game.Online.Chat } } - private static string[] getWords(string input) => input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + private static IEnumerable getWords(string input) => Regex.Matches(input, @"\w+").Select(c => c.Value); /// /// Finds the first matching string/word in both and (case-insensitive) From 4feae82434a33472ea17856fc00827aa0fd9c96e Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 19 Jan 2020 17:55:17 +0100 Subject: [PATCH 027/670] Split HandleMessages method --- osu.Game/Online/Chat/MessageNotifier.cs | 93 ++++++++++++++----------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 606882507f..431cc7bb00 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -103,48 +103,52 @@ namespace osu.Game.Online.Chat if (message.Id < channel.LastReadId) return; + // ignore messages from yourself var localUsername = localUser.Value.Username; if (message.Sender.Username == localUsername) continue; - var words = getWords(message.Content); - - void onClick() - { - notificationOverlay.Hide(); - chatOverlay.Show(); - channelManager.CurrentChannel.Value = channel; - } - - if (notifyOnChat.Value && channel.Type == ChannelType.PM) - { - var existingNotification = privateMessageNotifications.FirstOrDefault(n => n.Username == message.Sender.Username); - - if (existingNotification == null) - { - var notification = new PrivateMessageNotification(message.Sender.Username, onClick); - notificationOverlay?.Post(notification); - privateMessageNotifications.Add(notification); - } - else - { - existingNotification.MessageCount++; - } - + if (checkForPMs(channel, message)) continue; - } - if (notifyOnMention.Value && anyCaseInsensitive(words, localUsername)) - { - var notification = new MentionNotification(message.Sender.Username, onClick); - notificationOverlay?.Post(notification); - - continue; - } + // change output to bool again if another "message processor" is added. + checkForMentions(channel, message, localUsername); } } + private bool checkForPMs(Channel channel, Message message) + { + if (!notifyOnChat.Value || channel.Type != ChannelType.PM) + return false; + + var existingNotification = privateMessageNotifications.FirstOrDefault(n => n.Username == message.Sender.Username); + + if (existingNotification == null) + { + var notification = new PrivateMessageNotification(message.Sender.Username, channel); + notificationOverlay?.Post(notification); + privateMessageNotifications.Add(notification); + } + else + { + existingNotification.MessageCount++; + } + + return true; + } + + private void checkForMentions(Channel channel, Message message, string username) + { + var words = getWords(message.Content); + + if (!notifyOnMention.Value || !anyCaseInsensitive(words, username)) + return; + + var notification = new MentionNotification(message.Sender.Username, channel); + notificationOverlay?.Post(notification); + } + private static IEnumerable getWords(string input) => Regex.Matches(input, @"\w+").Select(c => c.Value); /// @@ -158,12 +162,12 @@ namespace osu.Game.Online.Chat public class PrivateMessageNotification : SimpleNotification { - public PrivateMessageNotification(string username, Action onClick) + public PrivateMessageNotification(string username, Channel channel) { Icon = FontAwesome.Solid.Envelope; Username = username; MessageCount = 1; - this.onClick = onClick; + Channel = channel; } private int messageCount; @@ -176,17 +180,19 @@ namespace osu.Game.Online.Chat public string Username { get; set; } - private readonly Action onClick; + public Channel Channel { get; set; } public override bool IsImportant => false; [BackgroundDependencyLoader] - private void load(OsuColour colours, MessageNotifier notifier) + private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay, ChannelManager channelManager, MessageNotifier notifier) { IconBackgound.Colour = colours.PurpleDark; Activated = delegate { - onClick?.Invoke(); + notificationOverlay.Hide(); + chatOverlay.Show(); + channelManager.CurrentChannel.Value = Channel; if (notifier.privateMessageNotifications.Contains(this)) notifier.privateMessageNotifications.Remove(this); @@ -198,24 +204,27 @@ namespace osu.Game.Online.Chat public class MentionNotification : SimpleNotification { - public MentionNotification(string username, Action onClick) + public MentionNotification(string username, Channel channel) { Icon = FontAwesome.Solid.At; Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; - this.onClick = onClick; + Channel = channel; } - private readonly Action onClick; + public Channel Channel { get; set; } public override bool IsImportant => false; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay, ChannelManager channelManager) { IconBackgound.Colour = colours.PurpleDark; Activated = delegate { - onClick?.Invoke(); + notificationOverlay.Hide(); + chatOverlay.Show(); + channelManager.CurrentChannel.Value = Channel; + return true; }; } From 5f96940b7d8fb778991e9e9d3bfb3b445484cb96 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 19 Jan 2020 17:56:01 +0100 Subject: [PATCH 028/670] Remove unused injection --- osu.Game/Overlays/Chat/DrawableChannel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index d5f4d6c6d6..4c196f758d 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Chat } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Child = new OsuContextMenuContainer { From 1681e167383b54561b4fb4a86ecf35bf6b7a29e8 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 19 Jan 2020 18:20:42 +0100 Subject: [PATCH 029/670] Rework ChannelTabControl's AddChannel method to not auto select and let ChatOverlay handle this --- osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs | 7 +------ osu.Game/Overlays/ChatOverlay.cs | 7 ++----- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs index e30c1678d5..4e6bc48b8a 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -59,17 +59,12 @@ namespace osu.Game.Overlays.Chat.Tabs /// /// Adds a channel to the ChannelTabControl. - /// The first channel added will automaticly selected if is true. /// /// The channel that is going to be added. - /// If the current channel should be changed if none was selected before - public void AddChannel(Channel channel, bool setChannel = true) + public void AddChannel(Channel channel) { if (!Items.Contains(channel)) AddItem(channel); - - if (Current.Value == null && setChannel) - Current.Value = channel; } /// diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 9bd9f89665..3eba0811e3 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -222,13 +222,10 @@ namespace osu.Game.Overlays channelManager.JoinedChannels.ItemsAdded += onChannelAddedToJoinedChannels; channelManager.JoinedChannels.ItemsRemoved += onChannelRemovedFromJoinedChannels; - bool channelSelected = channelManager.CurrentChannel.Value != null; - foreach (Channel channel in channelManager.JoinedChannels) - ChannelTabControl.AddChannel(channel, !channelSelected); + ChannelTabControl.AddChannel(channel); - if (channelSelected) - ChannelTabControl.Current.Value = channelManager.CurrentChannel.Value; + ChannelTabControl.Current.Value = channelManager.CurrentChannel.Value ?? channelManager.JoinedChannels.First(); channelManager.AvailableChannels.ItemsAdded += availableChannelsChanged; channelManager.AvailableChannels.ItemsRemoved += availableChannelsChanged; From 4b871f61e38de3a5b0c9dd507e9a5e0b30e496c8 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 19 Jan 2020 18:23:12 +0100 Subject: [PATCH 030/670] Use Humanizer for counting PMs in text --- osu.Game/Online/Chat/MessageNotifier.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 431cc7bb00..b7fc41e57f 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -175,7 +176,11 @@ namespace osu.Game.Online.Chat public int MessageCount { get => messageCount; - set => Text = (messageCount = value) > 1 ? $"You received {messageCount} private messages from '{Username}'. Click to read it!" : $"You received a private message from '{Username}'. Click to read it!"; + set + { + messageCount = value; + Text = $"You received {"private message".ToQuantity(messageCount)} from '{Username}'. Click to read it!"; + } } public string Username { get; set; } From 7d1fc388ce2eef2b55f1ebdfd8e22c23b64e0815 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 19 Jan 2020 18:34:48 +0100 Subject: [PATCH 031/670] Resolve code quality errors --- osu.Game/Online/Chat/MessageNotifier.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index b7fc41e57f..2e6d1befd2 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -36,7 +36,6 @@ namespace osu.Game.Online.Chat private Bindable notifyOnMention; private Bindable notifyOnChat; - private Bindable highlightWords; private Bindable localUser; /// @@ -159,8 +158,6 @@ namespace osu.Game.Online.Chat private static bool anyCaseInsensitive(IEnumerable x, string y) => x.Any(x2 => x2.Equals(y, StringComparison.OrdinalIgnoreCase)); - - public class PrivateMessageNotification : SimpleNotification { public PrivateMessageNotification(string username, Channel channel) From be2a88c8a503fe31539872014a77473635f653b8 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 19 Jan 2020 18:40:17 +0100 Subject: [PATCH 032/670] Remove left over config entry --- osu.Game/Configuration/OsuConfigManager.cs | 1 - osu.Game/Online/Chat/MessageNotifier.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 2968dadb40..42b757c326 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -194,7 +194,6 @@ namespace osu.Game.Configuration IntroSequence, ChatHighlightName, ChatMessageNotification, - HighlightWords, UIHoldActivationDelay, HitLighting, MenuBackgroundSource diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 2e6d1befd2..58a3dd51a9 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -50,7 +50,6 @@ namespace osu.Game.Online.Chat { notifyOnMention = config.GetBindable(OsuSetting.ChatHighlightName); notifyOnChat = config.GetBindable(OsuSetting.ChatMessageNotification); - highlightWords = config.GetBindable(OsuSetting.HighlightWords); localUser = api.LocalUser; // Listen for new messages From f98347b3bba3a9f5f84d388709aecff51c12645e Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 19 Jan 2020 18:56:15 +0100 Subject: [PATCH 033/670] Allow no channels to be present --- osu.Game/Overlays/ChatOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 3eba0811e3..f49f5ef18b 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -225,7 +225,7 @@ namespace osu.Game.Overlays foreach (Channel channel in channelManager.JoinedChannels) ChannelTabControl.AddChannel(channel); - ChannelTabControl.Current.Value = channelManager.CurrentChannel.Value ?? channelManager.JoinedChannels.First(); + ChannelTabControl.Current.Value = channelManager.CurrentChannel.Value ?? channelManager.JoinedChannels.FirstOrDefault(); channelManager.AvailableChannels.ItemsAdded += availableChannelsChanged; channelManager.AvailableChannels.ItemsRemoved += availableChannelsChanged; From 63c8ae8211b548dec9cc8bbf3b86504d81d0098f Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 21 Jan 2020 23:42:15 +0100 Subject: [PATCH 034/670] Use IDs for checking against message author --- osu.Game/Online/Chat/MessageNotifier.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 58a3dd51a9..5739054750 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -103,16 +103,14 @@ namespace osu.Game.Online.Chat return; // ignore messages from yourself - var localUsername = localUser.Value.Username; - - if (message.Sender.Username == localUsername) + if (message.Sender.Id == localUser.Value.Id) continue; if (checkForPMs(channel, message)) continue; // change output to bool again if another "message processor" is added. - checkForMentions(channel, message, localUsername); + checkForMentions(channel, message, localUser.Value.Username); } } From 4d6ff31134b481e6d537c3d94a6161701bde793c Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 21 Jan 2020 23:43:21 +0100 Subject: [PATCH 035/670] Wrap getWords() with anyCaseInsensitive() --- osu.Game/Online/Chat/MessageNotifier.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 5739054750..58941044c7 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -137,9 +137,7 @@ namespace osu.Game.Online.Chat private void checkForMentions(Channel channel, Message message, string username) { - var words = getWords(message.Content); - - if (!notifyOnMention.Value || !anyCaseInsensitive(words, username)) + if (!notifyOnMention.Value || !anyCaseInsensitive(getWords(message.Content), username)) return; var notification = new MentionNotification(message.Sender.Username, channel); From 47a92a13b0745ff9c3f927bd66edc69c33fd8b62 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 00:13:07 +0100 Subject: [PATCH 036/670] Change code comments --- osu.Game/Online/Chat/MessageNotifier.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 58941044c7..22d5ef303f 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -92,7 +92,7 @@ namespace osu.Game.Online.Chat public void HandleMessages(Channel channel, IEnumerable messages) { - // don't show if the ChatOverlay and the target channel is visible. + // Only send notifications, if ChatOverlay and the target channel aren't visible. if (IsActive && channelManager.CurrentChannel.Value == channel) return; @@ -102,7 +102,6 @@ namespace osu.Game.Online.Chat if (message.Id < channel.LastReadId) return; - // ignore messages from yourself if (message.Sender.Id == localUser.Value.Id) continue; From 9fd494b057597b28c8e9708ec26b469891a37bea Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 00:27:46 +0100 Subject: [PATCH 037/670] Fix order where messages are checked in --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 22d5ef303f..302600b687 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -96,7 +96,7 @@ namespace osu.Game.Online.Chat if (IsActive && channelManager.CurrentChannel.Value == channel) return; - foreach (var message in messages) + foreach (var message in messages.OrderByDescending(m => m.Id)) { // ignore messages that already have been read if (message.Id < channel.LastReadId) From 699547e1a214c226c96593acf2ef1eace1b67398 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 00:28:08 +0100 Subject: [PATCH 038/670] Also exclude last read message --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 302600b687..a941b970fb 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -99,7 +99,7 @@ namespace osu.Game.Online.Chat foreach (var message in messages.OrderByDescending(m => m.Id)) { // ignore messages that already have been read - if (message.Id < channel.LastReadId) + if (message.Id <= channel.LastReadId) return; if (message.Sender.Id == localUser.Value.Id) From 5978e2c0e222e64736199df9a84f39c128fe8b52 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 00:28:59 +0100 Subject: [PATCH 039/670] Redo how instances of PM notifications are removed --- osu.Game/Online/Chat/MessageNotifier.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index a941b970fb..9819754b85 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -122,7 +122,8 @@ namespace osu.Game.Online.Chat if (existingNotification == null) { - var notification = new PrivateMessageNotification(message.Sender.Username, channel); + var notification = new PrivateMessageNotification(message.Sender.Username, channel, (n) => privateMessageNotifications.Remove(n)); + notificationOverlay?.Post(notification); privateMessageNotifications.Add(notification); } @@ -154,12 +155,13 @@ namespace osu.Game.Online.Chat public class PrivateMessageNotification : SimpleNotification { - public PrivateMessageNotification(string username, Channel channel) + public PrivateMessageNotification(string username, Channel channel, Action onRemove) { Icon = FontAwesome.Solid.Envelope; Username = username; MessageCount = 1; Channel = channel; + OnRemove = onRemove; } private int messageCount; @@ -178,23 +180,28 @@ namespace osu.Game.Online.Chat public Channel Channel { get; set; } + public Action OnRemove { get; set; } + public override bool IsImportant => false; [BackgroundDependencyLoader] - private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay, ChannelManager channelManager, MessageNotifier notifier) + private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay, ChannelManager channelManager) { IconBackgound.Colour = colours.PurpleDark; + Activated = delegate { notificationOverlay.Hide(); chatOverlay.Show(); channelManager.CurrentChannel.Value = Channel; - if (notifier.privateMessageNotifications.Contains(this)) - notifier.privateMessageNotifications.Remove(this); - return true; }; + + Closed += delegate + { + OnRemove.Invoke(this); + }; } } @@ -215,6 +222,7 @@ namespace osu.Game.Online.Chat private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay, ChannelManager channelManager) { IconBackgound.Colour = colours.PurpleDark; + Activated = delegate { notificationOverlay.Hide(); From 795051e25699fa9d4b3c9f6f1df73ef9db92358d Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 00:29:12 +0100 Subject: [PATCH 040/670] Prevent channel duplicates --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 9819754b85..c2ea94a279 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -79,7 +79,7 @@ namespace osu.Game.Online.Chat /// public void HandleMessages(long channelId, IEnumerable messages) { - var channel = channelManager.JoinedChannels.FirstOrDefault(c => c.Id == channelId); + var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == channelId); if (channel == null) { From 88ea1138b6fbabba95abad898beda23e1faa96d9 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 00:31:44 +0100 Subject: [PATCH 041/670] Compile regex --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index c2ea94a279..7dc19e1370 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -144,7 +144,7 @@ namespace osu.Game.Online.Chat notificationOverlay?.Post(notification); } - private static IEnumerable getWords(string input) => Regex.Matches(input, @"\w+").Select(c => c.Value); + private static IEnumerable getWords(string input) => Regex.Matches(input, @"\w+", RegexOptions.Compiled).Select(c => c.Value); /// /// Finds the first matching string/word in both and (case-insensitive) From d29694d788e988fe47e22120cd2ff42445d2e2fb Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 00:41:46 +0100 Subject: [PATCH 042/670] Add additional comment to explain the code order --- osu.Game/Online/Chat/MessageNotifier.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 7dc19e1370..c832259338 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -105,6 +105,7 @@ namespace osu.Game.Online.Chat if (message.Sender.Id == localUser.Value.Id) continue; + // check for private messages first, if true, skip checking mentions to prevent duplicate notifications about the same message. if (checkForPMs(channel, message)) continue; From 73d4b6a6be2d5e1a396670bb863fc2114365c779 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 00:53:49 +0100 Subject: [PATCH 043/670] Remove redundant lambda signature parentheses :/ --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index c832259338..21e92c98e4 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -123,7 +123,7 @@ namespace osu.Game.Online.Chat if (existingNotification == null) { - var notification = new PrivateMessageNotification(message.Sender.Username, channel, (n) => privateMessageNotifications.Remove(n)); + var notification = new PrivateMessageNotification(message.Sender.Username, channel, n => privateMessageNotifications.Remove(n)); notificationOverlay?.Post(notification); privateMessageNotifications.Add(notification); From e4accb3344fd275692105b4de6eb025a0393918b Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 10:47:51 +0100 Subject: [PATCH 044/670] Remove IsActive property as it never really made sense to have it in the first place --- osu.Game/Online/Chat/MessageNotifier.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 21e92c98e4..8cdafdfd9c 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -38,11 +38,6 @@ namespace osu.Game.Online.Chat private Bindable notifyOnChat; private Bindable localUser; - /// - /// Determines if the user is able to see incoming messages. - /// - public bool IsActive => chatOverlay?.IsPresent == true; - private readonly List privateMessageNotifications = new List(); [BackgroundDependencyLoader] @@ -93,7 +88,7 @@ namespace osu.Game.Online.Chat public void HandleMessages(Channel channel, IEnumerable messages) { // Only send notifications, if ChatOverlay and the target channel aren't visible. - if (IsActive && channelManager.CurrentChannel.Value == channel) + if (chatOverlay?.IsPresent == true && channelManager.CurrentChannel.Value == channel) return; foreach (var message in messages.OrderByDescending(m => m.Id)) From 771155e88251c948370921a456adfc972159123f Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 10:48:55 +0100 Subject: [PATCH 045/670] No notification "debouncing" --- osu.Game/Online/Chat/MessageNotifier.cs | 38 +++---------------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 8cdafdfd9c..745bf43c02 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -38,8 +38,6 @@ namespace osu.Game.Online.Chat private Bindable notifyOnChat; private Bindable localUser; - private readonly List privateMessageNotifications = new List(); - [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { @@ -114,19 +112,9 @@ namespace osu.Game.Online.Chat if (!notifyOnChat.Value || channel.Type != ChannelType.PM) return false; - var existingNotification = privateMessageNotifications.FirstOrDefault(n => n.Username == message.Sender.Username); + var notification = new PrivateMessageNotification(message.Sender.Username, channel); - if (existingNotification == null) - { - var notification = new PrivateMessageNotification(message.Sender.Username, channel, n => privateMessageNotifications.Remove(n)); - - notificationOverlay?.Post(notification); - privateMessageNotifications.Add(notification); - } - else - { - existingNotification.MessageCount++; - } + notificationOverlay?.Post(notification); return true; } @@ -151,25 +139,12 @@ namespace osu.Game.Online.Chat public class PrivateMessageNotification : SimpleNotification { - public PrivateMessageNotification(string username, Channel channel, Action onRemove) + public PrivateMessageNotification(string username, Channel channel) { Icon = FontAwesome.Solid.Envelope; Username = username; - MessageCount = 1; Channel = channel; - OnRemove = onRemove; - } - - private int messageCount; - - public int MessageCount - { - get => messageCount; - set - { - messageCount = value; - Text = $"You received {"private message".ToQuantity(messageCount)} from '{Username}'. Click to read it!"; - } + Text = $"You received a private message from '{Username}'. Click to read it!"; } public string Username { get; set; } @@ -193,11 +168,6 @@ namespace osu.Game.Online.Chat return true; }; - - Closed += delegate - { - OnRemove.Invoke(this); - }; } } From 3d2625836ac0dd63b83401ccb90ad27ae03b33f1 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 10:50:27 +0100 Subject: [PATCH 046/670] Remove static from getWords method --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 745bf43c02..71139c81b7 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -128,7 +128,7 @@ namespace osu.Game.Online.Chat notificationOverlay?.Post(notification); } - private static IEnumerable getWords(string input) => Regex.Matches(input, @"\w+", RegexOptions.Compiled).Select(c => c.Value); + private IEnumerable getWords(string input) => Regex.Matches(input, @"\w+", RegexOptions.Compiled).Select(c => c.Value); /// /// Finds the first matching string/word in both and (case-insensitive) From c6f450f93295f5ee65696ed179efd68706d2f0b6 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Jan 2020 11:23:27 +0100 Subject: [PATCH 047/670] Resolve code analysis errors --- osu.Game/Online/Chat/MessageNotifier.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 71139c81b7..f6f1b9cb7d 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -147,9 +146,9 @@ namespace osu.Game.Online.Chat Text = $"You received a private message from '{Username}'. Click to read it!"; } - public string Username { get; set; } + public string Username { get; } - public Channel Channel { get; set; } + public Channel Channel { get; } public Action OnRemove { get; set; } @@ -180,7 +179,7 @@ namespace osu.Game.Online.Chat Channel = channel; } - public Channel Channel { get; set; } + public Channel Channel { get; } public override bool IsImportant => false; From 158b9690526846546755f5de6f572d0c540441e9 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 25 Jan 2020 14:40:16 +0100 Subject: [PATCH 048/670] Remove XML doc from HandleMessages --- osu.Game/Online/Chat/MessageNotifier.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index f6f1b9cb7d..a4fbb6fef3 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -66,9 +66,6 @@ namespace osu.Game.Online.Chat HandleMessages(messages.First().ChannelId, messages); } - /// - /// Resolves the channel id - /// public void HandleMessages(long channelId, IEnumerable messages) { var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == channelId); From 00da45ead4d9a4a36b1379e19a2f82863da6add2 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 25 Jan 2020 14:40:53 +0100 Subject: [PATCH 049/670] Matching strings instead of splitting --- osu.Game/Online/Chat/MessageNotifier.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index a4fbb6fef3..074b171022 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -117,21 +116,25 @@ namespace osu.Game.Online.Chat private void checkForMentions(Channel channel, Message message, string username) { - if (!notifyOnMention.Value || !anyCaseInsensitive(getWords(message.Content), username)) + if (!notifyOnMention.Value || !isMentioning(message.Content, username)) return; var notification = new MentionNotification(message.Sender.Username, channel); notificationOverlay?.Post(notification); } - private IEnumerable getWords(string input) => Regex.Matches(input, @"\w+", RegexOptions.Compiled).Select(c => c.Value); - /// - /// Finds the first matching string/word in both and (case-insensitive) + /// Checks if contains , if not, retries making spaces into underscores. /// - private static string hasCaseInsensitive(IEnumerable x, IEnumerable y) => x.FirstOrDefault(x2 => anyCaseInsensitive(y, x2)); + /// If the mentions the + private bool isMentioning(string message, string username) + { + // sanitize input to handle casing + message = message.ToLower(); + username = username.ToLower(); - private static bool anyCaseInsensitive(IEnumerable x, string y) => x.Any(x2 => x2.Equals(y, StringComparison.OrdinalIgnoreCase)); + return message.Contains(username) || message.Contains(username.Replace(' ', '_')); + } public class PrivateMessageNotification : SimpleNotification { From 16c500d0b0dd29191466608bcd8f7fa970e3419c Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 25 Jan 2020 16:41:45 +0100 Subject: [PATCH 050/670] Add mention tests --- osu.Game.Tests/Chat/MessageNotifierTests.cs | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 osu.Game.Tests/Chat/MessageNotifierTests.cs diff --git a/osu.Game.Tests/Chat/MessageNotifierTests.cs b/osu.Game.Tests/Chat/MessageNotifierTests.cs new file mode 100644 index 0000000000..d46e18b0b4 --- /dev/null +++ b/osu.Game.Tests/Chat/MessageNotifierTests.cs @@ -0,0 +1,27 @@ +using NUnit.Framework; +using osu.Game.Online.Chat; + +namespace osu.Game.Tests.Chat +{ + [TestFixture] + public class MessageNotifierTests + { + private readonly MessageNotifier messageNotifier = new MessageNotifier(); + + [Test] + public void TestMentions() + { + // Message (with mention, different casing) + Assert.IsTrue(messageNotifier.IsMentioning("Hey, Somebody Playing OSU!", "Somebody playing osu!")); + + // Message (with mention, underscores) + Assert.IsTrue(messageNotifier.IsMentioning("Hey, Somebody_playing_osu!", "Somebody playing osu!")); + + // Message (with mention, different casing, underscores) + Assert.IsTrue(messageNotifier.IsMentioning("Hey, Somebody_Playing_OSU!", "Somebody playing osu!")); + + // Message (without mention) + Assert.IsTrue(!messageNotifier.IsMentioning("peppy, can you please fix this?", "Cookiezi")); + } + } +} From e0ef6725494e75be59325183149d0ab8ed256f03 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 25 Jan 2020 16:43:51 +0100 Subject: [PATCH 051/670] Use binded list --- osu.Game/Online/Chat/MessageNotifier.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 074b171022..4d371f655d 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -35,6 +35,7 @@ namespace osu.Game.Online.Chat private Bindable notifyOnMention; private Bindable notifyOnChat; private Bindable localUser; + private BindableList joinedChannels = new BindableList(); [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) @@ -43,14 +44,16 @@ namespace osu.Game.Online.Chat notifyOnChat = config.GetBindable(OsuSetting.ChatMessageNotification); localUser = api.LocalUser; + channelManager.JoinedChannels.BindTo(joinedChannels); + // Listen for new messages - channelManager.JoinedChannels.ItemsAdded += joinedChannels => + joinedChannels.ItemsAdded += joinedChannels => { foreach (var channel in joinedChannels) channel.NewMessagesArrived += channel_NewMessagesArrived; }; - channelManager.JoinedChannels.ItemsRemoved += leftChannels => + joinedChannels.ItemsRemoved += leftChannels => { foreach (var channel in leftChannels) channel.NewMessagesArrived -= channel_NewMessagesArrived; @@ -116,7 +119,7 @@ namespace osu.Game.Online.Chat private void checkForMentions(Channel channel, Message message, string username) { - if (!notifyOnMention.Value || !isMentioning(message.Content, username)) + if (!notifyOnMention.Value || !IsMentioning(message.Content, username)) return; var notification = new MentionNotification(message.Sender.Username, channel); From f9def8355237dcc540cb4336c7132b7a4efa8a65 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 25 Jan 2020 16:44:45 +0100 Subject: [PATCH 052/670] Make IsMentioning public to allow it to be used for testing --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 4d371f655d..9f14a0fc21 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -130,7 +130,7 @@ namespace osu.Game.Online.Chat /// Checks if contains , if not, retries making spaces into underscores. /// /// If the mentions the - private bool isMentioning(string message, string username) + public bool IsMentioning(string message, string username) { // sanitize input to handle casing message = message.ToLower(); From 9e6fde7d09ebc8376df331f9b4e629bf34fe54f5 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 25 Jan 2020 16:50:12 +0100 Subject: [PATCH 053/670] Add license header --- osu.Game.Tests/Chat/MessageNotifierTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/MessageNotifierTests.cs b/osu.Game.Tests/Chat/MessageNotifierTests.cs index d46e18b0b4..b5a9a63961 100644 --- a/osu.Game.Tests/Chat/MessageNotifierTests.cs +++ b/osu.Game.Tests/Chat/MessageNotifierTests.cs @@ -1,4 +1,7 @@ -using NUnit.Framework; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; using osu.Game.Online.Chat; namespace osu.Game.Tests.Chat From 65644731e0d2e86280ed7b73a5c0bd9b06be1ff2 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 25 Jan 2020 17:03:39 +0100 Subject: [PATCH 054/670] Make field readonly --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 9f14a0fc21..41df7e7291 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -35,7 +35,7 @@ namespace osu.Game.Online.Chat private Bindable notifyOnMention; private Bindable notifyOnChat; private Bindable localUser; - private BindableList joinedChannels = new BindableList(); + private readonly BindableList joinedChannels = new BindableList(); [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) From 5e91a3f0f8ac53c66e3d77897e607956a50f4149 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 29 Jan 2020 01:59:52 +0100 Subject: [PATCH 055/670] Use IndexOf --- osu.Game/Online/Chat/MessageNotifier.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 41df7e7291..30784c9934 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -130,14 +130,7 @@ namespace osu.Game.Online.Chat /// Checks if contains , if not, retries making spaces into underscores. /// /// If the mentions the - public bool IsMentioning(string message, string username) - { - // sanitize input to handle casing - message = message.ToLower(); - username = username.ToLower(); - - return message.Contains(username) || message.Contains(username.Replace(' ', '_')); - } + public bool IsMentioning(string message, string username) => message.IndexOf(username, StringComparison.OrdinalIgnoreCase) != -1 || message.IndexOf(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase) != -1; public class PrivateMessageNotification : SimpleNotification { From d7a52aa80109cbb34b73f220ede35fa2e789a88b Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 29 Jan 2020 02:06:10 +0100 Subject: [PATCH 056/670] Use test scenes --- osu.Game.Tests/Chat/MessageNotifierTests.cs | 30 ------ .../Visual/Online/TestSceneMessageNotifier.cs | 99 +++++++++++++++++++ 2 files changed, 99 insertions(+), 30 deletions(-) delete mode 100644 osu.Game.Tests/Chat/MessageNotifierTests.cs create mode 100644 osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs diff --git a/osu.Game.Tests/Chat/MessageNotifierTests.cs b/osu.Game.Tests/Chat/MessageNotifierTests.cs deleted file mode 100644 index b5a9a63961..0000000000 --- a/osu.Game.Tests/Chat/MessageNotifierTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Game.Online.Chat; - -namespace osu.Game.Tests.Chat -{ - [TestFixture] - public class MessageNotifierTests - { - private readonly MessageNotifier messageNotifier = new MessageNotifier(); - - [Test] - public void TestMentions() - { - // Message (with mention, different casing) - Assert.IsTrue(messageNotifier.IsMentioning("Hey, Somebody Playing OSU!", "Somebody playing osu!")); - - // Message (with mention, underscores) - Assert.IsTrue(messageNotifier.IsMentioning("Hey, Somebody_playing_osu!", "Somebody playing osu!")); - - // Message (with mention, different casing, underscores) - Assert.IsTrue(messageNotifier.IsMentioning("Hey, Somebody_Playing_OSU!", "Somebody playing osu!")); - - // Message (without mention) - Assert.IsTrue(!messageNotifier.IsMentioning("peppy, can you please fix this?", "Cookiezi")); - } - } -} diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs new file mode 100644 index 0000000000..632f66354c --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -0,0 +1,99 @@ +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneMessageNotifier : OsuTestScene + { + private User friend; + private Channel publicChannel; + private Channel privateMesssageChannel; + private TestContainer testContainer; + + private int messageIdCounter; + + [SetUp] + public void Setup() + { + friend = new User() { Id = 0, Username = "Friend" }; + publicChannel = new Channel() { Id = 1, Name = "osu" }; + privateMesssageChannel = new Channel(friend) { Id = 2, Name = friend.Username, Type = ChannelType.PM }; + + Child = testContainer = new TestContainer(new Channel[] { publicChannel, privateMesssageChannel }) + { + RelativeSizeAxes = Axes.Both, + }; + + testContainer.ChatOverlay.Show(); + } + + [Test] + public void TestPublicChannelMention() + { + AddStep("Switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMesssageChannel); + + AddStep("Send regular message", () => publicChannel.AddNewMessages(new Message(messageIdCounter++) { + Content = "Hello everyone!", + Sender = friend, + ChannelId = publicChannel.Id + })); + AddAssert("Expect no notifications", () => testContainer.NotificationOverlay.UnreadCount.Value == 0); + + AddStep("Send message containing mention", () => publicChannel.AddNewMessages(new Message(messageIdCounter++) { + Content = $"Hello {API.LocalUser.Value.Username.ToLowerInvariant()}!", + Sender = friend, + ChannelId = publicChannel.Id + })); + AddAssert("Expect 1 notification", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); + } + + [Test] + public void TestPrivateMessageNotification() + { + AddStep("Send PM", () => privateMesssageChannel.AddNewMessages(new Message(messageIdCounter++) + { + Content = $"Hello {API.LocalUser.Value.Username}!", + Sender = friend, + ChannelId = privateMesssageChannel.Id + })); + AddAssert("Expect 1 notification", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); + } + + private class TestContainer : Container + { + private Channel[] channels; + + public TestContainer(Channel[] channels) => this.channels = channels; + + [Cached] + public ChannelManager ChannelManager { get; } = new ChannelManager(); + + [Cached] + public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay(); + + [Cached] + public MessageNotifier MessageNotifier { get; } = new MessageNotifier(); + + [Cached] + public ChatOverlay ChatOverlay { get; } = new ChatOverlay(); + + [BackgroundDependencyLoader] + private void load() + { + AddRange(new Drawable[] { ChannelManager, NotificationOverlay }); + + ((BindableList)ChannelManager.AvailableChannels).AddRange(channels); + + AddRange(new Drawable[] { ChatOverlay, MessageNotifier }); + + ((BindableList)ChannelManager.JoinedChannels).AddRange(channels); + } + } + } +} From 48231317d28880bd334b836da4d00ba7971e9494 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 29 Jan 2020 02:07:08 +0100 Subject: [PATCH 057/670] Make IsMentioning private --- osu.Game/Online/Chat/MessageNotifier.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 30784c9934..0e79a13917 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -119,7 +119,7 @@ namespace osu.Game.Online.Chat private void checkForMentions(Channel channel, Message message, string username) { - if (!notifyOnMention.Value || !IsMentioning(message.Content, username)) + if (!notifyOnMention.Value || !isMentioning(message.Content, username)) return; var notification = new MentionNotification(message.Sender.Username, channel); @@ -130,7 +130,7 @@ namespace osu.Game.Online.Chat /// Checks if contains , if not, retries making spaces into underscores. /// /// If the mentions the - public bool IsMentioning(string message, string username) => message.IndexOf(username, StringComparison.OrdinalIgnoreCase) != -1 || message.IndexOf(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase) != -1; + private bool isMentioning(string message, string username) => message.IndexOf(username, StringComparison.OrdinalIgnoreCase) != -1 || message.IndexOf(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase) != -1; public class PrivateMessageNotification : SimpleNotification { From e7881bd3811ed8eda28d8b4b51c1f4a93059dd94 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 29 Jan 2020 02:13:32 +0100 Subject: [PATCH 058/670] Single lines. --- .../Visual/Online/TestSceneMessageNotifier.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 632f66354c..51d5f837fc 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -38,18 +38,10 @@ namespace osu.Game.Tests.Visual.Online { AddStep("Switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMesssageChannel); - AddStep("Send regular message", () => publicChannel.AddNewMessages(new Message(messageIdCounter++) { - Content = "Hello everyone!", - Sender = friend, - ChannelId = publicChannel.Id - })); + AddStep("Send regular message", () => publicChannel.AddNewMessages(new Message(messageIdCounter++) { Content = "Hello everyone!", Sender = friend, ChannelId = publicChannel.Id })); AddAssert("Expect no notifications", () => testContainer.NotificationOverlay.UnreadCount.Value == 0); - AddStep("Send message containing mention", () => publicChannel.AddNewMessages(new Message(messageIdCounter++) { - Content = $"Hello {API.LocalUser.Value.Username.ToLowerInvariant()}!", - Sender = friend, - ChannelId = publicChannel.Id - })); + AddStep("Send message containing mention", () => publicChannel.AddNewMessages(new Message(messageIdCounter++) { Content = $"Hello {API.LocalUser.Value.Username.ToLowerInvariant()}!", Sender = friend, ChannelId = publicChannel.Id })); AddAssert("Expect 1 notification", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); } From 4f109c02d31e90dc525f92ffa4291a1a15529592 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 29 Jan 2020 02:18:46 +0100 Subject: [PATCH 059/670] Add license header --- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 51d5f837fc..df0611d33f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -1,4 +1,7 @@ -using NUnit.Framework; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; From 738f1f0c565f56b6f36cfa2dddcfba46411566cd Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 29 Jan 2020 02:27:07 +0100 Subject: [PATCH 060/670] Turn lines into another single line --- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index df0611d33f..64a3a75eda 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -51,12 +51,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPrivateMessageNotification() { - AddStep("Send PM", () => privateMesssageChannel.AddNewMessages(new Message(messageIdCounter++) - { - Content = $"Hello {API.LocalUser.Value.Username}!", - Sender = friend, - ChannelId = privateMesssageChannel.Id - })); + AddStep("Send PM", () => privateMesssageChannel.AddNewMessages(new Message(messageIdCounter++) { Content = $"Hello {API.LocalUser.Value.Username}!", Sender = friend, ChannelId = privateMesssageChannel.Id })); AddAssert("Expect 1 notification", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); } From 25b080c78df4e842f759bbd9587daec28381116a Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 30 Jan 2020 03:41:21 +0100 Subject: [PATCH 061/670] Resolve CI/CA errors --- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 64a3a75eda..1dcff4b017 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -24,11 +24,11 @@ namespace osu.Game.Tests.Visual.Online [SetUp] public void Setup() { - friend = new User() { Id = 0, Username = "Friend" }; - publicChannel = new Channel() { Id = 1, Name = "osu" }; + friend = new User { Id = 0, Username = "Friend" }; + publicChannel = new Channel { Id = 1, Name = "osu" }; privateMesssageChannel = new Channel(friend) { Id = 2, Name = friend.Username, Type = ChannelType.PM }; - Child = testContainer = new TestContainer(new Channel[] { publicChannel, privateMesssageChannel }) + Child = testContainer = new TestContainer(new[] { publicChannel, privateMesssageChannel }) { RelativeSizeAxes = Axes.Both, }; @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Online private class TestContainer : Container { - private Channel[] channels; + private readonly Channel[] channels; public TestContainer(Channel[] channels) => this.channels = channels; From 8523e3d205fcd8bff4ecf36566ac69ac11f3a040 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 30 Jan 2020 05:24:26 +0100 Subject: [PATCH 062/670] Schedule child updating --- .../Visual/Online/TestSceneMessageNotifier.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 1dcff4b017..4afa013345 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -28,12 +28,15 @@ namespace osu.Game.Tests.Visual.Online publicChannel = new Channel { Id = 1, Name = "osu" }; privateMesssageChannel = new Channel(friend) { Id = 2, Name = friend.Username, Type = ChannelType.PM }; - Child = testContainer = new TestContainer(new[] { publicChannel, privateMesssageChannel }) + Schedule(() => { - RelativeSizeAxes = Axes.Both, - }; + Child = testContainer = new TestContainer(new[] { publicChannel, privateMesssageChannel }) + { + RelativeSizeAxes = Axes.Both, + }; - testContainer.ChatOverlay.Show(); + testContainer.ChatOverlay.Show(); + }); } [Test] From 79f47fe7d791b57872158456a264cab4782ae35e Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 3 Feb 2020 22:08:01 +0100 Subject: [PATCH 063/670] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 4afa013345..33af4568ca 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Online { private User friend; private Channel publicChannel; - private Channel privateMesssageChannel; + private Channel privateMessageChannel; private TestContainer testContainer; private int messageIdCounter; From 176e1e7ec2b742bf079818646f3d5aaeffbdc932 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 3 Feb 2020 22:19:55 +0100 Subject: [PATCH 064/670] Rename references --- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 33af4568ca..981aaf5b17 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -26,11 +26,11 @@ namespace osu.Game.Tests.Visual.Online { friend = new User { Id = 0, Username = "Friend" }; publicChannel = new Channel { Id = 1, Name = "osu" }; - privateMesssageChannel = new Channel(friend) { Id = 2, Name = friend.Username, Type = ChannelType.PM }; + privateMessageChannel = new Channel(friend) { Id = 2, Name = friend.Username, Type = ChannelType.PM }; Schedule(() => { - Child = testContainer = new TestContainer(new[] { publicChannel, privateMesssageChannel }) + Child = testContainer = new TestContainer(new[] { publicChannel, privateMessageChannel }) { RelativeSizeAxes = Axes.Both, }; @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPublicChannelMention() { - AddStep("Switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMesssageChannel); + AddStep("Switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel); AddStep("Send regular message", () => publicChannel.AddNewMessages(new Message(messageIdCounter++) { Content = "Hello everyone!", Sender = friend, ChannelId = publicChannel.Id })); AddAssert("Expect no notifications", () => testContainer.NotificationOverlay.UnreadCount.Value == 0); @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPrivateMessageNotification() { - AddStep("Send PM", () => privateMesssageChannel.AddNewMessages(new Message(messageIdCounter++) { Content = $"Hello {API.LocalUser.Value.Username}!", Sender = friend, ChannelId = privateMesssageChannel.Id })); + AddStep("Send PM", () => privateMessageChannel.AddNewMessages(new Message(messageIdCounter++) { Content = $"Hello {API.LocalUser.Value.Username}!", Sender = friend, ChannelId = privateMessageChannel.Id })); AddAssert("Expect 1 notification", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); } From 2936f83fd95eb62bae3f0737fd0c8445178226cc Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 3 Feb 2020 23:00:29 +0100 Subject: [PATCH 065/670] Improve load order --- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 981aaf5b17..a935851282 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -79,12 +79,9 @@ namespace osu.Game.Tests.Visual.Online [BackgroundDependencyLoader] private void load() { - AddRange(new Drawable[] { ChannelManager, NotificationOverlay }); + AddRange(new Drawable[] { ChannelManager, ChatOverlay, NotificationOverlay, MessageNotifier }); ((BindableList)ChannelManager.AvailableChannels).AddRange(channels); - - AddRange(new Drawable[] { ChatOverlay, MessageNotifier }); - ((BindableList)ChannelManager.JoinedChannels).AddRange(channels); } } From 835d4f25ffbaefa3a79c04b778ef2cff5648eb82 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 3 Feb 2020 23:02:58 +0100 Subject: [PATCH 066/670] Add notification testing to tests and show notification overlay while testing --- .../Visual/Online/TestSceneMessageNotifier.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index a935851282..1490331266 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -1,18 +1,22 @@ // 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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Online.Chat; using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; using osu.Game.Users; +using osuTK.Input; namespace osu.Game.Tests.Visual.Online { - public class TestSceneMessageNotifier : OsuTestScene + public class TestSceneMessageNotifier : ManualInputManagerTestScene { private User friend; private Channel publicChannel; @@ -49,13 +53,35 @@ namespace osu.Game.Tests.Visual.Online AddStep("Send message containing mention", () => publicChannel.AddNewMessages(new Message(messageIdCounter++) { Content = $"Hello {API.LocalUser.Value.Username.ToLowerInvariant()}!", Sender = friend, ChannelId = publicChannel.Id })); AddAssert("Expect 1 notification", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); + + AddStep("Open notification overlay", () => testContainer.NotificationOverlay.Show()); + AddStep("Click notification", clickNotification); + + AddAssert("Expect ChatOverlay is open", () => testContainer.ChatOverlay.State.Value == Visibility.Visible); + AddAssert("Expect the public channel to be selected", () => testContainer.ChannelManager.CurrentChannel.Value == publicChannel); } [Test] public void TestPrivateMessageNotification() { + AddStep("Switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel); + AddStep("Send PM", () => privateMessageChannel.AddNewMessages(new Message(messageIdCounter++) { Content = $"Hello {API.LocalUser.Value.Username}!", Sender = friend, ChannelId = privateMessageChannel.Id })); AddAssert("Expect 1 notification", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); + + AddStep("Open notification overlay", () => testContainer.NotificationOverlay.Show()); + AddStep("Click notification", clickNotification); + + AddAssert("Expect ChatOverlay is open", () => testContainer.ChatOverlay.State.Value == Visibility.Visible); + AddAssert("Expect the PM channel to be selected", () => testContainer.ChannelManager.CurrentChannel.Value == privateMessageChannel); + } + + private void clickNotification() where T : Notification + { + var notification = testContainer.NotificationOverlay.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(notification); + InputManager.Click(MouseButton.Left); } private class TestContainer : Container From 4eedd82032b0be8599128d852a5d176881035fbc Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 3 Feb 2020 23:03:27 +0100 Subject: [PATCH 067/670] Don't unnecessarily expose properties --- osu.Game/Online/Chat/MessageNotifier.cs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 0e79a13917..4f04a78adc 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -137,16 +137,14 @@ namespace osu.Game.Online.Chat public PrivateMessageNotification(string username, Channel channel) { Icon = FontAwesome.Solid.Envelope; - Username = username; - Channel = channel; - Text = $"You received a private message from '{Username}'. Click to read it!"; + this.username = username; + this.channel = channel; + Text = $"You received a private message from '{this.username}'. Click to read it!"; } - public string Username { get; } + private readonly string username; - public Channel Channel { get; } - - public Action OnRemove { get; set; } + private readonly Channel channel; public override bool IsImportant => false; @@ -159,7 +157,7 @@ namespace osu.Game.Online.Chat { notificationOverlay.Hide(); chatOverlay.Show(); - channelManager.CurrentChannel.Value = Channel; + channelManager.CurrentChannel.Value = channel; return true; }; @@ -172,10 +170,10 @@ namespace osu.Game.Online.Chat { Icon = FontAwesome.Solid.At; Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; - Channel = channel; + this.channel = channel; } - public Channel Channel { get; } + private readonly Channel channel; public override bool IsImportant => false; @@ -188,7 +186,7 @@ namespace osu.Game.Online.Chat { notificationOverlay.Hide(); chatOverlay.Show(); - channelManager.CurrentChannel.Value = Channel; + channelManager.CurrentChannel.Value = channel; return true; }; From ea5eaba0a96cb3060e743952157ac4388854fa08 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 3 Feb 2020 23:05:46 +0100 Subject: [PATCH 068/670] Use ChannelManager.JoinChannel() --- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 1490331266..ecd5892468 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -108,7 +108,9 @@ namespace osu.Game.Tests.Visual.Online AddRange(new Drawable[] { ChannelManager, ChatOverlay, NotificationOverlay, MessageNotifier }); ((BindableList)ChannelManager.AvailableChannels).AddRange(channels); - ((BindableList)ChannelManager.JoinedChannels).AddRange(channels); + + foreach (var channel in channels) + ChannelManager.JoinChannel(channel); } } } From f16b90a152a3fdb885dff743f31eee6f1f7178aa Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 3 Feb 2020 23:56:23 +0100 Subject: [PATCH 069/670] Remove username data from PrivateMessageNotification --- osu.Game/Online/Chat/MessageNotifier.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 4f04a78adc..0d821dff32 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -137,13 +137,10 @@ namespace osu.Game.Online.Chat public PrivateMessageNotification(string username, Channel channel) { Icon = FontAwesome.Solid.Envelope; - this.username = username; + Text = $"You received a private message from '{username}'. Click to read it!"; this.channel = channel; - Text = $"You received a private message from '{this.username}'. Click to read it!"; } - private readonly string username; - private readonly Channel channel; public override bool IsImportant => false; From a66fd17691182f862e80d5fc7507002edfbad2bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2020 19:23:46 +0100 Subject: [PATCH 070/670] Expand test coverage --- .../Visual/Online/TestSceneMessageNotifier.cs | 96 +++++++++++++++---- 1 file changed, 80 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index ecd5892468..5e0a3994e1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -46,36 +46,100 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPublicChannelMention() { - AddStep("Switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel); + AddStep("switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel); - AddStep("Send regular message", () => publicChannel.AddNewMessages(new Message(messageIdCounter++) { Content = "Hello everyone!", Sender = friend, ChannelId = publicChannel.Id })); - AddAssert("Expect no notifications", () => testContainer.NotificationOverlay.UnreadCount.Value == 0); + AddStep("receive public message", () => receiveMessage(friend, publicChannel, "Hello everyone")); + AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0); - AddStep("Send message containing mention", () => publicChannel.AddNewMessages(new Message(messageIdCounter++) { Content = $"Hello {API.LocalUser.Value.Username.ToLowerInvariant()}!", Sender = friend, ChannelId = publicChannel.Id })); - AddAssert("Expect 1 notification", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); + AddStep("receive message containing mention", () => receiveMessage(friend, publicChannel, $"Hello {API.LocalUser.Value.Username.ToLowerInvariant()}!")); + AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); - AddStep("Open notification overlay", () => testContainer.NotificationOverlay.Show()); - AddStep("Click notification", clickNotification); + AddStep("open notification overlay", () => testContainer.NotificationOverlay.Show()); + AddStep("click notification", clickNotification); - AddAssert("Expect ChatOverlay is open", () => testContainer.ChatOverlay.State.Value == Visibility.Visible); - AddAssert("Expect the public channel to be selected", () => testContainer.ChannelManager.CurrentChannel.Value == publicChannel); + AddAssert("chat overlay is open", () => testContainer.ChatOverlay.State.Value == Visibility.Visible); + AddAssert("public channel is selected", () => testContainer.ChannelManager.CurrentChannel.Value == publicChannel); } [Test] public void TestPrivateMessageNotification() { - AddStep("Switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel); + AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel); - AddStep("Send PM", () => privateMessageChannel.AddNewMessages(new Message(messageIdCounter++) { Content = $"Hello {API.LocalUser.Value.Username}!", Sender = friend, ChannelId = privateMessageChannel.Id })); - AddAssert("Expect 1 notification", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); + AddStep("receive PM", () => receiveMessage(friend, privateMessageChannel, $"Hello {API.LocalUser.Value.Username}")); + AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); - AddStep("Open notification overlay", () => testContainer.NotificationOverlay.Show()); - AddStep("Click notification", clickNotification); + AddStep("open notification overlay", () => testContainer.NotificationOverlay.Show()); + AddStep("click notification", clickNotification); - AddAssert("Expect ChatOverlay is open", () => testContainer.ChatOverlay.State.Value == Visibility.Visible); - AddAssert("Expect the PM channel to be selected", () => testContainer.ChannelManager.CurrentChannel.Value == privateMessageChannel); + AddAssert("chat overlay is open", () => testContainer.ChatOverlay.State.Value == Visibility.Visible); + AddAssert("PM channel is selected", () => testContainer.ChannelManager.CurrentChannel.Value == privateMessageChannel); } + [Test] + public void TestNoNotificationWhenPMChannelOpen() + { + AddStep("switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel); + + AddStep("receive PM", () => receiveMessage(friend, privateMessageChannel, "you're reading this, right?")); + + AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0); + } + + [Test] + public void TestNoNotificationWhenMentionedInOpenPublicChannel() + { + AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel); + + AddStep("receive mention", () => receiveMessage(friend, publicChannel, $"{API.LocalUser.Value.Username.ToUpperInvariant()} has been reading this")); + + AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0); + } + + [Test] + public void TestNoNotificationOnSelfMention() + { + AddStep("switch to PM channel", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel); + + AddStep("receive self-mention", () => receiveMessage(API.LocalUser.Value, publicChannel, $"my name is {API.LocalUser.Value.Username}")); + + AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0); + } + + [Test] + public void TestNoNotificationOnPMFromSelf() + { + AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel); + + AddStep("receive PM from self", () => receiveMessage(API.LocalUser.Value, privateMessageChannel, "hey hey")); + + AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0); + } + + [Test] + public void TestNotificationsNotFiredTwice() + { + AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel); + + AddStep("receive same PM twice", () => + { + var message = createMessage(friend, privateMessageChannel, "hey hey"); + privateMessageChannel.AddNewMessages(message, message); + }); + + AddStep("open notification overlay", () => testContainer.NotificationOverlay.Show()); + AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1); + } + + private void receiveMessage(User sender, Channel channel, string content) => channel.AddNewMessages(createMessage(sender, channel, content)); + + private Message createMessage(User sender, Channel channel, string content) => new Message(messageIdCounter++) + { + Content = content, + Sender = sender, + ChannelId = channel.Id + }; + private void clickNotification() where T : Notification { var notification = testContainer.NotificationOverlay.ChildrenOfType().Single(); From 9378b216e6879414566f63fc2b7c23f17337232b Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 5 Feb 2020 19:01:51 +0100 Subject: [PATCH 071/670] Lowercase the N inside channel_NewMessagesArrived --- osu.Game/Online/Chat/MessageNotifier.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 0d821dff32..166a073512 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -50,17 +50,17 @@ namespace osu.Game.Online.Chat joinedChannels.ItemsAdded += joinedChannels => { foreach (var channel in joinedChannels) - channel.NewMessagesArrived += channel_NewMessagesArrived; + channel.NewMessagesArrived += channel_newMessagesArrived; }; joinedChannels.ItemsRemoved += leftChannels => { foreach (var channel in leftChannels) - channel.NewMessagesArrived -= channel_NewMessagesArrived; + channel.NewMessagesArrived -= channel_newMessagesArrived; }; } - private void channel_NewMessagesArrived(IEnumerable messages) + private void channel_newMessagesArrived(IEnumerable messages) { if (messages == null || !messages.Any()) return; From 5875f2158c34ff169a2c85573a5f06d3db1f4d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2020 19:20:16 +0100 Subject: [PATCH 072/670] Properly rename event handler --- osu.Game/Online/Chat/MessageNotifier.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 166a073512..016c25d8ab 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -50,17 +50,17 @@ namespace osu.Game.Online.Chat joinedChannels.ItemsAdded += joinedChannels => { foreach (var channel in joinedChannels) - channel.NewMessagesArrived += channel_newMessagesArrived; + channel.NewMessagesArrived += newMessagesArrived; }; joinedChannels.ItemsRemoved += leftChannels => { foreach (var channel in leftChannels) - channel.NewMessagesArrived -= channel_newMessagesArrived; + channel.NewMessagesArrived -= newMessagesArrived; }; } - private void channel_newMessagesArrived(IEnumerable messages) + private void newMessagesArrived(IEnumerable messages) { if (messages == null || !messages.Any()) return; From 7cd228db07f50d1f489bcf8e3514f48061dc168e Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 7 Feb 2020 16:50:22 +0100 Subject: [PATCH 073/670] Change notifyOnChat to notifyOnPM --- osu.Game/Online/Chat/MessageNotifier.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 166a073512..3b000675ac 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -33,7 +33,7 @@ namespace osu.Game.Online.Chat private ChannelManager channelManager { get; set; } private Bindable notifyOnMention; - private Bindable notifyOnChat; + private Bindable notifyOnPM; private Bindable localUser; private readonly BindableList joinedChannels = new BindableList(); @@ -41,7 +41,7 @@ namespace osu.Game.Online.Chat private void load(OsuConfigManager config, IAPIProvider api) { notifyOnMention = config.GetBindable(OsuSetting.ChatHighlightName); - notifyOnChat = config.GetBindable(OsuSetting.ChatMessageNotification); + notifyOnPM = config.GetBindable(OsuSetting.ChatMessageNotification); localUser = api.LocalUser; channelManager.JoinedChannels.BindTo(joinedChannels); @@ -107,7 +107,7 @@ namespace osu.Game.Online.Chat private bool checkForPMs(Channel channel, Message message) { - if (!notifyOnChat.Value || channel.Type != ChannelType.PM) + if (!notifyOnPM.Value || channel.Type != ChannelType.PM) return false; var notification = new PrivateMessageNotification(message.Sender.Username, channel); From dd86443264fb3861ee90a453f67eb013afcacd61 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 7 Feb 2020 16:51:37 +0100 Subject: [PATCH 074/670] Make isMentioning static --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 3b000675ac..975df9714e 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -130,7 +130,7 @@ namespace osu.Game.Online.Chat /// Checks if contains , if not, retries making spaces into underscores. /// /// If the mentions the - private bool isMentioning(string message, string username) => message.IndexOf(username, StringComparison.OrdinalIgnoreCase) != -1 || message.IndexOf(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase) != -1; + private static bool isMentioning(string message, string username) => message.IndexOf(username, StringComparison.OrdinalIgnoreCase) != -1 || message.IndexOf(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase) != -1; public class PrivateMessageNotification : SimpleNotification { From 41915df1f37fdd5bc9015a35b939f344d5ce0f42 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 7 Feb 2020 16:52:53 +0100 Subject: [PATCH 075/670] Change comment --- osu.Game/Online/Chat/MessageNotifier.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 975df9714e..8c92892a1e 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -96,7 +96,8 @@ namespace osu.Game.Online.Chat if (message.Sender.Id == localUser.Value.Id) continue; - // check for private messages first, if true, skip checking mentions to prevent duplicate notifications about the same message. + // check for private messages first, + // to avoid both posting two notifications about the same message if (checkForPMs(channel, message)) continue; From b6aedb22d8a5b169b23f528bd3430dfc82162074 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sun, 15 Mar 2020 01:25:02 +0100 Subject: [PATCH 076/670] Add approachcircle mod --- .../Mods/OsuModApproachCircle.cs | 60 +++++++++++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 1 + 2 files changed, 61 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs new file mode 100644 index 0000000000..42851e5cda --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Mods +{ + internal class OsuModApproachCircle : Mod, IApplicableToDrawableHitObjects + { + public override string Name => "Approach Circle"; + public override string Acronym => "AC"; + public override string Description => "Never trust the approach circles..."; + public override double ScoreMultiplier => 1; + + public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; + + [SettingSource("Easing", "Change the easing type of the approach circles.", 0)] + public Bindable BindableEasing { get; } = new Bindable(); + + [SettingSource("Scale", "Change the factor of the approach circle scale.", 1)] + public BindableFloat Scale { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 2, + MaxValue = 10, + Default = 4, + Value = 4, + }; + + public void ApplyToDrawableHitObjects(IEnumerable drawables) + { + drawables.ForEach(drawable => + { + drawable.ApplyCustomUpdateState += (drawableHitObj, state) => + { + if (!(drawableHitObj is DrawableOsuHitObject drawableOsuHitObj) || !(drawableHitObj is DrawableHitCircle hitCircle)) return; + + var obj = drawableOsuHitObj.HitObject; + + hitCircle.BeginAbsoluteSequence(obj.StartTime - obj.TimePreempt, true); + hitCircle.ApproachCircle.ScaleTo(Scale.Value); + + hitCircle.ApproachCircle.FadeIn(Math.Min(obj.TimeFadeIn, obj.TimePreempt)); + hitCircle.ApproachCircle.ScaleTo(1f, obj.TimePreempt, BindableEasing.Value); + + hitCircle.ApproachCircle.Expire(true); + }; + }); + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 148869f5e8..64b8ca4ab1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -150,6 +150,7 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new OsuModGrow(), new OsuModDeflate()), new MultiMod(new ModWindUp(), new ModWindDown()), new OsuModTraceable(), + new OsuModApproachCircle(), }; case ModType.System: From d9aef6f813d9666e18dbe313498317fea200dbb4 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sun, 15 Mar 2020 18:53:40 +0100 Subject: [PATCH 077/670] Change label of attribute --- osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs index 42851e5cda..4e75e4b0a8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs @@ -26,14 +26,15 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Easing", "Change the easing type of the approach circles.", 0)] public Bindable BindableEasing { get; } = new Bindable(); - [SettingSource("Scale", "Change the factor of the approach circle scale.", 1)] + [SettingSource("Scale the size", "Change the factor of the approach circle scale.", 1)] + public BindableFloat Scale { get; } = new BindableFloat { Precision = 0.1f, MinValue = 2, MaxValue = 10, Default = 4, - Value = 4, + Value = 4 }; public void ApplyToDrawableHitObjects(IEnumerable drawables) From 83a4fb6f37f0fe725e52771e1d762cd407a56e2c Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sun, 15 Mar 2020 19:19:09 +0100 Subject: [PATCH 078/670] Remove empty lines --- osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs index 4e75e4b0a8..0e41bef3c8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs @@ -20,14 +20,12 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "AC"; public override string Description => "Never trust the approach circles..."; public override double ScoreMultiplier => 1; - public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; [SettingSource("Easing", "Change the easing type of the approach circles.", 0)] public Bindable BindableEasing { get; } = new Bindable(); [SettingSource("Scale the size", "Change the factor of the approach circle scale.", 1)] - public BindableFloat Scale { get; } = new BindableFloat { Precision = 0.1f, From 8892f2c3dd2bb92d3738b122456223c30a1cea8d Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sun, 15 Mar 2020 20:41:28 +0100 Subject: [PATCH 079/670] Remove redundant check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs index 0e41bef3c8..b227b05436 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods { drawable.ApplyCustomUpdateState += (drawableHitObj, state) => { - if (!(drawableHitObj is DrawableOsuHitObject drawableOsuHitObj) || !(drawableHitObj is DrawableHitCircle hitCircle)) return; + if (!(drawableHitObj is DrawableHitCircle hitCircle)) return; var obj = drawableOsuHitObj.HitObject; From bc705616ab62867cebc66e502cff5de82b46f5b6 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sun, 15 Mar 2020 20:46:12 +0100 Subject: [PATCH 080/670] Fix bdach's suggestion --- osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs index b227b05436..5b5992954c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Mods { if (!(drawableHitObj is DrawableHitCircle hitCircle)) return; - var obj = drawableOsuHitObj.HitObject; + var obj = hitCircle.HitObject; hitCircle.BeginAbsoluteSequence(obj.StartTime - obj.TimePreempt, true); hitCircle.ApproachCircle.ScaleTo(Scale.Value); From 405030da471b6388d015b941a04cb466b2def9da Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 16 Mar 2020 20:34:53 +0100 Subject: [PATCH 081/670] Resolving bdach's requested to change the enum member names and reduce the amount of selectable easing types --- .../Mods/OsuModApproachCircle.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs index 5b5992954c..8e0175b5a3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; [SettingSource("Easing", "Change the easing type of the approach circles.", 0)] - public Bindable BindableEasing { get; } = new Bindable(); + public Bindable BindableEasing { get; } = new Bindable(); [SettingSource("Scale the size", "Change the factor of the approach circle scale.", 1)] public BindableFloat Scale { get; } = new BindableFloat @@ -49,11 +49,25 @@ namespace osu.Game.Rulesets.Osu.Mods hitCircle.ApproachCircle.ScaleTo(Scale.Value); hitCircle.ApproachCircle.FadeIn(Math.Min(obj.TimeFadeIn, obj.TimePreempt)); - hitCircle.ApproachCircle.ScaleTo(1f, obj.TimePreempt, BindableEasing.Value); + + hitCircle.ApproachCircle.ScaleTo(1f, obj.TimePreempt, (Easing)BindableEasing.Value); hitCircle.ApproachCircle.Expire(true); }; }); } + + internal enum ReadableEasing + { + Accelerate = 2, + Accelerate2 = 6, + Accelerate3 = 12, + AccelerateInAfterDeceleraingeOut = 29, + CasualBounces = 32, + CasualBouncesWhileAccelerating = 24, + Decelerate = 1, + DecelerateAfterAccelerating = 8, + Default = 0, + } } } From ee103c9a1e4804744bed343d7a647f2d8e6a023f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 16:59:01 +0900 Subject: [PATCH 082/670] Add prerelease realm package --- osu.Game/osu.Game.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f28a55e016..eeacb10d14 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -28,6 +28,7 @@ + From dce9937e9b93898aa28fffe227ad45578f021e7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 14:19:14 +0900 Subject: [PATCH 083/670] Add automapper for detaching support --- osu.Game/osu.Game.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index eeacb10d14..1a762be9c9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,6 +18,7 @@ + From 9cfede2e7e57305d6916b058eef96bf19de254f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 14:07:36 +0900 Subject: [PATCH 084/670] Setup context, write usage, wrapper classes --- osu.Game/Database/IHasGuidPrimaryKey.cs | 16 ++ osu.Game/Database/IRealmFactory.cs | 19 ++ osu.Game/Database/RealmBackedStore.cs | 27 +++ osu.Game/Database/RealmContextFactory.cs | 262 +++++++++++++++++++++++ osu.Game/Database/RealmWriteUsage.cs | 55 +++++ osu.Game/OsuGameBase.cs | 4 + 6 files changed, 383 insertions(+) create mode 100644 osu.Game/Database/IHasGuidPrimaryKey.cs create mode 100644 osu.Game/Database/IRealmFactory.cs create mode 100644 osu.Game/Database/RealmBackedStore.cs create mode 100644 osu.Game/Database/RealmContextFactory.cs create mode 100644 osu.Game/Database/RealmWriteUsage.cs diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs new file mode 100644 index 0000000000..3dda32a5b0 --- /dev/null +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -0,0 +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; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace osu.Game.Database +{ + public interface IHasGuidPrimaryKey + { + [JsonIgnore] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + Guid ID { get; set; } + } +} diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs new file mode 100644 index 0000000000..d65bcaebbe --- /dev/null +++ b/osu.Game/Database/IRealmFactory.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Realms; + +namespace osu.Game.Database +{ + public interface IRealmFactory + { + public Realm Get() => Realm.GetInstance(); + + /// + /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// This method may block if a write is already active on a different thread. + /// + /// A usage containing a usable context. + RealmWriteUsage GetForWrite(); + } +} diff --git a/osu.Game/Database/RealmBackedStore.cs b/osu.Game/Database/RealmBackedStore.cs new file mode 100644 index 0000000000..e37831d9d5 --- /dev/null +++ b/osu.Game/Database/RealmBackedStore.cs @@ -0,0 +1,27 @@ +// 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.Platform; + +namespace osu.Game.Database +{ + public abstract class RealmBackedStore + { + protected readonly Storage Storage; + + protected readonly IRealmFactory ContextFactory; + + protected RealmBackedStore(IRealmFactory contextFactory, Storage storage = null) + { + ContextFactory = contextFactory; + Storage = storage; + } + + /// + /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary. + /// + public virtual void Cleanup() + { + } + } +} diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs new file mode 100644 index 0000000000..826e098669 --- /dev/null +++ b/osu.Game/Database/RealmContextFactory.cs @@ -0,0 +1,262 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using AutoMapper; +using osu.Framework.Platform; +using osu.Framework.Statistics; +using osu.Framework.Threading; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Input.Bindings; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Skinning; +using Realms; + +namespace osu.Game.Database +{ + public class RealmContextFactory : IRealmFactory + { + private readonly Storage storage; + private readonly Scheduler scheduler; + + private const string database_name = @"client"; + + private ThreadLocal threadContexts; + + private readonly object writeLock = new object(); + + private ThreadLocal refreshCompleted = new ThreadLocal(); + + private bool rollbackRequired; + + private int currentWriteUsages; + + private Transaction currentWriteTransaction; + + public RealmContextFactory(Storage storage, Scheduler scheduler) + { + this.storage = storage; + this.scheduler = scheduler; + recreateThreadContexts(); + } + + private static readonly GlobalStatistic reads = GlobalStatistics.Get("Database", "Get (Read)"); + private static readonly GlobalStatistic writes = GlobalStatistics.Get("Database", "Get (Write)"); + private static readonly GlobalStatistic commits = GlobalStatistics.Get("Database", "Commits"); + private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Database", "Rollbacks"); + private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Database", "Contexts"); + private Thread writingThread; + + /// + /// Get a context for the current thread for read-only usage. + /// If a is in progress, the existing write-safe context will be returned. + /// + public Realm Get() + { + reads.Value++; + return getContextForCurrentThread(); + } + + /// + /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// This method may block if a write is already active on a different thread. + /// + /// A usage containing a usable context. + public RealmWriteUsage GetForWrite() + { + writes.Value++; + Monitor.Enter(writeLock); + Realm context; + + try + { + context = getContextForCurrentThread(); + + if (currentWriteTransaction == null) + { + writingThread = Thread.CurrentThread; + currentWriteTransaction = context.BeginWrite(); + } + } + catch + { + // retrieval of a context could trigger a fatal error. + Monitor.Exit(writeLock); + throw; + } + + Interlocked.Increment(ref currentWriteUsages); + + return new RealmWriteUsage(context, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; + } + + // TODO: remove if not necessary. + public void Schedule(Action action) => scheduler.Add(action); + + private Realm getContextForCurrentThread() + { + var context = threadContexts.Value; + if (context?.IsClosed != false) + threadContexts.Value = context = CreateContext(); + + if (!refreshCompleted.Value) + { + context.Refresh(); + refreshCompleted.Value = true; + } + + return context; + } + + private void usageCompleted(RealmWriteUsage usage) + { + int usages = Interlocked.Decrement(ref currentWriteUsages); + + try + { + rollbackRequired |= usage.RollbackRequired; + + if (usages == 0) + { + if (rollbackRequired) + { + rollbacks.Value++; + currentWriteTransaction?.Rollback(); + } + else + { + commits.Value++; + currentWriteTransaction?.Commit(); + } + + currentWriteTransaction = null; + writingThread = null; + rollbackRequired = false; + + refreshCompleted = new ThreadLocal(); + } + } + finally + { + Monitor.Exit(writeLock); + } + } + + private void recreateThreadContexts() + { + // Contexts for other threads are not disposed as they may be in use elsewhere. Instead, fresh contexts are exposed + // for other threads to use, and we rely on the finalizer inside OsuDbContext to handle their previous contexts + threadContexts?.Value.Dispose(); + threadContexts = new ThreadLocal(CreateContext, true); + } + + protected virtual Realm CreateContext() + { + contexts.Value++; + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))); + } + + public void ResetDatabase() + { + lock (writeLock) + { + recreateThreadContexts(); + storage.DeleteDatabase(database_name); + } + } + } + + [SuppressMessage("ReSharper", "CA2225")] + public class RealmWrapper : IEquatable> + where T : RealmObject, IHasGuidPrimaryKey + { + public Guid ID { get; } + + private readonly ThreadLocal threadValues; + + public readonly IRealmFactory ContextFactory; + + public RealmWrapper(T original, IRealmFactory contextFactory) + { + ContextFactory = contextFactory; + ID = original.ID; + + var originalContext = original.Realm; + + threadValues = new ThreadLocal(() => + { + var context = ContextFactory?.Get(); + + if (context == null || originalContext?.IsSameInstance(context) != false) + return original; + + return context.Find(ID); + }); + } + + public T Get() => threadValues.Value; + + public RealmWrapper WrapChild(Func lookup) + where TChild : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(lookup(Get()), ContextFactory); + + // ReSharper disable once CA2225 + public static implicit operator T(RealmWrapper wrapper) + => wrapper?.Get().Detach(); + + // ReSharper disable once CA2225 + public static implicit operator RealmWrapper(T obj) => obj.WrapAsUnmanaged(); + + public bool Equals(RealmWrapper other) => other != null && other.ID == ID; + + public override string ToString() => Get().ToString(); + } + + public static class RealmExtensions + { + private static readonly IMapper mapper = new MapperConfiguration(c => + { + c.ShouldMapField = fi => false; + c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; + + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + + c.CreateMap() + .ForMember(s => s.Beatmaps, d => d.MapFrom(s => s.Beatmaps)) + .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) + .MaxDepth(2); + + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + }).CreateMapper(); + + public static T Detach(this T obj) where T : RealmObject + { + if (!obj.IsManaged) + return obj; + + var detached = mapper.Map(obj); + + //typeof(RealmObject).GetField("_realm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(detached, null); + + return detached; + } + + public static RealmWrapper Wrap(this T obj, IRealmFactory contextFactory) + where T : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(obj, contextFactory); + + public static RealmWrapper WrapAsUnmanaged(this T obj) + where T : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(obj, null); + } +} diff --git a/osu.Game/Database/RealmWriteUsage.cs b/osu.Game/Database/RealmWriteUsage.cs new file mode 100644 index 0000000000..35e30e8123 --- /dev/null +++ b/osu.Game/Database/RealmWriteUsage.cs @@ -0,0 +1,55 @@ +// 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 Realms; + +namespace osu.Game.Database +{ + public class RealmWriteUsage : IDisposable + { + public readonly Realm Context; + private readonly Action usageCompleted; + + public bool RollbackRequired { get; private set; } + + public RealmWriteUsage(Realm context, Action onCompleted) + { + Context = context; + usageCompleted = onCompleted; + } + + /// + /// Whether this write usage will commit a transaction on completion. + /// If false, there is a parent usage responsible for transaction commit. + /// + public bool IsTransactionLeader; + + private bool isDisposed; + + protected void Dispose(bool disposing) + { + if (isDisposed) return; + + isDisposed = true; + + usageCompleted?.Invoke(this); + } + + public void Rollback(Exception error = null) + { + RollbackRequired = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~RealmWriteUsage() + { + Dispose(false); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0b5abc4e31..8ec6976c63 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -146,6 +146,8 @@ namespace osu.Game private DatabaseContextFactory contextFactory; + private RealmContextFactory realmFactory; + protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); [BackgroundDependencyLoader] @@ -167,6 +169,8 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage, Scheduler)); + dependencies.CacheAs(Storage); var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); From 5372d95d167953e812d25941d5fd44e46e47e36c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 14:29:22 +0900 Subject: [PATCH 085/670] Initialise FodyWeavers --- osu.Game/FodyWeavers.xml | 3 +++ osu.Game/FodyWeavers.xsd | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 osu.Game/FodyWeavers.xml create mode 100644 osu.Game/FodyWeavers.xsd diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml new file mode 100644 index 0000000000..cc07b89533 --- /dev/null +++ b/osu.Game/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/osu.Game/FodyWeavers.xsd b/osu.Game/FodyWeavers.xsd new file mode 100644 index 0000000000..f526bddb09 --- /dev/null +++ b/osu.Game/FodyWeavers.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file From d5ac97ece849f46711e97e5e0e290119a4370ef2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 14:35:15 +0900 Subject: [PATCH 086/670] Add realm store / key binding implementations --- osu.Game/Input/Bindings/RealmKeyBinding.cs | 33 ++++++++ osu.Game/Input/RealmKeyBindingStore.cs | 92 ++++++++++++++++++++++ osu.Game/OsuGameBase.cs | 1 + 3 files changed, 126 insertions(+) create mode 100644 osu.Game/Input/Bindings/RealmKeyBinding.cs create mode 100644 osu.Game/Input/RealmKeyBindingStore.cs diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs new file mode 100644 index 0000000000..a8cd1c3fb6 --- /dev/null +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -0,0 +1,33 @@ +// 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.Input.Bindings; +using osu.Game.Database; +using Realms; + +namespace osu.Game.Input.Bindings +{ + [MapTo("KeyBinding")] + public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey + { + public Guid ID { get; set; } + + public int? RulesetID { get; set; } + + public int? Variant { get; set; } + + [Ignored] + public KeyBinding KeyBinding + { + get + { + var split = KeyBindingString.Split(':'); + return new KeyBinding(split[0], int.Parse(split[1])); + } + set => KeyBindingString = $"{value.KeyCombination}:{(int)value.Action}"; + } + + public string KeyBindingString { get; set; } + } +} diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs new file mode 100644 index 0000000000..471a25dd0d --- /dev/null +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Bindings; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets; + +namespace osu.Game.Input +{ + public class RealmKeyBindingStore : RealmBackedStore + { + public event Action KeyBindingChanged; + + public RealmKeyBindingStore(RealmContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) + : base(contextFactory, storage) + { + using (ContextFactory.GetForWrite()) + { + foreach (RulesetInfo info in rulesets.AvailableRulesets) + { + var ruleset = info.CreateInstance(); + foreach (var variant in ruleset.AvailableVariants) + insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + } + } + } + + public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); + + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) + { + using (var usage = ContextFactory.GetForWrite()) + { + // compare counts in database vs defaults + foreach (var group in defaults.GroupBy(k => k.Action)) + { + int count = Query(rulesetId, variant).Count(k => k.KeyBinding.Action == group.Key); + int aimCount = group.Count(); + + if (aimCount <= count) + continue; + + foreach (var insertable in group.Skip(count).Take(aimCount - count)) + { + // insert any defaults which are missing. + usage.Context.Add(new RealmKeyBinding + { + KeyBinding = new KeyBinding + { + KeyCombination = insertable.KeyCombination, + Action = insertable.Action, + }, + RulesetID = rulesetId, + Variant = variant + }); + } + } + } + } + + /// + /// Retrieve s for a specified ruleset/variant content. + /// + /// The ruleset's internal ID. + /// An optional variant. + /// + public List Query(int? rulesetId = null, int? variant = null) => + ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + + public void Update(KeyBinding keyBinding) + { + using (ContextFactory.GetForWrite()) + { + //todo: fix + // var dbKeyBinding = (RealmKeyBinding)keyBinding; + // Refresh(ref dbKeyBinding); + // + // if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) + // return; + // + // dbKeyBinding.KeyCombination = keyBinding.KeyCombination; + } + + KeyBindingChanged?.Invoke(); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8ec6976c63..95b1d3100c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -266,6 +266,7 @@ namespace osu.Game AddInternal(scorePerformanceManager); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + dependencies.Cache(new RealmKeyBindingStore(realmFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new SessionStatics()); From 5d7ab4a7f12b7a07b4b2b1779908512307433f1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:01:41 +0900 Subject: [PATCH 087/670] Rename global statistics to be specific to realm --- osu.Game/Database/RealmContextFactory.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 826e098669..c37068947e 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -45,11 +45,11 @@ namespace osu.Game.Database recreateThreadContexts(); } - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Database", "Get (Read)"); - private static readonly GlobalStatistic writes = GlobalStatistics.Get("Database", "Get (Write)"); - private static readonly GlobalStatistic commits = GlobalStatistics.Get("Database", "Commits"); - private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Database", "Rollbacks"); - private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Database", "Contexts"); + private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); + private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); + private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); + private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); + private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Realm", "Contexts"); private Thread writingThread; /// From ae76eca5648525abf580b4dfa6ebc78eb69fd423 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:41:29 +0900 Subject: [PATCH 088/670] Add basic realm migration support --- osu.Game/Database/RealmContextFactory.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c37068947e..b918eb0e78 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -43,6 +43,15 @@ namespace osu.Game.Database this.storage = storage; this.scheduler = scheduler; recreateThreadContexts(); + + using (CreateContext()) + { + // ensure our schema is up-to-date and migrated. + } + } + + private void onMigration(Migration migration, ulong oldschemaversion) + { } private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); @@ -158,7 +167,11 @@ namespace osu.Game.Database protected virtual Realm CreateContext() { contexts.Value++; - return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))); + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + { + SchemaVersion = 2, + MigrationCallback = onMigration + }); } public void ResetDatabase() From 845d5cdea23bf0fc179b3a1226c109d2cfbd6b94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:41:58 +0900 Subject: [PATCH 089/670] Switch guid to store as string until fody issues are resolved See https://github.com/realm/realm-dotnet/issues/740#issuecomment-755898968 --- osu.Game/Database/IHasGuidPrimaryKey.cs | 11 ++++++++++- osu.Game/Database/RealmContextFactory.cs | 2 +- osu.Game/Input/Bindings/RealmKeyBinding.cs | 3 +-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index 3dda32a5b0..33618e990d 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -4,13 +4,22 @@ using System; using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; +using Realms; namespace osu.Game.Database { public interface IHasGuidPrimaryKey { + [JsonIgnore] + [Ignored] + public Guid Guid + { + get => new Guid(ID); + set => ID = value.ToString(); + } + [JsonIgnore] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - Guid ID { get; set; } + string ID { get; set; } } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index b918eb0e78..feb03c1609 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -197,7 +197,7 @@ namespace osu.Game.Database public RealmWrapper(T original, IRealmFactory contextFactory) { ContextFactory = contextFactory; - ID = original.ID; + ID = original.Guid; var originalContext = original.Realm; diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index a8cd1c3fb6..332e4e2b21 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.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 osu.Framework.Input.Bindings; using osu.Game.Database; using Realms; @@ -11,7 +10,7 @@ namespace osu.Game.Input.Bindings [MapTo("KeyBinding")] public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey { - public Guid ID { get; set; } + public string ID { get; set; } public int? RulesetID { get; set; } From ee6a26bd6eff8988b30d061eb4b2bcbbaddbd755 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:42:21 +0900 Subject: [PATCH 090/670] Initialise new key bindings with a primary key --- osu.Game/Input/RealmKeyBindingStore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 471a25dd0d..752e254a43 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -50,6 +50,7 @@ namespace osu.Game.Input // insert any defaults which are missing. usage.Context.Add(new RealmKeyBinding { + ID = Guid.NewGuid().ToString(), KeyBinding = new KeyBinding { KeyCombination = insertable.KeyCombination, From 391259c713cb9b8303839ea4872c42f4345ea197 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:51:16 +0900 Subject: [PATCH 091/670] Add missing implementation details to realm keybinding store --- osu.Game/Input/RealmKeyBindingStore.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 752e254a43..f81d701e62 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -30,6 +30,23 @@ namespace osu.Game.Input } } + /// + /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. + /// + /// The action to lookup. + /// A set of display strings for all the user's key configuration for the action. + public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) + { + foreach (var action in Query().Where(b => (GlobalAction)b.KeyBinding.Action == globalAction)) + { + string str = action.KeyBinding.KeyCombination.ReadableString(); + + // even if found, the readable string may be empty for an unbound action. + if (str.Length > 0) + yield return str; + } + } + public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) From 382a40b24375c328ec356cf802cc37b67a30e2a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:51:29 +0900 Subject: [PATCH 092/670] Tidy up some missed inspections in RealmContextFactory --- osu.Game/Database/RealmContextFactory.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index feb03c1609..8e1a0bb8f7 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -59,7 +59,6 @@ namespace osu.Game.Database private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Realm", "Contexts"); - private Thread writingThread; /// /// Get a context for the current thread for read-only usage. @@ -86,11 +85,7 @@ namespace osu.Game.Database { context = getContextForCurrentThread(); - if (currentWriteTransaction == null) - { - writingThread = Thread.CurrentThread; - currentWriteTransaction = context.BeginWrite(); - } + currentWriteTransaction ??= context.BeginWrite(); } catch { @@ -144,7 +139,6 @@ namespace osu.Game.Database } currentWriteTransaction = null; - writingThread = null; rollbackRequired = false; refreshCompleted = new ThreadLocal(); From a9a3a959914cb288dec68dedb74f2b1236c9b23d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:51:38 +0900 Subject: [PATCH 093/670] Replace KeybindingStore with realm version --- osu.Game/OsuGameBase.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 95b1d3100c..22e72d9f36 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -73,7 +73,7 @@ namespace osu.Game protected FileStore FileStore; - protected KeyBindingStore KeyBindingStore; + protected RealmKeyBindingStore KeyBindingStore; protected SettingsStore SettingsStore; @@ -265,8 +265,10 @@ namespace osu.Game dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); - dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); - dependencies.Cache(new RealmKeyBindingStore(realmFactory, RulesetStore)); + // todo: migrate to realm + // dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + + dependencies.Cache(KeyBindingStore = new RealmKeyBindingStore(realmFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new SessionStatics()); From 43f417b53ab036b267dc4f59eab046085f3f1493 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 16:38:49 +0900 Subject: [PATCH 094/670] Add and consume IKeyBindingStore interface --- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 2 +- .../Bindings/DatabasedKeyBindingContainer.cs | 12 ++---- osu.Game/Input/IKeyBindingStore.cs | 40 +++++++++++++++++++ osu.Game/Input/KeyBindingStore.cs | 14 +++++-- osu.Game/Input/RealmKeyBindingStore.cs | 14 +++++-- osu.Game/OsuGameBase.cs | 4 +- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 2 +- .../KeyBinding/KeyBindingsSubsection.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 16 ++++++-- 9 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 osu.Game/Input/IKeyBindingStore.cs diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index b347c39c1e..eddaf36f92 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual typeof(FileStore), typeof(ScoreManager), typeof(BeatmapManager), - typeof(KeyBindingStore), + typeof(IKeyBindingStore), typeof(SettingsStore), typeof(RulesetConfigCache), typeof(OsuColour), diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 94edc33099..edaf18a760 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Rulesets; -using System.Linq; namespace osu.Game.Input.Bindings { @@ -21,7 +20,8 @@ namespace osu.Game.Input.Bindings private readonly int? variant; - private KeyBindingStore store; + [Resolved] + private IKeyBindingStore store { get; set; } public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); @@ -42,12 +42,6 @@ namespace osu.Game.Input.Bindings throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided."); } - [BackgroundDependencyLoader] - private void load(KeyBindingStore keyBindings) - { - store = keyBindings; - } - protected override void LoadComplete() { base.LoadComplete(); @@ -69,7 +63,7 @@ namespace osu.Game.Input.Bindings // fallback to defaults instead. KeyBindings = DefaultKeyBindings; else - KeyBindings = store.Query(ruleset?.ID, variant).ToList(); + KeyBindings = store.Query(ruleset?.ID, variant); } } } diff --git a/osu.Game/Input/IKeyBindingStore.cs b/osu.Game/Input/IKeyBindingStore.cs new file mode 100644 index 0000000000..3574c7237f --- /dev/null +++ b/osu.Game/Input/IKeyBindingStore.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; + +namespace osu.Game.Input +{ + public interface IKeyBindingStore + { + event Action KeyBindingChanged; + + void Register(KeyBindingContainer manager); + + /// + /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. + /// + /// The action to lookup. + /// A set of display strings for all the user's key configuration for the action. + IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction); + + /// + /// Retrieve s for a specified ruleset/variant content. + /// + /// The ruleset's internal ID. + /// An optional variant. + /// + List Query(int? rulesetId = null, int? variant = null); + + /// + /// Retrieve s for the specified action. + /// + /// The action to lookup. + List Query(GlobalAction action); + + public void Update(KeyBinding buttonKeyBinding) => throw new NotImplementedException(); + } +} diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index bc73d74d74..bbf26c4d8f 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets; namespace osu.Game.Input { - public class KeyBindingStore : DatabaseBackedStore + public class KeyBindingStore : DatabaseBackedStore, IKeyBindingStore { public event Action KeyBindingChanged; @@ -39,7 +39,7 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction)) + foreach (var action in query().Where(b => (GlobalAction)b.Action == globalAction)) { string str = action.KeyCombination.ReadableString(); @@ -49,6 +49,12 @@ namespace osu.Game.Input } } + public List Query(int? rulesetId = null, int? variant = null) + => query(rulesetId, variant).OfType().ToList(); + + public List Query(GlobalAction action) + => query(null, null).Where(dkb => (GlobalAction)dkb.Action == action).OfType().ToList(); + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = ContextFactory.GetForWrite()) @@ -56,7 +62,7 @@ namespace osu.Game.Input // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = Query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key); + int count = query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -86,7 +92,7 @@ namespace osu.Game.Input /// The ruleset's internal ID. /// An optional variant. /// - public List Query(int? rulesetId = null, int? variant = null) => + private List query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); public void Update(KeyBinding keyBinding) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index f81d701e62..07a340b25c 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets; namespace osu.Game.Input { - public class RealmKeyBindingStore : RealmBackedStore + public class RealmKeyBindingStore : RealmBackedStore, IKeyBindingStore { public event Action KeyBindingChanged; @@ -37,7 +37,7 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in Query().Where(b => (GlobalAction)b.KeyBinding.Action == globalAction)) + foreach (var action in query().Where(b => (GlobalAction)b.KeyBinding.Action == globalAction)) { string str = action.KeyBinding.KeyCombination.ReadableString(); @@ -56,7 +56,7 @@ namespace osu.Game.Input // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = Query(rulesetId, variant).Count(k => k.KeyBinding.Action == group.Key); + int count = query(rulesetId, variant).Count(k => (int)k.KeyBinding.Action == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -87,9 +87,15 @@ namespace osu.Game.Input /// The ruleset's internal ID. /// An optional variant. /// - public List Query(int? rulesetId = null, int? variant = null) => + private List query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + public List Query(int? rulesetId = null, int? variant = null) + => query(rulesetId, variant).Select(k => k.KeyBinding).ToList(); + + public List Query(GlobalAction action) + => query(null, null).Where(rkb => rkb.KeyBindingString.StartsWith($"{(int)action}:", StringComparison.Ordinal)).Select(k => k.KeyBinding).ToList(); + public void Update(KeyBinding keyBinding) { using (ContextFactory.GetForWrite()) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 22e72d9f36..95e1bc69b3 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -73,7 +73,7 @@ namespace osu.Game protected FileStore FileStore; - protected RealmKeyBindingStore KeyBindingStore; + protected IKeyBindingStore KeyBindingStore; protected SettingsStore SettingsStore; @@ -268,7 +268,7 @@ namespace osu.Game // todo: migrate to realm // dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); - dependencies.Cache(KeyBindingStore = new RealmKeyBindingStore(realmFactory, RulesetStore)); + dependencies.CacheAs(KeyBindingStore = new RealmKeyBindingStore(realmFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new SessionStatics()); diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index b808d49fa2..d9f63328d0 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -66,7 +66,7 @@ namespace osu.Game.Overlays.KeyBinding } [Resolved] - private KeyBindingStore store { get; set; } + private IKeyBindingStore store { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index d784b7aec9..b5d6bc98c3 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.KeyBinding } [BackgroundDependencyLoader] - private void load(KeyBindingStore store) + private void load(IKeyBindingStore store) { var bindings = store.Query(Ruleset?.ID, variant); diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 49b9c62d85..747f5e9bd0 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.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.Linq; using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; @@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private KeyBindingStore keyBindings { get; set; } + private IKeyBindingStore keyBindings { get; set; } protected ToolbarButton() : base(HoverSampleSet.Loud) @@ -171,9 +172,16 @@ namespace osu.Game.Overlays.Toolbar if (tooltipKeyBinding.IsValid) return; - var binding = keyBindings.Query().Find(b => (GlobalAction)b.Action == Hotkey); - var keyBindingString = binding?.KeyCombination.ReadableString(); - keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) ? $" ({keyBindingString})" : string.Empty; + keyBindingTooltip.Text = string.Empty; + + if (Hotkey != null) + { + KeyCombination? binding = keyBindings.Query(Hotkey.Value).FirstOrDefault()?.KeyCombination; + var keyBindingString = binding?.ReadableString(); + + if (!string.IsNullOrEmpty(keyBindingString)) + keyBindingTooltip.Text = $" ({keyBindingString})"; + } tooltipKeyBinding.Validate(); } From a77519c6bde35c3b24e0e1888ec7059e1f149555 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 16:53:36 +0900 Subject: [PATCH 095/670] Store KeyBinding action to its own field in realm Also improve the Query method for action types by using generic field --- osu.Game/Database/RealmContextFactory.cs | 2 +- osu.Game/Input/Bindings/RealmKeyBinding.cs | 14 ++++++++------ osu.Game/Input/IKeyBindingStore.cs | 2 +- osu.Game/Input/KeyBindingStore.cs | 7 +++++-- osu.Game/Input/RealmKeyBindingStore.cs | 9 +++++++-- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 8e1a0bb8f7..2f6ccb8911 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -163,7 +163,7 @@ namespace osu.Game.Database contexts.Value++; return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) { - SchemaVersion = 2, + SchemaVersion = 3, MigrationCallback = onMigration }); } diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 332e4e2b21..eb04766d04 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -16,17 +16,19 @@ namespace osu.Game.Input.Bindings public int? Variant { get; set; } + public int Action { get; set; } + + public string KeyCombination { get; set; } + [Ignored] public KeyBinding KeyBinding { - get + get => new KeyBinding(KeyCombination, Action); + set { - var split = KeyBindingString.Split(':'); - return new KeyBinding(split[0], int.Parse(split[1])); + KeyCombination = value.KeyCombination.ToString(); + Action = (int)value.Action; } - set => KeyBindingString = $"{value.KeyCombination}:{(int)value.Action}"; } - - public string KeyBindingString { get; set; } } } diff --git a/osu.Game/Input/IKeyBindingStore.cs b/osu.Game/Input/IKeyBindingStore.cs index 3574c7237f..c5e68dc6ca 100644 --- a/osu.Game/Input/IKeyBindingStore.cs +++ b/osu.Game/Input/IKeyBindingStore.cs @@ -33,7 +33,7 @@ namespace osu.Game.Input /// Retrieve s for the specified action. /// /// The action to lookup. - List Query(GlobalAction action); + List Query(T action) where T : Enum; public void Update(KeyBinding buttonKeyBinding) => throw new NotImplementedException(); } diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index bbf26c4d8f..53eb0024f8 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -52,8 +52,11 @@ namespace osu.Game.Input public List Query(int? rulesetId = null, int? variant = null) => query(rulesetId, variant).OfType().ToList(); - public List Query(GlobalAction action) - => query(null, null).Where(dkb => (GlobalAction)dkb.Action == action).OfType().ToList(); + public List Query(T action) where T : Enum + { + int lookup = (int)(object)action; + return query(null, null).Where(rkb => (int)rkb.Action == lookup).OfType().ToList(); + } private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 07a340b25c..37d0ce18ed 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -93,8 +93,13 @@ namespace osu.Game.Input public List Query(int? rulesetId = null, int? variant = null) => query(rulesetId, variant).Select(k => k.KeyBinding).ToList(); - public List Query(GlobalAction action) - => query(null, null).Where(rkb => rkb.KeyBindingString.StartsWith($"{(int)action}:", StringComparison.Ordinal)).Select(k => k.KeyBinding).ToList(); + public List Query(T action) + where T : Enum + { + int lookup = (int)(object)action; + + return query(null, null).Where(rkb => rkb.Action == lookup).Select(k => k.KeyBinding).ToList(); + } public void Update(KeyBinding keyBinding) { From 8765aaf9e669a88eacd31089671bfac20fd31e66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 15:49:01 +0900 Subject: [PATCH 096/670] Use IKeyBinding for all key binding usages (and add update flow via primary key) --- .../Gameplay/TestSceneReplayRecorder.cs | 2 +- .../Gameplay/TestSceneReplayRecording.cs | 2 +- .../Gameplay/TestSceneSpectatorPlayback.cs | 2 +- .../Input/Bindings/DatabasedKeyBinding.cs | 24 ++++++++----- .../Bindings/DatabasedKeyBindingContainer.cs | 2 +- .../Input/Bindings/GlobalActionContainer.cs | 2 +- osu.Game/Input/Bindings/RealmKeyBinding.cs | 15 +++++++- osu.Game/Input/IKeyBindingStore.cs | 9 ++--- osu.Game/Input/KeyBindingStore.cs | 19 +++++++---- osu.Game/Input/RealmKeyBindingStore.cs | 34 ++++++++----------- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 13 +++---- 11 files changed, 72 insertions(+), 52 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index b2ad7ca5b4..802dbf2021 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestKeyBindingContainer : KeyBindingContainer { - public override IEnumerable DefaultKeyBindings => new[] + public override IEnumerable DefaultKeyBindings => new[] { new KeyBinding(InputKey.MouseLeft, TestAction.Down), }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index 40c4214749..6e338b7202 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestKeyBindingContainer : KeyBindingContainer { - public override IEnumerable DefaultKeyBindings => new[] + public override IEnumerable DefaultKeyBindings => new[] { new KeyBinding(InputKey.MouseLeft, TestAction.Down), }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index e148fa381c..a5fd5afcde 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -298,7 +298,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestKeyBindingContainer : KeyBindingContainer { - public override IEnumerable DefaultKeyBindings => new[] + public override IEnumerable DefaultKeyBindings => new[] { new KeyBinding(InputKey.MouseLeft, TestAction.Down), }; diff --git a/osu.Game/Input/Bindings/DatabasedKeyBinding.cs b/osu.Game/Input/Bindings/DatabasedKeyBinding.cs index 8c0072c3da..ad3493d0fc 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBinding.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBinding.cs @@ -8,7 +8,7 @@ using osu.Game.Database; namespace osu.Game.Input.Bindings { [Table("KeyBinding")] - public class DatabasedKeyBinding : KeyBinding, IHasPrimaryKey + public class DatabasedKeyBinding : IKeyBinding, IHasPrimaryKey { public int ID { get; set; } @@ -17,17 +17,23 @@ namespace osu.Game.Input.Bindings public int? Variant { get; set; } [Column("Keys")] - public string KeysString - { - get => KeyCombination.ToString(); - private set => KeyCombination = value; - } + public string KeysString { get; set; } [Column("Action")] - public int IntAction + public int IntAction { get; set; } + + [NotMapped] + public KeyCombination KeyCombination { - get => (int)Action; - set => Action = value; + get => KeysString; + set => KeysString = value.ToString(); + } + + [NotMapped] + public object Action + { + get => IntAction; + set => IntAction = (int)value; } } } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index edaf18a760..ab4854bfd2 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -23,7 +23,7 @@ namespace osu.Game.Input.Bindings [Resolved] private IKeyBindingStore store { get; set; } - public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); + public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); /// /// Create a new instance. diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index b8c2fa201f..8ccdb9249e 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Input.Bindings handler = game; } - public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings); + public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings); public IEnumerable GlobalKeyBindings => new[] { diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index eb04766d04..088a314fec 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -8,14 +8,27 @@ using Realms; namespace osu.Game.Input.Bindings { [MapTo("KeyBinding")] - public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey + public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { + [PrimaryKey] public string ID { get; set; } public int? RulesetID { get; set; } public int? Variant { get; set; } + KeyCombination IKeyBinding.KeyCombination + { + get => KeyCombination; + set => KeyCombination = value.ToString(); + } + + object IKeyBinding.Action + { + get => Action; + set => Action = (int)value; + } + public int Action { get; set; } public string KeyCombination { get; set; } diff --git a/osu.Game/Input/IKeyBindingStore.cs b/osu.Game/Input/IKeyBindingStore.cs index c5e68dc6ca..50994cb542 100644 --- a/osu.Game/Input/IKeyBindingStore.cs +++ b/osu.Game/Input/IKeyBindingStore.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Input.Bindings; +using osu.Game.Database; using osu.Game.Input.Bindings; namespace osu.Game.Input @@ -27,14 +28,14 @@ namespace osu.Game.Input /// The ruleset's internal ID. /// An optional variant. /// - List Query(int? rulesetId = null, int? variant = null); + List Query(int? rulesetId = null, int? variant = null); /// - /// Retrieve s for the specified action. + /// Retrieve s for the specified action. /// /// The action to lookup. - List Query(T action) where T : Enum; + List Query(T action) where T : Enum; - public void Update(KeyBinding buttonKeyBinding) => throw new NotImplementedException(); + void Update(IHasGuidPrimaryKey keyBinding, Action modification); } } diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index 53eb0024f8..ad6bcb4c7c 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -49,16 +49,21 @@ namespace osu.Game.Input } } - public List Query(int? rulesetId = null, int? variant = null) - => query(rulesetId, variant).OfType().ToList(); + public List Query(int? rulesetId = null, int? variant = null) + => query(rulesetId, variant).OfType().ToList(); - public List Query(T action) where T : Enum + public List Query(T action) where T : Enum { int lookup = (int)(object)action; - return query(null, null).Where(rkb => (int)rkb.Action == lookup).OfType().ToList(); + return query(null, null).Where(rkb => (int)rkb.Action == lookup).OfType().ToList(); } - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) + public void Update(IHasGuidPrimaryKey keyBinding, Action modification) + { + throw new NotImplementedException(); + } + + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = ContextFactory.GetForWrite()) { @@ -98,11 +103,11 @@ namespace osu.Game.Input private List query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); - public void Update(KeyBinding keyBinding) + public void Update(DatabasedKeyBinding keyBinding) { using (ContextFactory.GetForWrite()) { - var dbKeyBinding = (DatabasedKeyBinding)keyBinding; + var dbKeyBinding = keyBinding; Refresh(ref dbKeyBinding); if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 37d0ce18ed..2455578ddb 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -37,9 +37,9 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in query().Where(b => (GlobalAction)b.KeyBinding.Action == globalAction)) + foreach (var action in query().Where(b => (GlobalAction)b.Action == globalAction)) { - string str = action.KeyBinding.KeyCombination.ReadableString(); + string str = ((IKeyBinding)action).KeyCombination.ReadableString(); // even if found, the readable string may be empty for an unbound action. if (str.Length > 0) @@ -49,14 +49,14 @@ namespace osu.Game.Input public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = ContextFactory.GetForWrite()) { // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = query(rulesetId, variant).Count(k => (int)k.KeyBinding.Action == (int)group.Key); + int count = query(rulesetId, variant).Count(k => k.Action == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -87,32 +87,26 @@ namespace osu.Game.Input /// The ruleset's internal ID. /// An optional variant. /// - private List query(int? rulesetId = null, int? variant = null) => - ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + private IQueryable query(int? rulesetId = null, int? variant = null) => + ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); - public List Query(int? rulesetId = null, int? variant = null) - => query(rulesetId, variant).Select(k => k.KeyBinding).ToList(); + public List Query(int? rulesetId = null, int? variant = null) + => query(rulesetId, variant).ToList().Select(r => r.Detach()).ToList(); - public List Query(T action) + public List Query(T action) where T : Enum { int lookup = (int)(object)action; - return query(null, null).Where(rkb => rkb.Action == lookup).Select(k => k.KeyBinding).ToList(); + return query(null, null).Where(rkb => rkb.Action == lookup).ToList().Select(r => r.Detach()).ToList(); } - public void Update(KeyBinding keyBinding) + public void Update(IHasGuidPrimaryKey keyBinding, Action modification) { - using (ContextFactory.GetForWrite()) + using (var realm = ContextFactory.GetForWrite()) { - //todo: fix - // var dbKeyBinding = (RealmKeyBinding)keyBinding; - // Refresh(ref dbKeyBinding); - // - // if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) - // return; - // - // dbKeyBinding.KeyCombination = keyBinding.KeyCombination; + var realmKeyBinding = realm.Context.Find(keyBinding.ID); + modification(realmKeyBinding); } KeyBindingChanged?.Invoke(); diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index d9f63328d0..87d51e5268 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -25,7 +26,7 @@ namespace osu.Game.Overlays.KeyBinding public class KeyBindingRow : Container, IFilterable { private readonly object action; - private readonly IEnumerable bindings; + private readonly IEnumerable bindings; private const float transition_time = 150; @@ -53,7 +54,7 @@ namespace osu.Game.Overlays.KeyBinding public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text); - public KeyBindingRow(object action, IEnumerable bindings) + public KeyBindingRow(object action, IEnumerable bindings) { this.action = action; this.bindings = bindings; @@ -126,7 +127,7 @@ namespace osu.Game.Overlays.KeyBinding { var button = buttons[i++]; button.UpdateKeyCombination(d); - store.Update(button.KeyBinding); + store.Update((IHasGuidPrimaryKey)button.KeyBinding, k => k.KeyCombination = button.KeyBinding.KeyCombination); } } @@ -285,7 +286,7 @@ namespace osu.Game.Overlays.KeyBinding { if (bindTarget != null) { - store.Update(bindTarget.KeyBinding); + store.Update((IHasGuidPrimaryKey)bindTarget.KeyBinding, k => k.KeyCombination = bindTarget.KeyBinding.KeyCombination); bindTarget.IsBinding = false; Schedule(() => @@ -359,7 +360,7 @@ namespace osu.Game.Overlays.KeyBinding public class KeyButton : Container { - public readonly Framework.Input.Bindings.KeyBinding KeyBinding; + public readonly IKeyBinding KeyBinding; private readonly Box box; public readonly OsuSpriteText Text; @@ -381,7 +382,7 @@ namespace osu.Game.Overlays.KeyBinding } } - public KeyButton(Framework.Input.Bindings.KeyBinding keyBinding) + public KeyButton(IKeyBinding keyBinding) { KeyBinding = keyBinding; From 86daf65630592944e9f505c4db74745d9b8fa651 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 15:49:11 +0900 Subject: [PATCH 097/670] Fix primary key not being populated for KeyBinding --- osu.Game/Database/IHasGuidPrimaryKey.cs | 1 + osu.Game/Database/RealmContextFactory.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index 33618e990d..3b0888c654 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -20,6 +20,7 @@ namespace osu.Game.Database [JsonIgnore] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [PrimaryKey] string ID { get; set; } } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 2f6ccb8911..62e2dbba9a 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -163,7 +163,7 @@ namespace osu.Game.Database contexts.Value++; return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) { - SchemaVersion = 3, + SchemaVersion = 5, MigrationCallback = onMigration }); } @@ -211,6 +211,12 @@ namespace osu.Game.Database public RealmWrapper WrapChild(Func lookup) where TChild : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(lookup(Get()), ContextFactory); + public void PerformUpdate(Action perform) + { + using (ContextFactory.GetForWrite()) + perform(this); + } + // ReSharper disable once CA2225 public static implicit operator T(RealmWrapper wrapper) => wrapper?.Get().Detach(); @@ -241,6 +247,7 @@ namespace osu.Game.Database .MaxDepth(2); c.CreateMap(); + c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); From a1cb6d8c546327921b740aeb1ecc28b610177613 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 17:44:12 +0900 Subject: [PATCH 098/670] Remove unnecesssary local conversion method --- osu.Game/Input/Bindings/RealmKeyBinding.cs | 11 ----------- osu.Game/Input/RealmKeyBindingStore.cs | 7 ++----- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 088a314fec..1e690ddbab 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -32,16 +32,5 @@ namespace osu.Game.Input.Bindings public int Action { get; set; } public string KeyCombination { get; set; } - - [Ignored] - public KeyBinding KeyBinding - { - get => new KeyBinding(KeyCombination, Action); - set - { - KeyCombination = value.KeyCombination.ToString(); - Action = (int)value.Action; - } - } } } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 2455578ddb..33172921cb 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -68,11 +68,8 @@ namespace osu.Game.Input usage.Context.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), - KeyBinding = new KeyBinding - { - KeyCombination = insertable.KeyCombination, - Action = insertable.Action, - }, + KeyCombination = insertable.KeyCombination.ToString(), + Action = (int)insertable.Action, RulesetID = rulesetId, Variant = variant }); From 1abed11fb7b80977f6af07d2525c97d23ea917c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 17:44:19 +0900 Subject: [PATCH 099/670] Add basic migration logic of key bindings to realm --- osu.Game/OsuGameBase.cs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 95e1bc69b3..3fa55ab594 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -24,6 +24,7 @@ using osu.Game.Online.API; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Database; @@ -265,10 +266,10 @@ namespace osu.Game dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); - // todo: migrate to realm - // dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + migrateDataToRealm(); dependencies.CacheAs(KeyBindingStore = new RealmKeyBindingStore(realmFactory, RulesetStore)); + dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new SessionStatics()); @@ -322,6 +323,29 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); } + private void migrateDataToRealm() + { + using (var db = contextFactory.GetForWrite()) + using (var realm = realmFactory.GetForWrite()) + { + var existingBindings = db.Context.DatabasedKeyBinding; + + foreach (var dkb in existingBindings) + { + realm.Context.Add(new RealmKeyBinding + { + ID = Guid.NewGuid().ToString(), + KeyCombination = dkb.KeyCombination.ToString(), + Action = (int)dkb.Action, + RulesetID = dkb.RulesetID, + Variant = dkb.Variant + }); + } + + db.Context.RemoveRange(existingBindings); + } + } + private void onRulesetChanged(ValueChangedEvent r) { var dict = new Dictionary>(); From 56d34432f92b486e24623172b1cc18feeccdc421 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 15:56:41 +0900 Subject: [PATCH 100/670] Move public members up --- osu.Game/Input/RealmKeyBindingStore.cs | 44 +++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 33172921cb..9910882cef 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -49,6 +49,28 @@ namespace osu.Game.Input public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); + public List Query(int? rulesetId = null, int? variant = null) + => query(rulesetId, variant).ToList().Select(r => r.Detach()).ToList(); + + public List Query(T action) + where T : Enum + { + int lookup = (int)(object)action; + + return query(null, null).Where(rkb => rkb.Action == lookup).ToList().Select(r => r.Detach()).ToList(); + } + + public void Update(IHasGuidPrimaryKey keyBinding, Action modification) + { + using (var realm = ContextFactory.GetForWrite()) + { + var realmKeyBinding = realm.Context.Find(keyBinding.ID); + modification(realmKeyBinding); + } + + KeyBindingChanged?.Invoke(); + } + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = ContextFactory.GetForWrite()) @@ -86,27 +108,5 @@ namespace osu.Game.Input /// private IQueryable query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); - - public List Query(int? rulesetId = null, int? variant = null) - => query(rulesetId, variant).ToList().Select(r => r.Detach()).ToList(); - - public List Query(T action) - where T : Enum - { - int lookup = (int)(object)action; - - return query(null, null).Where(rkb => rkb.Action == lookup).ToList().Select(r => r.Detach()).ToList(); - } - - public void Update(IHasGuidPrimaryKey keyBinding, Action modification) - { - using (var realm = ContextFactory.GetForWrite()) - { - var realmKeyBinding = realm.Context.Find(keyBinding.ID); - modification(realmKeyBinding); - } - - KeyBindingChanged?.Invoke(); - } } } From f9717e8b693346c1867e0f0bbc9c69bca1b8f9f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:00:11 +0900 Subject: [PATCH 101/670] Don't migrate existing key bindings across if realm is already populated --- osu.Game/OsuGameBase.cs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3fa55ab594..4b64bf2e24 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -24,7 +24,6 @@ using osu.Game.Online.API; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; -using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Database; @@ -330,16 +329,20 @@ namespace osu.Game { var existingBindings = db.Context.DatabasedKeyBinding; - foreach (var dkb in existingBindings) + // only migrate data if the realm database is empty. + if (!realm.Context.All().Any()) { - realm.Context.Add(new RealmKeyBinding + foreach (var dkb in existingBindings) { - ID = Guid.NewGuid().ToString(), - KeyCombination = dkb.KeyCombination.ToString(), - Action = (int)dkb.Action, - RulesetID = dkb.RulesetID, - Variant = dkb.Variant - }); + realm.Context.Add(new RealmKeyBinding + { + ID = Guid.NewGuid().ToString(), + KeyCombination = dkb.KeyCombination.ToString(), + Action = (int)dkb.Action, + RulesetID = dkb.RulesetID, + Variant = dkb.Variant + }); + } } db.Context.RemoveRange(existingBindings); From 6fd098ca7cb84148d238a40a5c8c666cce0b327d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:18:25 +0900 Subject: [PATCH 102/670] Add full xmldoc to RealmKeyBindingStore --- osu.Game/Input/IKeyBindingStore.cs | 2 +- osu.Game/Input/KeyBindingStore.cs | 2 +- osu.Game/Input/RealmKeyBindingStore.cs | 42 +++++++++++++++++++++----- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/osu.Game/Input/IKeyBindingStore.cs b/osu.Game/Input/IKeyBindingStore.cs index 50994cb542..ef1d043c33 100644 --- a/osu.Game/Input/IKeyBindingStore.cs +++ b/osu.Game/Input/IKeyBindingStore.cs @@ -13,7 +13,7 @@ namespace osu.Game.Input { event Action KeyBindingChanged; - void Register(KeyBindingContainer manager); + void Register(KeyBindingContainer container); /// /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index ad6bcb4c7c..c55d62b7d6 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -30,7 +30,7 @@ namespace osu.Game.Input } } - public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); + public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings); /// /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 9910882cef..cd0b85cd8d 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -14,11 +14,15 @@ namespace osu.Game.Input { public class RealmKeyBindingStore : RealmBackedStore, IKeyBindingStore { + /// + /// Fired whenever any key binding change occurs, across all rulesets and types. + /// public event Action KeyBindingChanged; public RealmKeyBindingStore(RealmContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) : base(contextFactory, storage) { + // populate defaults from rulesets. using (ContextFactory.GetForWrite()) { foreach (RulesetInfo info in rulesets.AvailableRulesets) @@ -47,11 +51,27 @@ namespace osu.Game.Input } } - public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); + /// + /// Register a new type of , adding default bindings from . + /// + /// The container to populate defaults from. + public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings); + /// + /// Retrieve all key bindings for the provided specification. + /// + /// An optional ruleset ID. If null, global bindings are returned. + /// An optional ruleset variant. If null, the no-variant bindings are returned. + /// A list of all key bindings found for the query, detached from the database. public List Query(int? rulesetId = null, int? variant = null) => query(rulesetId, variant).ToList().Select(r => r.Detach()).ToList(); + /// + /// Retrieve all key bindings for the provided action type. + /// + /// The action to lookup. + /// The enum type of the action. + /// A list of all key bindings found for the query, detached from the database. public List Query(T action) where T : Enum { @@ -60,12 +80,21 @@ namespace osu.Game.Input return query(null, null).Where(rkb => rkb.Action == lookup).ToList().Select(r => r.Detach()).ToList(); } + /// + /// Update the database mapping for the provided key binding. + /// + /// The key binding to update. Can be detached from the database. + /// The modification to apply to the key binding. public void Update(IHasGuidPrimaryKey keyBinding, Action modification) { using (var realm = ContextFactory.GetForWrite()) { - var realmKeyBinding = realm.Context.Find(keyBinding.ID); - modification(realmKeyBinding); + RealmKeyBinding realmBinding = keyBinding as RealmKeyBinding; + + if (realmBinding?.IsManaged != true) + realmBinding = realm.Context.Find(keyBinding.ID); + + modification(realmBinding); } KeyBindingChanged?.Invoke(); @@ -101,11 +130,10 @@ namespace osu.Game.Input } /// - /// Retrieve s for a specified ruleset/variant content. + /// Retrieve live queryable s for a specified ruleset/variant content. /// - /// The ruleset's internal ID. - /// An optional variant. - /// + /// An optional ruleset ID. If null, global bindings are returned. + /// An optional ruleset variant. If null, the no-variant bindings are returned. private IQueryable query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); } From 6c90f9ceeda882f79526f4760f169a9e61905783 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:22:52 +0900 Subject: [PATCH 103/670] Move RealmWrapper to own file --- osu.Game/Database/RealmContextFactory.cs | 52 -------------------- osu.Game/Database/RealmWrapper.cs | 61 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 52 deletions(-) create mode 100644 osu.Game/Database/RealmWrapper.cs diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 62e2dbba9a..0cdcdc2ef0 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics.CodeAnalysis; using System.Threading; using AutoMapper; using osu.Framework.Platform; @@ -178,57 +177,6 @@ namespace osu.Game.Database } } - [SuppressMessage("ReSharper", "CA2225")] - public class RealmWrapper : IEquatable> - where T : RealmObject, IHasGuidPrimaryKey - { - public Guid ID { get; } - - private readonly ThreadLocal threadValues; - - public readonly IRealmFactory ContextFactory; - - public RealmWrapper(T original, IRealmFactory contextFactory) - { - ContextFactory = contextFactory; - ID = original.Guid; - - var originalContext = original.Realm; - - threadValues = new ThreadLocal(() => - { - var context = ContextFactory?.Get(); - - if (context == null || originalContext?.IsSameInstance(context) != false) - return original; - - return context.Find(ID); - }); - } - - public T Get() => threadValues.Value; - - public RealmWrapper WrapChild(Func lookup) - where TChild : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(lookup(Get()), ContextFactory); - - public void PerformUpdate(Action perform) - { - using (ContextFactory.GetForWrite()) - perform(this); - } - - // ReSharper disable once CA2225 - public static implicit operator T(RealmWrapper wrapper) - => wrapper?.Get().Detach(); - - // ReSharper disable once CA2225 - public static implicit operator RealmWrapper(T obj) => obj.WrapAsUnmanaged(); - - public bool Equals(RealmWrapper other) => other != null && other.ID == ID; - - public override string ToString() => Get().ToString(); - } - public static class RealmExtensions { private static readonly IMapper mapper = new MapperConfiguration(c => diff --git a/osu.Game/Database/RealmWrapper.cs b/osu.Game/Database/RealmWrapper.cs new file mode 100644 index 0000000000..06b1bbee90 --- /dev/null +++ b/osu.Game/Database/RealmWrapper.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Realms; + +namespace osu.Game.Database +{ + [SuppressMessage("ReSharper", "CA2225")] + public class RealmWrapper : IEquatable> + where T : RealmObject, IHasGuidPrimaryKey + { + public Guid ID { get; } + + private readonly ThreadLocal threadValues; + + public readonly IRealmFactory ContextFactory; + + public RealmWrapper(T original, IRealmFactory contextFactory) + { + ContextFactory = contextFactory; + ID = original.Guid; + + var originalContext = original.Realm; + + threadValues = new ThreadLocal(() => + { + var context = ContextFactory?.Get(); + + if (context == null || originalContext?.IsSameInstance(context) != false) + return original; + + return context.Find(ID); + }); + } + + public T Get() => threadValues.Value; + + public RealmWrapper WrapChild(Func lookup) + where TChild : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(lookup(Get()), ContextFactory); + + public void PerformUpdate(Action perform) + { + using (ContextFactory.GetForWrite()) + perform(this); + } + + // ReSharper disable once CA2225 + public static implicit operator T(RealmWrapper wrapper) + => wrapper?.Get().Detach(); + + // ReSharper disable once CA2225 + public static implicit operator RealmWrapper(T obj) => obj.WrapAsUnmanaged(); + + public bool Equals(RealmWrapper other) => other != null && other.ID == ID; + + public override string ToString() => Get().ToString(); + } +} From cdb3d20fc62465946a98730452baf6a381da14e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:24:12 +0900 Subject: [PATCH 104/670] Remove unnecessary warning suppression --- osu.Game/Database/RealmWrapper.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Database/RealmWrapper.cs b/osu.Game/Database/RealmWrapper.cs index 06b1bbee90..9792cce527 100644 --- a/osu.Game/Database/RealmWrapper.cs +++ b/osu.Game/Database/RealmWrapper.cs @@ -2,13 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics.CodeAnalysis; using System.Threading; using Realms; namespace osu.Game.Database { - [SuppressMessage("ReSharper", "CA2225")] public class RealmWrapper : IEquatable> where T : RealmObject, IHasGuidPrimaryKey { From 5bb4d359826f2df850a167c502bda25408edd1f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:26:45 +0900 Subject: [PATCH 105/670] Make RealmWrapper nullable enabled --- osu.Game/Database/RealmWrapper.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmWrapper.cs b/osu.Game/Database/RealmWrapper.cs index 9792cce527..a754e208bd 100644 --- a/osu.Game/Database/RealmWrapper.cs +++ b/osu.Game/Database/RealmWrapper.cs @@ -5,6 +5,8 @@ using System; using System.Threading; using Realms; +#nullable enable + namespace osu.Game.Database { public class RealmWrapper : IEquatable> @@ -25,7 +27,7 @@ namespace osu.Game.Database threadValues = new ThreadLocal(() => { - var context = ContextFactory?.Get(); + var context = ContextFactory.Get(); if (context == null || originalContext?.IsSameInstance(context) != false) return original; @@ -42,17 +44,15 @@ namespace osu.Game.Database public void PerformUpdate(Action perform) { using (ContextFactory.GetForWrite()) - perform(this); + perform(Get()); } - // ReSharper disable once CA2225 - public static implicit operator T(RealmWrapper wrapper) + public static implicit operator T?(RealmWrapper? wrapper) => wrapper?.Get().Detach(); - // ReSharper disable once CA2225 public static implicit operator RealmWrapper(T obj) => obj.WrapAsUnmanaged(); - public bool Equals(RealmWrapper other) => other != null && other.ID == ID; + public bool Equals(RealmWrapper? other) => other != null && other.ID == ID; public override string ToString() => Get().ToString(); } From 9f64f6059fd2597bf55f595c4b7b2b44180327a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:30:55 +0900 Subject: [PATCH 106/670] Rename RealmWrapper to Live --- osu.Game/Database/{RealmWrapper.cs => Live.cs} | 14 +++++++------- osu.Game/Database/RealmContextFactory.cs | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Database/{RealmWrapper.cs => Live.cs} (71%) diff --git a/osu.Game/Database/RealmWrapper.cs b/osu.Game/Database/Live.cs similarity index 71% rename from osu.Game/Database/RealmWrapper.cs rename to osu.Game/Database/Live.cs index a754e208bd..3c5e1db157 100644 --- a/osu.Game/Database/RealmWrapper.cs +++ b/osu.Game/Database/Live.cs @@ -9,7 +9,7 @@ using Realms; namespace osu.Game.Database { - public class RealmWrapper : IEquatable> + public class Live : IEquatable> where T : RealmObject, IHasGuidPrimaryKey { public Guid ID { get; } @@ -18,7 +18,7 @@ namespace osu.Game.Database public readonly IRealmFactory ContextFactory; - public RealmWrapper(T original, IRealmFactory contextFactory) + public Live(T original, IRealmFactory contextFactory) { ContextFactory = contextFactory; ID = original.Guid; @@ -38,8 +38,8 @@ namespace osu.Game.Database public T Get() => threadValues.Value; - public RealmWrapper WrapChild(Func lookup) - where TChild : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(lookup(Get()), ContextFactory); + public Live WrapChild(Func lookup) + where TChild : RealmObject, IHasGuidPrimaryKey => new Live(lookup(Get()), ContextFactory); public void PerformUpdate(Action perform) { @@ -47,12 +47,12 @@ namespace osu.Game.Database perform(Get()); } - public static implicit operator T?(RealmWrapper? wrapper) + public static implicit operator T?(Live? wrapper) => wrapper?.Get().Detach(); - public static implicit operator RealmWrapper(T obj) => obj.WrapAsUnmanaged(); + public static implicit operator Live(T obj) => obj.WrapAsUnmanaged(); - public bool Equals(RealmWrapper? other) => other != null && other.ID == ID; + public bool Equals(Live? other) => other != null && other.ID == ID; public override string ToString() => Get().ToString(); } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0cdcdc2ef0..71e9f8c4e1 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -215,10 +215,10 @@ namespace osu.Game.Database return detached; } - public static RealmWrapper Wrap(this T obj, IRealmFactory contextFactory) - where T : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(obj, contextFactory); + public static Live Wrap(this T obj, IRealmFactory contextFactory) + where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, contextFactory); - public static RealmWrapper WrapAsUnmanaged(this T obj) - where T : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(obj, null); + public static Live WrapAsUnmanaged(this T obj) + where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, null); } } From 20584c9e163cdb9680fd7ec985a3826bb52f6dc8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:50:14 +0900 Subject: [PATCH 107/670] Add full xmldoc for Live class --- osu.Game/Database/Live.cs | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs index 3c5e1db157..86cecd51d8 100644 --- a/osu.Game/Database/Live.cs +++ b/osu.Game/Database/Live.cs @@ -9,25 +9,36 @@ using Realms; namespace osu.Game.Database { + /// + /// Provides a method of passing realm live objects across threads in a safe fashion. + /// + /// + /// To consume this as a live instance, the live object should be stored and accessed via each time. + /// To consume this as a detached instance, assign to a variable of type . The implicit conversion will handle detaching an instance. + /// + /// The underlying object type. Should be a with a primary key provided via . public class Live : IEquatable> where T : RealmObject, IHasGuidPrimaryKey { + /// + /// The primary key of the object. + /// public Guid ID { get; } private readonly ThreadLocal threadValues; - public readonly IRealmFactory ContextFactory; + private readonly IRealmFactory contextFactory; public Live(T original, IRealmFactory contextFactory) { - ContextFactory = contextFactory; + this.contextFactory = contextFactory; ID = original.Guid; var originalContext = original.Realm; threadValues = new ThreadLocal(() => { - var context = ContextFactory.Get(); + var context = this.contextFactory.Get(); if (context == null || originalContext?.IsSameInstance(context) != false) return original; @@ -36,14 +47,27 @@ namespace osu.Game.Database }); } + /// + /// Retrieve a live reference to the data. + /// public T Get() => threadValues.Value; + /// + /// Wrap a property of this instance as its own live access object. + /// + /// The child to return. + /// The underlying child object type. Should be a with a primary key provided via . + /// A wrapped instance of the child. public Live WrapChild(Func lookup) - where TChild : RealmObject, IHasGuidPrimaryKey => new Live(lookup(Get()), ContextFactory); + where TChild : RealmObject, IHasGuidPrimaryKey => new Live(lookup(Get()), contextFactory); + /// + /// Perform a write operation on this live object. + /// + /// The action to perform. public void PerformUpdate(Action perform) { - using (ContextFactory.GetForWrite()) + using (contextFactory.GetForWrite()) perform(Get()); } From 05ca016deb04f6e5166f5255d490d82831c7b11e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:57:40 +0900 Subject: [PATCH 108/670] Make Live implement IHasGuidPrimaryKey --- osu.Game/Database/Live.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs index 86cecd51d8..7d607a4637 100644 --- a/osu.Game/Database/Live.cs +++ b/osu.Game/Database/Live.cs @@ -17,13 +17,19 @@ namespace osu.Game.Database /// To consume this as a detached instance, assign to a variable of type . The implicit conversion will handle detaching an instance. /// /// The underlying object type. Should be a with a primary key provided via . - public class Live : IEquatable> + public class Live : IEquatable>, IHasGuidPrimaryKey where T : RealmObject, IHasGuidPrimaryKey { /// /// The primary key of the object. /// - public Guid ID { get; } + public Guid Guid { get; } + + public string ID + { + get => Guid.ToString(); + set => throw new NotImplementedException(); + } private readonly ThreadLocal threadValues; @@ -32,7 +38,7 @@ namespace osu.Game.Database public Live(T original, IRealmFactory contextFactory) { this.contextFactory = contextFactory; - ID = original.Guid; + Guid = original.Guid; var originalContext = original.Realm; @@ -43,7 +49,7 @@ namespace osu.Game.Database if (context == null || originalContext?.IsSameInstance(context) != false) return original; - return context.Find(ID); + return context.Find(Guid); }); } @@ -76,7 +82,7 @@ namespace osu.Game.Database public static implicit operator Live(T obj) => obj.WrapAsUnmanaged(); - public bool Equals(Live? other) => other != null && other.ID == ID; + public bool Equals(Live? other) => other != null && other.Guid == Guid; public override string ToString() => Get().ToString(); } From 406e640fa9b6657c1a657ccb6f5425deb2b05015 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:59:56 +0900 Subject: [PATCH 109/670] Make key binding update method support all kinds of realm object states --- osu.Game/Input/RealmKeyBindingStore.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index cd0b85cd8d..5b2d2c0dc1 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -87,14 +87,22 @@ namespace osu.Game.Input /// The modification to apply to the key binding. public void Update(IHasGuidPrimaryKey keyBinding, Action modification) { + // the incoming instance could already be a live access object. + Live realmBinding = keyBinding as Live; + using (var realm = ContextFactory.GetForWrite()) { - RealmKeyBinding realmBinding = keyBinding as RealmKeyBinding; + if (realmBinding == null) + { + // the incoming instance could be a raw realm object. + if (!(keyBinding is RealmKeyBinding rkb)) + // if neither of the above cases succeeded, retrieve a realm object for further processing. + rkb = realm.Context.Find(keyBinding.ID); - if (realmBinding?.IsManaged != true) - realmBinding = realm.Context.Find(keyBinding.ID); + realmBinding = new Live(rkb, ContextFactory); + } - modification(realmBinding); + realmBinding.PerformUpdate(modification); } KeyBindingChanged?.Invoke(); From 70689eee2bf1603c3f1755cf46cc29bbb6029788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 17:27:21 +0900 Subject: [PATCH 110/670] Perform initial lookup if original is not managed --- osu.Game/Database/Live.cs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs index 7d607a4637..f4882ae325 100644 --- a/osu.Game/Database/Live.cs +++ b/osu.Game/Database/Live.cs @@ -14,7 +14,7 @@ namespace osu.Game.Database /// /// /// To consume this as a live instance, the live object should be stored and accessed via each time. - /// To consume this as a detached instance, assign to a variable of type . The implicit conversion will handle detaching an instance. + /// To consume this as a detached instance, assign to a variable of type . The implicit conversion will handle detaching an instance. /// /// The underlying object type. Should be a with a primary key provided via . public class Live : IEquatable>, IHasGuidPrimaryKey @@ -33,24 +33,34 @@ namespace osu.Game.Database private readonly ThreadLocal threadValues; + private readonly T original; + private readonly IRealmFactory contextFactory; - public Live(T original, IRealmFactory contextFactory) + public Live(T item, IRealmFactory contextFactory) { this.contextFactory = contextFactory; - Guid = original.Guid; - var originalContext = original.Realm; + original = item; + Guid = item.Guid; - threadValues = new ThreadLocal(() => - { - var context = this.contextFactory.Get(); + threadValues = new ThreadLocal(getThreadLocalValue); - if (context == null || originalContext?.IsSameInstance(context) != false) - return original; + // the instance passed in may not be in a managed state. + // for now let's immediately retrieve a managed object on the current thread. + // in the future we may want to delay this until the first access (only populating the Guid at construction time). + if (!item.IsManaged) + original = Get(); + } - return context.Find(Guid); - }); + private T getThreadLocalValue() + { + var context = contextFactory.Get(); + + // only use the original if no context is available or the source realm is the same. + if (context == null || original.Realm?.IsSameInstance(context) == true) return original; + + return context.Find(ID); } /// From a13b6abcff74b5bd9782d8caf7dbf733b7417a59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 18:56:36 +0900 Subject: [PATCH 111/670] Remove incorrect default specification from IRealmFactory interface --- osu.Game/Database/IRealmFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index d65bcaebbe..7b126e10ba 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -7,7 +7,7 @@ namespace osu.Game.Database { public interface IRealmFactory { - public Realm Get() => Realm.GetInstance(); + Realm Get(); /// /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). From 6736db327aa51bce4345e07afe9358eb46731251 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 18:57:56 +0900 Subject: [PATCH 112/670] Remove scheduler being passed in for now --- osu.Game/Database/RealmContextFactory.cs | 10 ++-------- osu.Game/OsuGameBase.cs | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 71e9f8c4e1..5e8bda65f8 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Threading; using AutoMapper; using osu.Framework.Platform; using osu.Framework.Statistics; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Input.Bindings; @@ -21,7 +19,6 @@ namespace osu.Game.Database public class RealmContextFactory : IRealmFactory { private readonly Storage storage; - private readonly Scheduler scheduler; private const string database_name = @"client"; @@ -37,10 +34,10 @@ namespace osu.Game.Database private Transaction currentWriteTransaction; - public RealmContextFactory(Storage storage, Scheduler scheduler) + public RealmContextFactory(Storage storage) { this.storage = storage; - this.scheduler = scheduler; + recreateThreadContexts(); using (CreateContext()) @@ -98,9 +95,6 @@ namespace osu.Game.Database return new RealmWriteUsage(context, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; } - // TODO: remove if not necessary. - public void Schedule(Action action) => scheduler.Add(action); - private Realm getContextForCurrentThread() { var context = threadContexts.Value; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4b64bf2e24..513f44ad5f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -169,7 +169,7 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage, Scheduler)); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); dependencies.CacheAs(Storage); From dd50b5870ecb6ef1d09ac3871f4efb7dcd27bf74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 18:58:56 +0900 Subject: [PATCH 113/670] Move extensions methods into own class --- osu.Game/Database/RealmContextFactory.cs | 53 --------------------- osu.Game/Database/RealmExtensions.cs | 60 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 53 deletions(-) create mode 100644 osu.Game/Database/RealmExtensions.cs diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 5e8bda65f8..f78dd65fcc 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,16 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading; -using AutoMapper; using osu.Framework.Platform; using osu.Framework.Statistics; -using osu.Game.Beatmaps; -using osu.Game.Configuration; -using osu.Game.Input.Bindings; -using osu.Game.IO; -using osu.Game.Rulesets; -using osu.Game.Scoring; -using osu.Game.Skinning; using Realms; namespace osu.Game.Database @@ -170,49 +162,4 @@ namespace osu.Game.Database } } } - - public static class RealmExtensions - { - private static readonly IMapper mapper = new MapperConfiguration(c => - { - c.ShouldMapField = fi => false; - c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; - - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - - c.CreateMap() - .ForMember(s => s.Beatmaps, d => d.MapFrom(s => s.Beatmaps)) - .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) - .MaxDepth(2); - - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - }).CreateMapper(); - - public static T Detach(this T obj) where T : RealmObject - { - if (!obj.IsManaged) - return obj; - - var detached = mapper.Map(obj); - - //typeof(RealmObject).GetField("_realm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(detached, null); - - return detached; - } - - public static Live Wrap(this T obj, IRealmFactory contextFactory) - where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, contextFactory); - - public static Live WrapAsUnmanaged(this T obj) - where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, null); - } } diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs new file mode 100644 index 0000000000..8823d75b89 --- /dev/null +++ b/osu.Game/Database/RealmExtensions.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using AutoMapper; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Input.Bindings; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Skinning; +using Realms; + +namespace osu.Game.Database +{ + public static class RealmExtensions + { + private static readonly IMapper mapper = new MapperConfiguration(c => + { + c.ShouldMapField = fi => false; + c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; + + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + + c.CreateMap() + .ForMember(s => s.Beatmaps, d => d.MapFrom(s => s.Beatmaps)) + .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) + .MaxDepth(2); + + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + }).CreateMapper(); + + public static T Detach(this T obj) where T : RealmObject + { + if (!obj.IsManaged) + return obj; + + var detached = mapper.Map(obj); + + //typeof(RealmObject).GetField("_realm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(detached, null); + + return detached; + } + + public static Live Wrap(this T obj, IRealmFactory contextFactory) + where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, contextFactory); + + public static Live WrapAsUnmanaged(this T obj) + where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, null); + } +} From d810af82eca67d14f62ad3301312489c3c58b3fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 19:28:07 +0900 Subject: [PATCH 114/670] Expose Live.Detach() method for ease of use --- osu.Game/Database/Live.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs index f4882ae325..24a2aa258b 100644 --- a/osu.Game/Database/Live.cs +++ b/osu.Game/Database/Live.cs @@ -68,6 +68,11 @@ namespace osu.Game.Database /// public T Get() => threadValues.Value; + /// + /// Retrieve a detached copy of the data. + /// + public T Detach() => Get().Detach(); + /// /// Wrap a property of this instance as its own live access object. /// @@ -88,7 +93,7 @@ namespace osu.Game.Database } public static implicit operator T?(Live? wrapper) - => wrapper?.Get().Detach(); + => wrapper?.Detach() ?? null; public static implicit operator Live(T obj) => obj.WrapAsUnmanaged(); From fc55d67c66899de1a238a95466ce069adc919231 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 19:46:51 +0900 Subject: [PATCH 115/670] Add helper method for detaching lists from realm --- osu.Game/Database/RealmExtensions.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 8823d75b89..99df125f86 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.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 AutoMapper; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -39,6 +40,16 @@ namespace osu.Game.Database c.CreateMap(); }).CreateMapper(); + public static List Detach(this List items) where T : RealmObject + { + var list = new List(); + + foreach (var obj in items) + list.Add(obj.Detach()); + + return list; + } + public static T Detach(this T obj) where T : RealmObject { if (!obj.IsManaged) From 536e7229d0cb82504a39f0a18e120da91e0b0f12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 19:47:43 +0900 Subject: [PATCH 116/670] Remove unused EF class and unnecessary interface --- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 2 +- .../Bindings/DatabasedKeyBindingContainer.cs | 2 +- osu.Game/Input/IKeyBindingStore.cs | 41 ------ osu.Game/Input/KeyBindingStore.cs | 122 ------------------ osu.Game/Input/RealmKeyBindingStore.cs | 9 +- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 2 +- .../KeyBinding/KeyBindingsSubsection.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 2 +- 9 files changed, 10 insertions(+), 174 deletions(-) delete mode 100644 osu.Game/Input/IKeyBindingStore.cs delete mode 100644 osu.Game/Input/KeyBindingStore.cs diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index eddaf36f92..bcad8f2d3c 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual typeof(FileStore), typeof(ScoreManager), typeof(BeatmapManager), - typeof(IKeyBindingStore), + typeof(RealmKeyBindingStore), typeof(SettingsStore), typeof(RulesetConfigCache), typeof(OsuColour), diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index ab4854bfd2..62c09440d5 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Input.Bindings private readonly int? variant; [Resolved] - private IKeyBindingStore store { get; set; } + private RealmKeyBindingStore store { get; set; } public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); diff --git a/osu.Game/Input/IKeyBindingStore.cs b/osu.Game/Input/IKeyBindingStore.cs deleted file mode 100644 index ef1d043c33..0000000000 --- a/osu.Game/Input/IKeyBindingStore.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using osu.Framework.Input.Bindings; -using osu.Game.Database; -using osu.Game.Input.Bindings; - -namespace osu.Game.Input -{ - public interface IKeyBindingStore - { - event Action KeyBindingChanged; - - void Register(KeyBindingContainer container); - - /// - /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. - /// - /// The action to lookup. - /// A set of display strings for all the user's key configuration for the action. - IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction); - - /// - /// Retrieve s for a specified ruleset/variant content. - /// - /// The ruleset's internal ID. - /// An optional variant. - /// - List Query(int? rulesetId = null, int? variant = null); - - /// - /// Retrieve s for the specified action. - /// - /// The action to lookup. - List Query(T action) where T : Enum; - - void Update(IHasGuidPrimaryKey keyBinding, Action modification); - } -} diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs deleted file mode 100644 index c55d62b7d6..0000000000 --- a/osu.Game/Input/KeyBindingStore.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Input.Bindings; -using osu.Framework.Platform; -using osu.Game.Database; -using osu.Game.Input.Bindings; -using osu.Game.Rulesets; - -namespace osu.Game.Input -{ - public class KeyBindingStore : DatabaseBackedStore, IKeyBindingStore - { - public event Action KeyBindingChanged; - - public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) - : base(contextFactory, storage) - { - using (ContextFactory.GetForWrite()) - { - foreach (var info in rulesets.AvailableRulesets) - { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); - } - } - } - - public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings); - - /// - /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. - /// - /// The action to lookup. - /// A set of display strings for all the user's key configuration for the action. - public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) - { - foreach (var action in query().Where(b => (GlobalAction)b.Action == globalAction)) - { - string str = action.KeyCombination.ReadableString(); - - // even if found, the readable string may be empty for an unbound action. - if (str.Length > 0) - yield return str; - } - } - - public List Query(int? rulesetId = null, int? variant = null) - => query(rulesetId, variant).OfType().ToList(); - - public List Query(T action) where T : Enum - { - int lookup = (int)(object)action; - return query(null, null).Where(rkb => (int)rkb.Action == lookup).OfType().ToList(); - } - - public void Update(IHasGuidPrimaryKey keyBinding, Action modification) - { - throw new NotImplementedException(); - } - - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) - { - using (var usage = ContextFactory.GetForWrite()) - { - // compare counts in database vs defaults - foreach (var group in defaults.GroupBy(k => k.Action)) - { - int count = query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key); - int aimCount = group.Count(); - - if (aimCount <= count) - continue; - - foreach (var insertable in group.Skip(count).Take(aimCount - count)) - { - // insert any defaults which are missing. - usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding - { - KeyCombination = insertable.KeyCombination, - Action = insertable.Action, - RulesetID = rulesetId, - Variant = variant - }); - - // required to ensure stable insert order (https://github.com/dotnet/efcore/issues/11686) - usage.Context.SaveChanges(); - } - } - } - } - - /// - /// Retrieve s for a specified ruleset/variant content. - /// - /// The ruleset's internal ID. - /// An optional variant. - /// - private List query(int? rulesetId = null, int? variant = null) => - ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); - - public void Update(DatabasedKeyBinding keyBinding) - { - using (ContextFactory.GetForWrite()) - { - var dbKeyBinding = keyBinding; - Refresh(ref dbKeyBinding); - - if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) - return; - - dbKeyBinding.KeyCombination = keyBinding.KeyCombination; - } - - KeyBindingChanged?.Invoke(); - } - } -} diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 5b2d2c0dc1..fccd216e4d 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets; namespace osu.Game.Input { - public class RealmKeyBindingStore : RealmBackedStore, IKeyBindingStore + public class RealmKeyBindingStore : RealmBackedStore { /// /// Fired whenever any key binding change occurs, across all rulesets and types. @@ -63,8 +63,7 @@ namespace osu.Game.Input /// An optional ruleset ID. If null, global bindings are returned. /// An optional ruleset variant. If null, the no-variant bindings are returned. /// A list of all key bindings found for the query, detached from the database. - public List Query(int? rulesetId = null, int? variant = null) - => query(rulesetId, variant).ToList().Select(r => r.Detach()).ToList(); + public List Query(int? rulesetId = null, int? variant = null) => query(rulesetId, variant).ToList(); /// /// Retrieve all key bindings for the provided action type. @@ -72,12 +71,12 @@ namespace osu.Game.Input /// The action to lookup. /// The enum type of the action. /// A list of all key bindings found for the query, detached from the database. - public List Query(T action) + public List Query(T action) where T : Enum { int lookup = (int)(object)action; - return query(null, null).Where(rkb => rkb.Action == lookup).ToList().Select(r => r.Detach()).ToList(); + return query(null, null).Where(rkb => rkb.Action == lookup).ToList(); } /// diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 513f44ad5f..07918748df 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -73,7 +73,7 @@ namespace osu.Game protected FileStore FileStore; - protected IKeyBindingStore KeyBindingStore; + protected RealmKeyBindingStore KeyBindingStore; protected SettingsStore SettingsStore; diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 87d51e5268..0a065c9dbc 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.KeyBinding } [Resolved] - private IKeyBindingStore store { get; set; } + private RealmKeyBindingStore store { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index b5d6bc98c3..fbd9a17e2b 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.KeyBinding } [BackgroundDependencyLoader] - private void load(IKeyBindingStore store) + private void load(RealmKeyBindingStore store) { var bindings = store.Query(Ruleset?.ID, variant); diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 747f5e9bd0..69e4f734ad 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private IKeyBindingStore keyBindings { get; set; } + private RealmKeyBindingStore keyBindings { get; set; } protected ToolbarButton() : base(HoverSampleSet.Loud) From 8f9b19a76e861f871a653afda37713dc4a6a200c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 19:47:51 +0900 Subject: [PATCH 117/670] Detach at point of usage, rather than point of retrieval --- osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs | 3 ++- osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 62c09440d5..48cab674ca 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; +using osu.Game.Database; using osu.Game.Rulesets; namespace osu.Game.Input.Bindings @@ -63,7 +64,7 @@ namespace osu.Game.Input.Bindings // fallback to defaults instead. KeyBindings = DefaultKeyBindings; else - KeyBindings = store.Query(ruleset?.ID, variant); + KeyBindings = store.Query(ruleset?.ID, variant).Detach(); } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index fbd9a17e2b..bdcbf02ee6 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -6,12 +6,13 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osuTK; -using osu.Game.Graphics; namespace osu.Game.Overlays.KeyBinding { @@ -34,14 +35,14 @@ namespace osu.Game.Overlays.KeyBinding [BackgroundDependencyLoader] private void load(RealmKeyBindingStore store) { - var bindings = store.Query(Ruleset?.ID, variant); + var bindings = store.Query(Ruleset?.ID, variant).Detach(); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { int intKey = (int)defaultGroup.Key; // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => ((int)b.Action).Equals(intKey))) + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey))) { AllowMainMouseButtons = Ruleset != null, Defaults = defaultGroup.Select(d => d.KeyCombination) From 0789621b857b30ccc6aee8d5dc151eca2c367451 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 13:51:37 +0900 Subject: [PATCH 118/670] Elaborate on comment mentioning migrations --- osu.Game/Database/RealmContextFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index f78dd65fcc..4325d58d07 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -34,11 +34,11 @@ namespace osu.Game.Database using (CreateContext()) { - // ensure our schema is up-to-date and migrated. + // creating a context will ensure our schema is up-to-date and migrated. } } - private void onMigration(Migration migration, ulong oldschemaversion) + private void onMigration(Migration migration, ulong lastSchemaVersion) { } From ffb42c37dfd644355e3532d5588258029d16fb63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:25:07 +0900 Subject: [PATCH 119/670] Move schema version to const --- osu.Game/Database/RealmContextFactory.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 4325d58d07..243f8c2847 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -14,6 +14,8 @@ namespace osu.Game.Database private const string database_name = @"client"; + private const int schema_version = 5; + private ThreadLocal threadContexts; private readonly object writeLock = new object(); @@ -148,8 +150,8 @@ namespace osu.Game.Database contexts.Value++; return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) { - SchemaVersion = 5, - MigrationCallback = onMigration + SchemaVersion = schema_version, + MigrationCallback = onMigration, }); } From 8cbad1dc1c318db308affd0514a506aa160a148d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:25:22 +0900 Subject: [PATCH 120/670] Add logging of opened and created contexts --- osu.Game/Database/RealmContextFactory.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 243f8c2847..e0fd44ed4a 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -48,7 +48,8 @@ namespace osu.Game.Database private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); - private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Realm", "Contexts"); + private static readonly GlobalStatistic contexts_open = GlobalStatistics.Get("Realm", "Contexts (Open)"); + private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); /// /// Get a context for the current thread for read-only usage. @@ -92,11 +93,16 @@ namespace osu.Game.Database private Realm getContextForCurrentThread() { var context = threadContexts.Value; + if (context?.IsClosed != false) threadContexts.Value = context = CreateContext(); + contexts_open.Value = threadContexts.Values.Count; + if (!refreshCompleted.Value) { + // to keep things simple, realm refreshes are currently performed per thread context at the point of retrieval. + // in the future this should likely be run as part of the update loop for the main (update thread) context. context.Refresh(); refreshCompleted.Value = true; } @@ -147,7 +153,8 @@ namespace osu.Game.Database protected virtual Realm CreateContext() { - contexts.Value++; + contexts_created.Value++; + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) { SchemaVersion = schema_version, From 0dca9c8c464eae0440c4426401d76e52897e8afa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:36:35 +0900 Subject: [PATCH 121/670] Tidy up RealmContextFactory; remove delete/dispose method which wouldn't work due to threading --- osu.Game/Database/RealmContextFactory.cs | 52 +++++++++--------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index e0fd44ed4a..e11379869a 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; using Realms; @@ -16,8 +17,11 @@ namespace osu.Game.Database private const int schema_version = 5; - private ThreadLocal threadContexts; + private readonly ThreadLocal threadContexts; + /// + /// Lock object which is held for the duration of a write operation (via ). + /// private readonly object writeLock = new object(); private ThreadLocal refreshCompleted = new ThreadLocal(); @@ -32,10 +36,11 @@ namespace osu.Game.Database { this.storage = storage; - recreateThreadContexts(); + threadContexts = new ThreadLocal(createContext, true); - using (CreateContext()) + using (var realm = Get()) { + Logger.Log($"Opened realm {database_name} at version {realm.Config.SchemaVersion}"); // creating a context will ensure our schema is up-to-date and migrated. } } @@ -95,7 +100,7 @@ namespace osu.Game.Database var context = threadContexts.Value; if (context?.IsClosed != false) - threadContexts.Value = context = CreateContext(); + threadContexts.Value = context = createContext(); contexts_open.Value = threadContexts.Values.Count; @@ -110,6 +115,17 @@ namespace osu.Game.Database return context; } + private Realm createContext() + { + contexts_created.Value++; + + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + { + SchemaVersion = schema_version, + MigrationCallback = onMigration, + }); + } + private void usageCompleted(RealmWriteUsage usage) { int usages = Interlocked.Decrement(ref currentWriteUsages); @@ -142,33 +158,5 @@ namespace osu.Game.Database Monitor.Exit(writeLock); } } - - private void recreateThreadContexts() - { - // Contexts for other threads are not disposed as they may be in use elsewhere. Instead, fresh contexts are exposed - // for other threads to use, and we rely on the finalizer inside OsuDbContext to handle their previous contexts - threadContexts?.Value.Dispose(); - threadContexts = new ThreadLocal(CreateContext, true); - } - - protected virtual Realm CreateContext() - { - contexts_created.Value++; - - return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) - { - SchemaVersion = schema_version, - MigrationCallback = onMigration, - }); - } - - public void ResetDatabase() - { - lock (writeLock) - { - recreateThreadContexts(); - storage.DeleteDatabase(database_name); - } - } } } From 2e4c3c8e3941a15eaa2d7a3703d2ebe1c9b3fdd6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:42:43 +0900 Subject: [PATCH 122/670] Avoid closing initial context after migrations (unnecessary) --- osu.Game/Database/RealmContextFactory.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index e11379869a..fa0fecc90c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -38,11 +38,9 @@ namespace osu.Game.Database threadContexts = new ThreadLocal(createContext, true); - using (var realm = Get()) - { - Logger.Log($"Opened realm {database_name} at version {realm.Config.SchemaVersion}"); - // creating a context will ensure our schema is up-to-date and migrated. - } + // creating a context will ensure our schema is up-to-date and migrated. + var realm = Get(); + Logger.Log($"Opened realm \"{realm.Config.DatabasePath}\" at version {realm.Config.SchemaVersion}"); } private void onMigration(Migration migration, ulong lastSchemaVersion) From ff16d2f490dc7836dd96728e4ffdb81b20db185c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:55:45 +0900 Subject: [PATCH 123/670] Mark classes nullable --- osu.Game/Database/RealmBackedStore.cs | 6 ++++-- osu.Game/Input/RealmKeyBindingStore.cs | 23 ++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/RealmBackedStore.cs b/osu.Game/Database/RealmBackedStore.cs index e37831d9d5..4e58ef773b 100644 --- a/osu.Game/Database/RealmBackedStore.cs +++ b/osu.Game/Database/RealmBackedStore.cs @@ -3,15 +3,17 @@ using osu.Framework.Platform; +#nullable enable + namespace osu.Game.Database { public abstract class RealmBackedStore { - protected readonly Storage Storage; + protected readonly Storage? Storage; protected readonly IRealmFactory ContextFactory; - protected RealmBackedStore(IRealmFactory contextFactory, Storage storage = null) + protected RealmBackedStore(IRealmFactory contextFactory, Storage? storage = null) { ContextFactory = contextFactory; Storage = storage; diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index fccd216e4d..95751306f3 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -10,6 +10,8 @@ using osu.Game.Database; using osu.Game.Input.Bindings; using osu.Game.Rulesets; +#nullable enable + namespace osu.Game.Input { public class RealmKeyBindingStore : RealmBackedStore @@ -17,19 +19,22 @@ namespace osu.Game.Input /// /// Fired whenever any key binding change occurs, across all rulesets and types. /// - public event Action KeyBindingChanged; + public event Action? KeyBindingChanged; - public RealmKeyBindingStore(RealmContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) + public RealmKeyBindingStore(RealmContextFactory contextFactory, RulesetStore? rulesets, Storage? storage = null) : base(contextFactory, storage) { - // populate defaults from rulesets. - using (ContextFactory.GetForWrite()) + if (rulesets != null) { - foreach (RulesetInfo info in rulesets.AvailableRulesets) + // populate defaults from rulesets. + using (ContextFactory.GetForWrite()) { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + foreach (RulesetInfo info in rulesets.AvailableRulesets) + { + var ruleset = info.CreateInstance(); + foreach (var variant in ruleset.AvailableVariants) + insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + } } } } @@ -87,7 +92,7 @@ namespace osu.Game.Input public void Update(IHasGuidPrimaryKey keyBinding, Action modification) { // the incoming instance could already be a live access object. - Live realmBinding = keyBinding as Live; + Live? realmBinding = keyBinding as Live; using (var realm = ContextFactory.GetForWrite()) { From a6997a6fc67d44754c59d6c71d112baddefe81a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:59:48 +0900 Subject: [PATCH 124/670] Move ruleset key binding registration to an explicit method rather than the constructor --- osu.Game/Input/RealmKeyBindingStore.cs | 30 ++++++++++++++------------ osu.Game/OsuGameBase.cs | 6 +++++- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 95751306f3..8b962e2e9a 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -21,22 +21,9 @@ namespace osu.Game.Input /// public event Action? KeyBindingChanged; - public RealmKeyBindingStore(RealmContextFactory contextFactory, RulesetStore? rulesets, Storage? storage = null) + public RealmKeyBindingStore(RealmContextFactory contextFactory, Storage? storage = null) : base(contextFactory, storage) { - if (rulesets != null) - { - // populate defaults from rulesets. - using (ContextFactory.GetForWrite()) - { - foreach (RulesetInfo info in rulesets.AvailableRulesets) - { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); - } - } - } } /// @@ -62,6 +49,21 @@ namespace osu.Game.Input /// The container to populate defaults from. public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings); + /// + /// Register a ruleset, adding default bindings for each of its variants. + /// + /// The ruleset to populate defaults from. + public void Register(RulesetInfo ruleset) + { + var instance = ruleset.CreateInstance(); + + using (ContextFactory.GetForWrite()) + { + foreach (var variant in instance.AvailableVariants) + insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); + } + } + /// /// Retrieve all key bindings for the provided specification. /// diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 07918748df..65eca8255e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -267,7 +267,7 @@ namespace osu.Game migrateDataToRealm(); - dependencies.CacheAs(KeyBindingStore = new RealmKeyBindingStore(realmFactory, RulesetStore)); + dependencies.CacheAs(KeyBindingStore = new RealmKeyBindingStore(realmFactory)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); @@ -310,6 +310,10 @@ namespace osu.Game base.Content.Add(CreateScalingContainer().WithChild(MenuCursorContainer)); KeyBindingStore.Register(globalBindings); + + foreach (var r in RulesetStore.AvailableRulesets) + KeyBindingStore.Register(r); + dependencies.Cache(globalBindings); PreviewTrackManager previewTrackManager; From 9a5410e5d235431cc8f2394cd1d1567f9a38a676 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 15:19:50 +0900 Subject: [PATCH 125/670] Add basic test coverage --- .../Database/TestRealmKeyBindingStore.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 osu.Game.Tests/Database/TestRealmKeyBindingStore.cs diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs new file mode 100644 index 0000000000..d8eb3a9906 --- /dev/null +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Input; +using osu.Game.Input.Bindings; +using osuTK.Input; + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public class TestRealmKeyBindingStore + { + private NativeStorage storage; + + private RealmKeyBindingStore keyBindingStore; + + private RealmContextFactory realmContextFactory; + + [SetUp] + public void SetUp() + { + var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + + storage = new NativeStorage(directory.FullName); + + realmContextFactory = new RealmContextFactory(storage); + keyBindingStore = new RealmKeyBindingStore(realmContextFactory); + } + + [Test] + public void TestDefaultsPopulationAndQuery() + { + Assert.That(keyBindingStore.Query().Count, Is.EqualTo(0)); + + KeyBindingContainer testContainer = new TestKeyBindingContainer(); + + keyBindingStore.Register(testContainer); + + Assert.That(keyBindingStore.Query().Count, Is.EqualTo(3)); + + Assert.That(keyBindingStore.Query(GlobalAction.Back).Count, Is.EqualTo(1)); + Assert.That(keyBindingStore.Query(GlobalAction.Select).Count, Is.EqualTo(2)); + } + + [Test] + public void TestUpdateViaQueriedReference() + { + KeyBindingContainer testContainer = new TestKeyBindingContainer(); + + keyBindingStore.Register(testContainer); + + var backBinding = keyBindingStore.Query(GlobalAction.Back).Single(); + + Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); + + keyBindingStore.Update(backBinding, binding => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); + + Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); + + // check still correct after re-query. + backBinding = keyBindingStore.Query(GlobalAction.Back).Single(); + Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); + } + + [TearDown] + public void TearDown() + { + storage.DeleteDirectory(string.Empty); + } + + public class TestKeyBindingContainer : KeyBindingContainer + { + public override IEnumerable DefaultKeyBindings => + new[] + { + new KeyBinding(InputKey.Escape, GlobalAction.Back), + new KeyBinding(InputKey.Enter, GlobalAction.Select), + new KeyBinding(InputKey.Space, GlobalAction.Select), + }; + } + } +} From 7769d95e7b62a11047a053b7bdeee8cc84e1ecfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 15:48:26 +0900 Subject: [PATCH 126/670] Add xmldoc for extension methods --- osu.Game/Database/RealmExtensions.cs | 53 +++++++++++++++++++++------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 99df125f86..e7dd335ea3 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.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 System.Collections.Generic; using AutoMapper; using osu.Game.Beatmaps; @@ -40,6 +41,12 @@ namespace osu.Game.Database c.CreateMap(); }).CreateMapper(); + /// + /// Create a detached copy of the each item in the list. + /// + /// A list of managed s to detach. + /// The type of object. + /// A list containing non-managed copies of provided items. public static List Detach(this List items) where T : RealmObject { var list = new List(); @@ -50,22 +57,44 @@ namespace osu.Game.Database return list; } - public static T Detach(this T obj) where T : RealmObject + /// + /// Create a detached copy of the each item in the list. + /// + /// The managed to detach. + /// The type of object. + /// A non-managed copy of provided item. Will return the provided item if already detached. + public static T Detach(this T item) where T : RealmObject { - if (!obj.IsManaged) - return obj; + if (!item.IsManaged) + return item; - var detached = mapper.Map(obj); - - //typeof(RealmObject).GetField("_realm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(detached, null); - - return detached; + return mapper.Map(item); } - public static Live Wrap(this T obj, IRealmFactory contextFactory) - where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, contextFactory); + /// + /// Wrap a managed instance of a realm object in a . + /// + /// The item to wrap. + /// A factory to retrieve realm contexts from. + /// The type of object. + /// A wrapped instance of the provided item. + public static Live Wrap(this T item, IRealmFactory contextFactory) + where T : RealmObject, IHasGuidPrimaryKey => new Live(item, contextFactory); - public static Live WrapAsUnmanaged(this T obj) - where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, null); + /// + /// Wrap an unmanaged instance of a realm object in a . + /// + /// The item to wrap. + /// The type of object. + /// A wrapped instance of the provided item. + /// Throws if the provided item is managed. + public static Live WrapAsUnmanaged(this T item) + where T : RealmObject, IHasGuidPrimaryKey + { + if (item.IsManaged) + throw new ArgumentException("Provided item must not be managed", nameof(item)); + + return new Live(item, null); + } } } From f0a9688baa5be4962c8908fd5adb46b8f1ec78d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 15:50:09 +0900 Subject: [PATCH 127/670] Remove unnecessary mapped type --- osu.Game/Database/RealmExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index e7dd335ea3..7b63395192 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -32,7 +32,6 @@ namespace osu.Game.Database .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) .MaxDepth(2); - c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); From 46a1d99c742c58502da7a1ffbbee7eef94f3c2e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 17:01:16 +0900 Subject: [PATCH 128/670] Allow detach to be run against an IQueryable directly --- osu.Game/Database/RealmExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 7b63395192..b25299bf23 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -41,12 +41,12 @@ namespace osu.Game.Database }).CreateMapper(); /// - /// Create a detached copy of the each item in the list. + /// Create a detached copy of the each item in the collection. /// /// A list of managed s to detach. /// The type of object. /// A list containing non-managed copies of provided items. - public static List Detach(this List items) where T : RealmObject + public static List Detach(this IEnumerable items) where T : RealmObject { var list = new List(); From 765d9cfae1c38e09fba32e3ab667f6f40651d63f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 17:01:40 +0900 Subject: [PATCH 129/670] Use direct access for query pattern --- .../Database/TestRealmKeyBindingStore.cs | 2 -- .../Bindings/DatabasedKeyBindingContainer.cs | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index d8eb3a9906..3402b95739 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -6,13 +6,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using NUnit.Framework; -using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; -using osuTK.Input; namespace osu.Game.Tests.Database { diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 48cab674ca..03da76b330 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Database; @@ -24,6 +25,9 @@ namespace osu.Game.Input.Bindings [Resolved] private RealmKeyBindingStore store { get; set; } + [Resolved] + private RealmContextFactory realmFactory { get; set; } + public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); /// @@ -64,7 +68,16 @@ namespace osu.Game.Input.Bindings // fallback to defaults instead. KeyBindings = DefaultKeyBindings; else - KeyBindings = store.Query(ruleset?.ID, variant).Detach(); + { + var rulesetId = ruleset?.ID; + + // #1 + KeyBindings = store.Query(rulesetId, variant).Detach(); + + // #2 (Clearly shows lifetime of realm context access) + using (var realm = realmFactory.Get()) + KeyBindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + } } } } From 192e58e0c6c447cb0ae93c641e1e19396c7a22a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 16:53:04 +0900 Subject: [PATCH 130/670] Update all read queries to use direct realm subscriptions/queries --- .../Database/TestRealmKeyBindingStore.cs | 14 +++-- osu.Game/Database/RealmBackedStore.cs | 6 +- osu.Game/Database/RealmContextFactory.cs | 2 +- .../Bindings/DatabasedKeyBindingContainer.cs | 55 +++++++++++-------- osu.Game/Input/RealmKeyBindingStore.cs | 36 +++--------- .../KeyBinding/KeyBindingsSubsection.cs | 25 +++++---- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 52 ++++++++++-------- 7 files changed, 97 insertions(+), 93 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 3402b95739..58633e2f03 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -37,18 +37,20 @@ namespace osu.Game.Tests.Database [Test] public void TestDefaultsPopulationAndQuery() { - Assert.That(keyBindingStore.Query().Count, Is.EqualTo(0)); + Assert.That(query().Count, Is.EqualTo(0)); KeyBindingContainer testContainer = new TestKeyBindingContainer(); keyBindingStore.Register(testContainer); - Assert.That(keyBindingStore.Query().Count, Is.EqualTo(3)); + Assert.That(query().Count, Is.EqualTo(3)); - Assert.That(keyBindingStore.Query(GlobalAction.Back).Count, Is.EqualTo(1)); - Assert.That(keyBindingStore.Query(GlobalAction.Select).Count, Is.EqualTo(2)); + Assert.That(query().Where(k => k.Action == (int)GlobalAction.Back).Count, Is.EqualTo(1)); + Assert.That(query().Where(k => k.Action == (int)GlobalAction.Select).Count, Is.EqualTo(2)); } + private IQueryable query() => realmContextFactory.Get().All(); + [Test] public void TestUpdateViaQueriedReference() { @@ -56,7 +58,7 @@ namespace osu.Game.Tests.Database keyBindingStore.Register(testContainer); - var backBinding = keyBindingStore.Query(GlobalAction.Back).Single(); + var backBinding = query().Single(k => k.Action == (int)GlobalAction.Back); Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); @@ -65,7 +67,7 @@ namespace osu.Game.Tests.Database Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); // check still correct after re-query. - backBinding = keyBindingStore.Query(GlobalAction.Back).Single(); + backBinding = query().Single(k => k.Action == (int)GlobalAction.Back); Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); } diff --git a/osu.Game/Database/RealmBackedStore.cs b/osu.Game/Database/RealmBackedStore.cs index 4e58ef773b..bc67e332fe 100644 --- a/osu.Game/Database/RealmBackedStore.cs +++ b/osu.Game/Database/RealmBackedStore.cs @@ -11,11 +11,11 @@ namespace osu.Game.Database { protected readonly Storage? Storage; - protected readonly IRealmFactory ContextFactory; + protected readonly IRealmFactory RealmFactory; - protected RealmBackedStore(IRealmFactory contextFactory, Storage? storage = null) + protected RealmBackedStore(IRealmFactory realmFactory, Storage? storage = null) { - ContextFactory = contextFactory; + RealmFactory = realmFactory; Storage = storage; } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index fa0fecc90c..b6eb28aa33 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -77,7 +77,7 @@ namespace osu.Game.Database try { - context = getContextForCurrentThread(); + context = createContext(); currentWriteTransaction ??= context.BeginWrite(); } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 03da76b330..d5ae4d9bd6 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Database; using osu.Game.Rulesets; +using Realms; namespace osu.Game.Input.Bindings { @@ -22,6 +23,9 @@ namespace osu.Game.Input.Bindings private readonly int? variant; + private IDisposable realmSubscription; + private IQueryable realmKeyBindings; + [Resolved] private RealmKeyBindingStore store { get; set; } @@ -49,35 +53,42 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { + var realm = realmFactory.Get(); + + if (ruleset == null || ruleset.ID.HasValue) + { + var rulesetId = ruleset?.ID; + + realmKeyBindings = realm.All() + .Where(b => b.RulesetID == rulesetId && b.Variant == variant); + + realmSubscription = realmKeyBindings + .SubscribeForNotifications((sender, changes, error) => + { + // first subscription ignored as we are handling this in LoadComplete. + if (changes == null) + return; + + ReloadMappings(); + }); + } + base.LoadComplete(); - store.KeyBindingChanged += ReloadMappings; + } + + protected override void ReloadMappings() + { + if (realmKeyBindings != null) + KeyBindings = realmKeyBindings.Detach(); + else + base.ReloadMappings(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - if (store != null) - store.KeyBindingChanged -= ReloadMappings; - } - - protected override void ReloadMappings() - { - if (ruleset != null && !ruleset.ID.HasValue) - // if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings. - // fallback to defaults instead. - KeyBindings = DefaultKeyBindings; - else - { - var rulesetId = ruleset?.ID; - - // #1 - KeyBindings = store.Query(rulesetId, variant).Detach(); - - // #2 (Clearly shows lifetime of realm context access) - using (var realm = realmFactory.Get()) - KeyBindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); - } + realmSubscription?.Dispose(); } } } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 8b962e2e9a..0af1beefb7 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -21,8 +21,8 @@ namespace osu.Game.Input /// public event Action? KeyBindingChanged; - public RealmKeyBindingStore(RealmContextFactory contextFactory, Storage? storage = null) - : base(contextFactory, storage) + public RealmKeyBindingStore(RealmContextFactory realmFactory, Storage? storage = null) + : base(realmFactory, storage) { } @@ -57,35 +57,13 @@ namespace osu.Game.Input { var instance = ruleset.CreateInstance(); - using (ContextFactory.GetForWrite()) + using (RealmFactory.GetForWrite()) { foreach (var variant in instance.AvailableVariants) insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); } } - /// - /// Retrieve all key bindings for the provided specification. - /// - /// An optional ruleset ID. If null, global bindings are returned. - /// An optional ruleset variant. If null, the no-variant bindings are returned. - /// A list of all key bindings found for the query, detached from the database. - public List Query(int? rulesetId = null, int? variant = null) => query(rulesetId, variant).ToList(); - - /// - /// Retrieve all key bindings for the provided action type. - /// - /// The action to lookup. - /// The enum type of the action. - /// A list of all key bindings found for the query, detached from the database. - public List Query(T action) - where T : Enum - { - int lookup = (int)(object)action; - - return query(null, null).Where(rkb => rkb.Action == lookup).ToList(); - } - /// /// Update the database mapping for the provided key binding. /// @@ -96,7 +74,7 @@ namespace osu.Game.Input // the incoming instance could already be a live access object. Live? realmBinding = keyBinding as Live; - using (var realm = ContextFactory.GetForWrite()) + using (var realm = RealmFactory.GetForWrite()) { if (realmBinding == null) { @@ -105,7 +83,7 @@ namespace osu.Game.Input // if neither of the above cases succeeded, retrieve a realm object for further processing. rkb = realm.Context.Find(keyBinding.ID); - realmBinding = new Live(rkb, ContextFactory); + realmBinding = new Live(rkb, RealmFactory); } realmBinding.PerformUpdate(modification); @@ -116,7 +94,7 @@ namespace osu.Game.Input private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { - using (var usage = ContextFactory.GetForWrite()) + using (var usage = RealmFactory.GetForWrite()) { // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) @@ -149,6 +127,6 @@ namespace osu.Game.Input /// An optional ruleset ID. If null, global bindings are returned. /// An optional ruleset variant. If null, the no-variant bindings are returned. private IQueryable query(int? rulesetId = null, int? variant = null) => - ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); + RealmFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index bdcbf02ee6..b067e50d4d 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; -using osu.Game.Input; +using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osuTK; @@ -33,20 +33,25 @@ namespace osu.Game.Overlays.KeyBinding } [BackgroundDependencyLoader] - private void load(RealmKeyBindingStore store) + private void load(RealmContextFactory realmFactory) { - var bindings = store.Query(Ruleset?.ID, variant).Detach(); + var rulesetId = Ruleset?.ID; - foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) + using (var realm = realmFactory.Get()) { - int intKey = (int)defaultGroup.Key; + var bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); - // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey))) + foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { - AllowMainMouseButtons = Ruleset != null, - Defaults = defaultGroup.Select(d => d.KeyCombination) - }); + int intKey = (int)defaultGroup.Key; + + // one row per valid action. + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey))) + { + AllowMainMouseButtons = Ruleset != null, + Defaults = defaultGroup.Select(d => d.KeyCombination) + }); + } } Add(new ResetButton diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 69e4f734ad..fcb5031657 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,12 +12,12 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Input; using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; @@ -75,7 +74,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private RealmKeyBindingStore keyBindings { get; set; } + private RealmContextFactory realmFactory { get; set; } protected ToolbarButton() : base(HoverSampleSet.Loud) @@ -158,32 +157,28 @@ namespace osu.Game.Overlays.Toolbar }; } - private readonly Cached tooltipKeyBinding = new Cached(); + private RealmKeyBinding realmKeyBinding; - [BackgroundDependencyLoader] - private void load() + protected override void LoadComplete() { - keyBindings.KeyBindingChanged += () => tooltipKeyBinding.Invalidate(); - updateKeyBindingTooltip(); - } - - private void updateKeyBindingTooltip() - { - if (tooltipKeyBinding.IsValid) - return; - - keyBindingTooltip.Text = string.Empty; + base.LoadComplete(); if (Hotkey != null) { - KeyCombination? binding = keyBindings.Query(Hotkey.Value).FirstOrDefault()?.KeyCombination; - var keyBindingString = binding?.ReadableString(); + var realm = realmFactory.Get(); + realmKeyBinding = realm.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.Action == (int)Hotkey.Value); - if (!string.IsNullOrEmpty(keyBindingString)) - keyBindingTooltip.Text = $" ({keyBindingString})"; + if (realmKeyBinding != null) + { + realmKeyBinding.PropertyChanged += (sender, args) => + { + if (args.PropertyName == nameof(realmKeyBinding.KeyCombination)) + updateKeyBindingTooltip(); + }; + } + + updateKeyBindingTooltip(); } - - tooltipKeyBinding.Validate(); } protected override bool OnMouseDown(MouseDownEvent e) => true; @@ -224,6 +219,19 @@ namespace osu.Game.Overlays.Toolbar public void OnReleased(GlobalAction action) { } + + private void updateKeyBindingTooltip() + { + if (realmKeyBinding != null) + { + KeyCombination? binding = ((IKeyBinding)realmKeyBinding).KeyCombination; + + var keyBindingString = binding?.ReadableString(); + + if (!string.IsNullOrEmpty(keyBindingString)) + keyBindingTooltip.Text = $" ({keyBindingString})"; + } + } } public class OpaqueBackground : Container From 78707c3b0615d7315289a3a68262fa456017b21e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 17:03:02 +0900 Subject: [PATCH 131/670] Remove unused event --- osu.Game/Input/RealmKeyBindingStore.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 0af1beefb7..b42d2688f0 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -16,11 +16,6 @@ namespace osu.Game.Input { public class RealmKeyBindingStore : RealmBackedStore { - /// - /// Fired whenever any key binding change occurs, across all rulesets and types. - /// - public event Action? KeyBindingChanged; - public RealmKeyBindingStore(RealmContextFactory realmFactory, Storage? storage = null) : base(realmFactory, storage) { @@ -88,8 +83,6 @@ namespace osu.Game.Input realmBinding.PerformUpdate(modification); } - - KeyBindingChanged?.Invoke(); } private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) From 542f535247d5d3a4c90570cbbcf0b273a6f7231d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 17:34:44 +0900 Subject: [PATCH 132/670] Pull out thread local contexts and have main realm refresh in update loop --- osu.Game/Database/IRealmFactory.cs | 12 ++- osu.Game/Database/RealmContextFactory.cs | 93 +++++++++++------------- osu.Game/OsuGameBase.cs | 1 + 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 7b126e10ba..0fffc3d7be 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -7,10 +7,18 @@ namespace osu.Game.Database { public interface IRealmFactory { - Realm Get(); + /// + /// The main realm context, bound to the update thread. + /// + public Realm Context { get; } /// - /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// Get a fresh context for read usage. + /// + Realm GetForRead(); + + /// + /// Request a context for write usage. /// This method may block if a write is already active on a different thread. /// /// A usage containing a usable context. diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index b6eb28aa33..ea3549a9cd 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading; +using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; @@ -9,16 +10,13 @@ using Realms; namespace osu.Game.Database { - public class RealmContextFactory : IRealmFactory + public class RealmContextFactory : Component, IRealmFactory { private readonly Storage storage; - private const string database_name = @"client"; private const int schema_version = 5; - private readonly ThreadLocal threadContexts; - /// /// Lock object which is held for the duration of a write operation (via ). /// @@ -32,54 +30,55 @@ namespace osu.Game.Database private Transaction currentWriteTransaction; - public RealmContextFactory(Storage storage) - { - this.storage = storage; - - threadContexts = new ThreadLocal(createContext, true); - - // creating a context will ensure our schema is up-to-date and migrated. - var realm = Get(); - Logger.Log($"Opened realm \"{realm.Config.DatabasePath}\" at version {realm.Config.SchemaVersion}"); - } - - private void onMigration(Migration migration, ulong lastSchemaVersion) - { - } - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); + private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Refreshes"); private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); private static readonly GlobalStatistic contexts_open = GlobalStatistics.Get("Realm", "Contexts (Open)"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); - /// - /// Get a context for the current thread for read-only usage. - /// If a is in progress, the existing write-safe context will be returned. - /// - public Realm Get() + private Realm context; + + public Realm Context { - reads.Value++; - return getContextForCurrentThread(); + get + { + if (context == null) + { + context = createContext(); + Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); + } + + // creating a context will ensure our schema is up-to-date and migrated. + + return context; + } + } + + public RealmContextFactory(Storage storage) + { + this.storage = storage; + } + + public Realm GetForRead() + { + reads.Value++; + return createContext(); } - /// - /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). - /// This method may block if a write is already active on a different thread. - /// - /// A usage containing a usable context. public RealmWriteUsage GetForWrite() { writes.Value++; Monitor.Enter(writeLock); - Realm context; + + Realm realm; try { - context = createContext(); + realm = createContext(); - currentWriteTransaction ??= context.BeginWrite(); + currentWriteTransaction ??= realm.BeginWrite(); } catch { @@ -90,27 +89,15 @@ namespace osu.Game.Database Interlocked.Increment(ref currentWriteUsages); - return new RealmWriteUsage(context, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; + return new RealmWriteUsage(realm, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; } - private Realm getContextForCurrentThread() + protected override void Update() { - var context = threadContexts.Value; + base.Update(); - if (context?.IsClosed != false) - threadContexts.Value = context = createContext(); - - contexts_open.Value = threadContexts.Values.Count; - - if (!refreshCompleted.Value) - { - // to keep things simple, realm refreshes are currently performed per thread context at the point of retrieval. - // in the future this should likely be run as part of the update loop for the main (update thread) context. - context.Refresh(); - refreshCompleted.Value = true; - } - - return context; + if (Context.Refresh()) + refreshes.Value++; } private Realm createContext() @@ -156,5 +143,9 @@ namespace osu.Game.Database Monitor.Exit(writeLock); } } + + private void onMigration(Migration migration, ulong lastSchemaVersion) + { + } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 65eca8255e..01f161cfd9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -170,6 +170,7 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); + AddInternal(realmFactory); dependencies.CacheAs(Storage); From 9d744d629f28f85ba8231aa51ff38de441e3fb18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 17:35:00 +0900 Subject: [PATCH 133/670] Update existing usages to use the main realm context where applicable --- osu.Game.Tests/Database/TestRealmKeyBindingStore.cs | 2 +- osu.Game/Database/Live.cs | 2 +- .../Input/Bindings/DatabasedKeyBindingContainer.cs | 6 ++---- osu.Game/Input/RealmKeyBindingStore.cs | 12 ++---------- .../Overlays/KeyBinding/KeyBindingsSubsection.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 3 +-- 6 files changed, 8 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 58633e2f03..691c55c601 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Database Assert.That(query().Where(k => k.Action == (int)GlobalAction.Select).Count, Is.EqualTo(2)); } - private IQueryable query() => realmContextFactory.Get().All(); + private IQueryable query() => realmContextFactory.Context.All(); [Test] public void TestUpdateViaQueriedReference() diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs index 24a2aa258b..49218ddd6e 100644 --- a/osu.Game/Database/Live.cs +++ b/osu.Game/Database/Live.cs @@ -55,7 +55,7 @@ namespace osu.Game.Database private T getThreadLocalValue() { - var context = contextFactory.Get(); + var context = contextFactory.Context; // only use the original if no context is available or the source realm is the same. if (context == null || original.Realm?.IsSameInstance(context) == true) return original; diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index d5ae4d9bd6..04f050f536 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -53,14 +53,12 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - var realm = realmFactory.Get(); - if (ruleset == null || ruleset.ID.HasValue) { var rulesetId = ruleset?.ID; - realmKeyBindings = realm.All() - .Where(b => b.RulesetID == rulesetId && b.Variant == variant); + realmKeyBindings = realmFactory.Context.All() + .Where(b => b.RulesetID == rulesetId && b.Variant == variant); realmSubscription = realmKeyBindings .SubscribeForNotifications((sender, changes, error) => diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index b42d2688f0..dd487af9bc 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -28,7 +28,7 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in query().Where(b => (GlobalAction)b.Action == globalAction)) + foreach (var action in RealmFactory.Context.All().Where(b => (GlobalAction)b.Action == globalAction)) { string str = ((IKeyBinding)action).KeyCombination.ReadableString(); @@ -92,7 +92,7 @@ namespace osu.Game.Input // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = query(rulesetId, variant).Count(k => k.Action == (int)group.Key); + int count = usage.Context.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.Action == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -113,13 +113,5 @@ namespace osu.Game.Input } } } - - /// - /// Retrieve live queryable s for a specified ruleset/variant content. - /// - /// An optional ruleset ID. If null, global bindings are returned. - /// An optional ruleset variant. If null, the no-variant bindings are returned. - private IQueryable query(int? rulesetId = null, int? variant = null) => - RealmFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index b067e50d4d..0f95d07da8 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.KeyBinding { var rulesetId = Ruleset?.ID; - using (var realm = realmFactory.Get()) + using (var realm = realmFactory.GetForRead()) { var bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index fcb5031657..7bb0eb894c 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -165,8 +165,7 @@ namespace osu.Game.Overlays.Toolbar if (Hotkey != null) { - var realm = realmFactory.Get(); - realmKeyBinding = realm.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.Action == (int)Hotkey.Value); + realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.Action == (int)Hotkey.Value); if (realmKeyBinding != null) { From 9086d7554270658fc70a12c0327f77c180bc05aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 17:59:47 +0900 Subject: [PATCH 134/670] Update write usages --- .../Database/TestRealmKeyBindingStore.cs | 7 +++- osu.Game/Input/RealmKeyBindingStore.cs | 26 ------------- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 39 +++++++++++++------ .../KeyBinding/KeyBindingsSubsection.cs | 2 +- 4 files changed, 34 insertions(+), 40 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 691c55c601..426593f5de 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -62,7 +62,12 @@ namespace osu.Game.Tests.Database Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); - keyBindingStore.Update(backBinding, binding => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); + var binding = backBinding; + + realmContextFactory.Context.Write(() => + { + ((IKeyBinding)binding).KeyCombination = new KeyCombination(InputKey.BackSpace); + }); Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index dd487af9bc..478f4792f8 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -59,32 +59,6 @@ namespace osu.Game.Input } } - /// - /// Update the database mapping for the provided key binding. - /// - /// The key binding to update. Can be detached from the database. - /// The modification to apply to the key binding. - public void Update(IHasGuidPrimaryKey keyBinding, Action modification) - { - // the incoming instance could already be a live access object. - Live? realmBinding = keyBinding as Live; - - using (var realm = RealmFactory.GetForWrite()) - { - if (realmBinding == null) - { - // the incoming instance could be a raw realm object. - if (!(keyBinding is RealmKeyBinding rkb)) - // if neither of the above cases succeeded, retrieve a realm object for further processing. - rkb = realm.Context.Find(keyBinding.ID); - - realmBinding = new Live(rkb, RealmFactory); - } - - realmBinding.PerformUpdate(modification); - } - } - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = RealmFactory.GetForWrite()) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 0a065c9dbc..f73d92f5c2 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.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 System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -16,7 +17,7 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Input; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -26,7 +27,7 @@ namespace osu.Game.Overlays.KeyBinding public class KeyBindingRow : Container, IFilterable { private readonly object action; - private readonly IEnumerable bindings; + private readonly IEnumerable bindings; private const float transition_time = 150; @@ -52,9 +53,9 @@ namespace osu.Game.Overlays.KeyBinding private FillFlowContainer cancelAndClearButtons; private FillFlowContainer buttons; - public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text); + public IEnumerable FilterTerms => bindings.Select(b => ((IKeyBinding)b).KeyCombination.ReadableString()).Prepend((string)text.Text); - public KeyBindingRow(object action, IEnumerable bindings) + public KeyBindingRow(object action, List bindings) { this.action = action; this.bindings = bindings; @@ -67,7 +68,7 @@ namespace osu.Game.Overlays.KeyBinding } [Resolved] - private RealmKeyBindingStore store { get; set; } + private RealmContextFactory realmFactory { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -127,7 +128,12 @@ namespace osu.Game.Overlays.KeyBinding { var button = buttons[i++]; button.UpdateKeyCombination(d); - store.Update((IHasGuidPrimaryKey)button.KeyBinding, k => k.KeyCombination = button.KeyBinding.KeyCombination); + + using (var write = realmFactory.GetForWrite()) + { + var binding = write.Context.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + binding.KeyCombination = button.KeyBinding.KeyCombination; + } } } @@ -286,7 +292,11 @@ namespace osu.Game.Overlays.KeyBinding { if (bindTarget != null) { - store.Update((IHasGuidPrimaryKey)bindTarget.KeyBinding, k => k.KeyCombination = bindTarget.KeyBinding.KeyCombination); + using (var write = realmFactory.GetForWrite()) + { + var binding = write.Context.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID); + binding.KeyCombination = bindTarget.KeyBinding.KeyCombination; + } bindTarget.IsBinding = false; Schedule(() => @@ -360,7 +370,7 @@ namespace osu.Game.Overlays.KeyBinding public class KeyButton : Container { - public readonly IKeyBinding KeyBinding; + public readonly RealmKeyBinding KeyBinding; private readonly Box box; public readonly OsuSpriteText Text; @@ -382,8 +392,11 @@ namespace osu.Game.Overlays.KeyBinding } } - public KeyButton(IKeyBinding keyBinding) + public KeyButton(RealmKeyBinding keyBinding) { + if (keyBinding.IsManaged) + throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(keyBinding)); + KeyBinding = keyBinding; Margin = new MarginPadding(padding); @@ -416,7 +429,7 @@ namespace osu.Game.Overlays.KeyBinding Margin = new MarginPadding(5), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = keyBinding.KeyCombination.ReadableString(), + Text = ((IKeyBinding)keyBinding).KeyCombination.ReadableString(), }, }; } @@ -455,8 +468,10 @@ namespace osu.Game.Overlays.KeyBinding public void UpdateKeyCombination(KeyCombination newCombination) { - KeyBinding.KeyCombination = newCombination; - Text.Text = KeyBinding.KeyCombination.ReadableString(); + var keyBinding = (IKeyBinding)KeyBinding; + + keyBinding.KeyCombination = newCombination; + Text.Text = keyBinding.KeyCombination.ReadableString(); } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index 0f95d07da8..a23f22cf57 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.KeyBinding int intKey = (int)defaultGroup.Key; // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey))) + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey)).ToList()) { AllowMainMouseButtons = Ruleset != null, Defaults = defaultGroup.Select(d => d.KeyCombination) From fcb4a53f37250cd4a7f9ac7b900747d4b7481f4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 18:07:35 +0900 Subject: [PATCH 135/670] Rename realm persisted properties to avoid casting necessity --- .../Database/TestRealmKeyBindingStore.cs | 16 +++++++-------- osu.Game/Input/Bindings/RealmKeyBinding.cs | 20 ++++++++++--------- osu.Game/Input/RealmKeyBindingStore.cs | 10 +++++----- osu.Game/OsuGameBase.cs | 4 ++-- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 14 ++++++------- .../KeyBinding/KeyBindingsSubsection.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 6 +++--- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 426593f5de..1a7e0d67c5 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -45,8 +45,8 @@ namespace osu.Game.Tests.Database Assert.That(query().Count, Is.EqualTo(3)); - Assert.That(query().Where(k => k.Action == (int)GlobalAction.Back).Count, Is.EqualTo(1)); - Assert.That(query().Where(k => k.Action == (int)GlobalAction.Select).Count, Is.EqualTo(2)); + Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Back).Count, Is.EqualTo(1)); + Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2)); } private IQueryable query() => realmContextFactory.Context.All(); @@ -58,22 +58,22 @@ namespace osu.Game.Tests.Database keyBindingStore.Register(testContainer); - var backBinding = query().Single(k => k.Action == (int)GlobalAction.Back); + var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back); - Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); + Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); var binding = backBinding; realmContextFactory.Context.Write(() => { - ((IKeyBinding)binding).KeyCombination = new KeyCombination(InputKey.BackSpace); + binding.KeyCombination = new KeyCombination(InputKey.BackSpace); }); - Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); + Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); // check still correct after re-query. - backBinding = query().Single(k => k.Action == (int)GlobalAction.Back); - Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); + backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back); + Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); } [TearDown] diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 1e690ddbab..ecffc1fd62 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -7,7 +7,7 @@ using Realms; namespace osu.Game.Input.Bindings { - [MapTo("KeyBinding")] + [MapTo(nameof(KeyBinding))] public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { [PrimaryKey] @@ -17,20 +17,22 @@ namespace osu.Game.Input.Bindings public int? Variant { get; set; } - KeyCombination IKeyBinding.KeyCombination + public KeyCombination KeyCombination { - get => KeyCombination; - set => KeyCombination = value.ToString(); + get => KeyCombinationString; + set => KeyCombinationString = value.ToString(); } - object IKeyBinding.Action + public object Action { - get => Action; - set => Action = (int)value; + get => ActionInt; + set => ActionInt = (int)value; } - public int Action { get; set; } + [MapTo(nameof(Action))] + public int ActionInt { get; set; } - public string KeyCombination { get; set; } + [MapTo(nameof(KeyCombination))] + public string KeyCombinationString { get; set; } } } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 478f4792f8..8e43811c36 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -28,9 +28,9 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in RealmFactory.Context.All().Where(b => (GlobalAction)b.Action == globalAction)) + foreach (var action in RealmFactory.Context.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) { - string str = ((IKeyBinding)action).KeyCombination.ReadableString(); + string str = action.KeyCombination.ReadableString(); // even if found, the readable string may be empty for an unbound action. if (str.Length > 0) @@ -66,7 +66,7 @@ namespace osu.Game.Input // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = usage.Context.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.Action == (int)group.Key); + int count = usage.Context.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -78,8 +78,8 @@ namespace osu.Game.Input usage.Context.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), - KeyCombination = insertable.KeyCombination.ToString(), - Action = (int)insertable.Action, + KeyCombinationString = insertable.KeyCombination.ToString(), + ActionInt = (int)insertable.Action, RulesetID = rulesetId, Variant = variant }); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 01f161cfd9..192867b8c8 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -342,8 +342,8 @@ namespace osu.Game realm.Context.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), - KeyCombination = dkb.KeyCombination.ToString(), - Action = (int)dkb.Action, + KeyCombinationString = dkb.KeyCombination.ToString(), + ActionInt = (int)dkb.Action, RulesetID = dkb.RulesetID, Variant = dkb.Variant }); diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index f73d92f5c2..34cdfd18fa 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.KeyBinding private FillFlowContainer cancelAndClearButtons; private FillFlowContainer buttons; - public IEnumerable FilterTerms => bindings.Select(b => ((IKeyBinding)b).KeyCombination.ReadableString()).Prepend((string)text.Text); + public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text); public KeyBindingRow(object action, List bindings) { @@ -132,7 +132,7 @@ namespace osu.Game.Overlays.KeyBinding using (var write = realmFactory.GetForWrite()) { var binding = write.Context.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); - binding.KeyCombination = button.KeyBinding.KeyCombination; + binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; } } } @@ -295,7 +295,7 @@ namespace osu.Game.Overlays.KeyBinding using (var write = realmFactory.GetForWrite()) { var binding = write.Context.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID); - binding.KeyCombination = bindTarget.KeyBinding.KeyCombination; + binding.KeyCombinationString = bindTarget.KeyBinding.KeyCombinationString; } bindTarget.IsBinding = false; @@ -429,7 +429,7 @@ namespace osu.Game.Overlays.KeyBinding Margin = new MarginPadding(5), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = ((IKeyBinding)keyBinding).KeyCombination.ReadableString(), + Text = keyBinding.KeyCombination.ReadableString(), }, }; } @@ -468,10 +468,8 @@ namespace osu.Game.Overlays.KeyBinding public void UpdateKeyCombination(KeyCombination newCombination) { - var keyBinding = (IKeyBinding)KeyBinding; - - keyBinding.KeyCombination = newCombination; - Text.Text = keyBinding.KeyCombination.ReadableString(); + KeyBinding.KeyCombination = newCombination; + Text.Text = KeyBinding.KeyCombination.ReadableString(); } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index a23f22cf57..fae42f5492 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.KeyBinding int intKey = (int)defaultGroup.Key; // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey)).ToList()) + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList()) { AllowMainMouseButtons = Ruleset != null, Defaults = defaultGroup.Select(d => d.KeyCombination) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 7bb0eb894c..305a17126a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -165,13 +165,13 @@ namespace osu.Game.Overlays.Toolbar if (Hotkey != null) { - realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.Action == (int)Hotkey.Value); + realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); if (realmKeyBinding != null) { realmKeyBinding.PropertyChanged += (sender, args) => { - if (args.PropertyName == nameof(realmKeyBinding.KeyCombination)) + if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) updateKeyBindingTooltip(); }; } @@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Toolbar { if (realmKeyBinding != null) { - KeyCombination? binding = ((IKeyBinding)realmKeyBinding).KeyCombination; + KeyCombination? binding = realmKeyBinding.KeyCombination; var keyBindingString = binding?.ReadableString(); From 5fa3a22f28ac40a857b1a21b49c13766ee8e752d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 18:13:30 +0900 Subject: [PATCH 136/670] Remove unused RealmBackedStore base class --- osu.Game/Database/RealmBackedStore.cs | 29 -------------------------- osu.Game/Input/RealmKeyBindingStore.cs | 15 ++++++------- 2 files changed, 8 insertions(+), 36 deletions(-) delete mode 100644 osu.Game/Database/RealmBackedStore.cs diff --git a/osu.Game/Database/RealmBackedStore.cs b/osu.Game/Database/RealmBackedStore.cs deleted file mode 100644 index bc67e332fe..0000000000 --- a/osu.Game/Database/RealmBackedStore.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Platform; - -#nullable enable - -namespace osu.Game.Database -{ - public abstract class RealmBackedStore - { - protected readonly Storage? Storage; - - protected readonly IRealmFactory RealmFactory; - - protected RealmBackedStore(IRealmFactory realmFactory, Storage? storage = null) - { - RealmFactory = realmFactory; - Storage = storage; - } - - /// - /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary. - /// - public virtual void Cleanup() - { - } - } -} diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 8e43811c36..756df6434e 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Input.Bindings; -using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Input.Bindings; using osu.Game.Rulesets; @@ -14,11 +13,13 @@ using osu.Game.Rulesets; namespace osu.Game.Input { - public class RealmKeyBindingStore : RealmBackedStore + public class RealmKeyBindingStore { - public RealmKeyBindingStore(RealmContextFactory realmFactory, Storage? storage = null) - : base(realmFactory, storage) + private readonly RealmContextFactory realmFactory; + + public RealmKeyBindingStore(RealmContextFactory realmFactory) { + this.realmFactory = realmFactory; } /// @@ -28,7 +29,7 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in RealmFactory.Context.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) + foreach (var action in realmFactory.Context.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) { string str = action.KeyCombination.ReadableString(); @@ -52,7 +53,7 @@ namespace osu.Game.Input { var instance = ruleset.CreateInstance(); - using (RealmFactory.GetForWrite()) + using (realmFactory.GetForWrite()) { foreach (var variant in instance.AvailableVariants) insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); @@ -61,7 +62,7 @@ namespace osu.Game.Input private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { - using (var usage = RealmFactory.GetForWrite()) + using (var usage = realmFactory.GetForWrite()) { // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) From 8442b34e84032f9365a6575cece19c86690999dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 18:24:19 +0900 Subject: [PATCH 137/670] Tidy up write usage class --- osu.Game/Database/IRealmFactory.cs | 2 +- osu.Game/Database/RealmContextFactory.cs | 81 ++++++------------------ osu.Game/Database/RealmWriteUsage.cs | 55 ---------------- 3 files changed, 22 insertions(+), 116 deletions(-) delete mode 100644 osu.Game/Database/RealmWriteUsage.cs diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 0fffc3d7be..4a92a5683b 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -22,6 +22,6 @@ namespace osu.Game.Database /// This method may block if a write is already active on a different thread. /// /// A usage containing a usable context. - RealmWriteUsage GetForWrite(); + RealmContextFactory.RealmWriteUsage GetForWrite(); } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index ea3549a9cd..dc8761fb3c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; @@ -13,6 +14,7 @@ namespace osu.Game.Database public class RealmContextFactory : Component, IRealmFactory { private readonly Storage storage; + private const string database_name = @"client"; private const int schema_version = 5; @@ -22,20 +24,11 @@ namespace osu.Game.Database /// private readonly object writeLock = new object(); - private ThreadLocal refreshCompleted = new ThreadLocal(); - - private bool rollbackRequired; - private int currentWriteUsages; - private Transaction currentWriteTransaction; - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); - private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Refreshes"); - private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); - private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); - private static readonly GlobalStatistic contexts_open = GlobalStatistics.Get("Realm", "Contexts (Open)"); + private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); private Realm context; @@ -72,24 +65,8 @@ namespace osu.Game.Database writes.Value++; Monitor.Enter(writeLock); - Realm realm; - - try - { - realm = createContext(); - - currentWriteTransaction ??= realm.BeginWrite(); - } - catch - { - // retrieval of a context could trigger a fatal error. - Monitor.Exit(writeLock); - throw; - } - Interlocked.Increment(ref currentWriteUsages); - - return new RealmWriteUsage(realm, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; + return new RealmWriteUsage(this); } protected override void Update() @@ -111,41 +88,25 @@ namespace osu.Game.Database }); } - private void usageCompleted(RealmWriteUsage usage) - { - int usages = Interlocked.Decrement(ref currentWriteUsages); - - try - { - rollbackRequired |= usage.RollbackRequired; - - if (usages == 0) - { - if (rollbackRequired) - { - rollbacks.Value++; - currentWriteTransaction?.Rollback(); - } - else - { - commits.Value++; - currentWriteTransaction?.Commit(); - } - - currentWriteTransaction = null; - rollbackRequired = false; - - refreshCompleted = new ThreadLocal(); - } - } - finally - { - Monitor.Exit(writeLock); - } - } - private void onMigration(Migration migration, ulong lastSchemaVersion) { } + + public class RealmWriteUsage : InvokeOnDisposal + { + public readonly Realm Context; + + public RealmWriteUsage(RealmContextFactory factory) + : base(factory, usageCompleted) + { + Context = factory.createContext(); + Context.BeginWrite(); + } + + private static void usageCompleted(RealmContextFactory factory) + { + Monitor.Exit(factory.writeLock); + } + } } } diff --git a/osu.Game/Database/RealmWriteUsage.cs b/osu.Game/Database/RealmWriteUsage.cs deleted file mode 100644 index 35e30e8123..0000000000 --- a/osu.Game/Database/RealmWriteUsage.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using Realms; - -namespace osu.Game.Database -{ - public class RealmWriteUsage : IDisposable - { - public readonly Realm Context; - private readonly Action usageCompleted; - - public bool RollbackRequired { get; private set; } - - public RealmWriteUsage(Realm context, Action onCompleted) - { - Context = context; - usageCompleted = onCompleted; - } - - /// - /// Whether this write usage will commit a transaction on completion. - /// If false, there is a parent usage responsible for transaction commit. - /// - public bool IsTransactionLeader; - - private bool isDisposed; - - protected void Dispose(bool disposing) - { - if (isDisposed) return; - - isDisposed = true; - - usageCompleted?.Invoke(this); - } - - public void Rollback(Exception error = null) - { - RollbackRequired = true; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - ~RealmWriteUsage() - { - Dispose(false); - } - } -} From 9bf9a8c351ac533bbdd4b33a7f1659dd9544168f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 18:36:05 +0900 Subject: [PATCH 138/670] Remove Live<> wrapper until it is needed --- osu.Game/Database/Live.cs | 104 --------------------------- osu.Game/Database/RealmExtensions.cs | 27 ------- 2 files changed, 131 deletions(-) delete mode 100644 osu.Game/Database/Live.cs diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs deleted file mode 100644 index 49218ddd6e..0000000000 --- a/osu.Game/Database/Live.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Threading; -using Realms; - -#nullable enable - -namespace osu.Game.Database -{ - /// - /// Provides a method of passing realm live objects across threads in a safe fashion. - /// - /// - /// To consume this as a live instance, the live object should be stored and accessed via each time. - /// To consume this as a detached instance, assign to a variable of type . The implicit conversion will handle detaching an instance. - /// - /// The underlying object type. Should be a with a primary key provided via . - public class Live : IEquatable>, IHasGuidPrimaryKey - where T : RealmObject, IHasGuidPrimaryKey - { - /// - /// The primary key of the object. - /// - public Guid Guid { get; } - - public string ID - { - get => Guid.ToString(); - set => throw new NotImplementedException(); - } - - private readonly ThreadLocal threadValues; - - private readonly T original; - - private readonly IRealmFactory contextFactory; - - public Live(T item, IRealmFactory contextFactory) - { - this.contextFactory = contextFactory; - - original = item; - Guid = item.Guid; - - threadValues = new ThreadLocal(getThreadLocalValue); - - // the instance passed in may not be in a managed state. - // for now let's immediately retrieve a managed object on the current thread. - // in the future we may want to delay this until the first access (only populating the Guid at construction time). - if (!item.IsManaged) - original = Get(); - } - - private T getThreadLocalValue() - { - var context = contextFactory.Context; - - // only use the original if no context is available or the source realm is the same. - if (context == null || original.Realm?.IsSameInstance(context) == true) return original; - - return context.Find(ID); - } - - /// - /// Retrieve a live reference to the data. - /// - public T Get() => threadValues.Value; - - /// - /// Retrieve a detached copy of the data. - /// - public T Detach() => Get().Detach(); - - /// - /// Wrap a property of this instance as its own live access object. - /// - /// The child to return. - /// The underlying child object type. Should be a with a primary key provided via . - /// A wrapped instance of the child. - public Live WrapChild(Func lookup) - where TChild : RealmObject, IHasGuidPrimaryKey => new Live(lookup(Get()), contextFactory); - - /// - /// Perform a write operation on this live object. - /// - /// The action to perform. - public void PerformUpdate(Action perform) - { - using (contextFactory.GetForWrite()) - perform(Get()); - } - - public static implicit operator T?(Live? wrapper) - => wrapper?.Detach() ?? null; - - public static implicit operator Live(T obj) => obj.WrapAsUnmanaged(); - - public bool Equals(Live? other) => other != null && other.Guid == Guid; - - public override string ToString() => Get().ToString(); - } -} diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index b25299bf23..07d397ca6c 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.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 System.Collections.Generic; using AutoMapper; using osu.Game.Beatmaps; @@ -69,31 +68,5 @@ namespace osu.Game.Database return mapper.Map(item); } - - /// - /// Wrap a managed instance of a realm object in a . - /// - /// The item to wrap. - /// A factory to retrieve realm contexts from. - /// The type of object. - /// A wrapped instance of the provided item. - public static Live Wrap(this T item, IRealmFactory contextFactory) - where T : RealmObject, IHasGuidPrimaryKey => new Live(item, contextFactory); - - /// - /// Wrap an unmanaged instance of a realm object in a . - /// - /// The item to wrap. - /// The type of object. - /// A wrapped instance of the provided item. - /// Throws if the provided item is managed. - public static Live WrapAsUnmanaged(this T item) - where T : RealmObject, IHasGuidPrimaryKey - { - if (item.IsManaged) - throw new ArgumentException("Provided item must not be managed", nameof(item)); - - return new Live(item, null); - } } } From 674e78fd93858e00ff4f54b930c70d5172ca528f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 18:38:30 +0900 Subject: [PATCH 139/670] Fix broken xmldoc --- osu.Game/Database/RealmExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 07d397ca6c..04b0820dd9 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -56,7 +56,7 @@ namespace osu.Game.Database } /// - /// Create a detached copy of the each item in the list. + /// Create a detached copy of the item. /// /// The managed to detach. /// The type of object. From 4759797d152c4eeabf128671f5a49f7fd0725c6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 15:51:07 +0900 Subject: [PATCH 140/670] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 492c88c7e4..8a74f8ddce 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1a762be9c9..e43da1df7b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -27,7 +27,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 93be3645ee..aecc8cd5eb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From af1509d892b0788cd6458865d1c004919d560091 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 15:51:19 +0900 Subject: [PATCH 141/670] Remove unused variable (but add back pending writes counter) --- osu.Game/Database/RealmContextFactory.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index dc8761fb3c..c18cd31bfa 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -24,12 +24,11 @@ namespace osu.Game.Database /// private readonly object writeLock = new object(); - private int currentWriteUsages; - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); + private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes"); private Realm context; @@ -63,9 +62,10 @@ namespace osu.Game.Database public RealmWriteUsage GetForWrite() { writes.Value++; + pending_writes.Value++; + Monitor.Enter(writeLock); - Interlocked.Increment(ref currentWriteUsages); return new RealmWriteUsage(this); } @@ -106,6 +106,7 @@ namespace osu.Game.Database private static void usageCompleted(RealmContextFactory factory) { Monitor.Exit(factory.writeLock); + pending_writes.Value--; } } } From 8a08d3f4efe807ca77f0d4fe893962a3d6ec9bfc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:13:10 +0900 Subject: [PATCH 142/670] Fix transactions not actually being committed --- osu.Game/Database/RealmContextFactory.cs | 39 +++++++++++++++---- osu.Game/Input/RealmKeyBindingStore.cs | 13 +++---- osu.Game/OsuGameBase.cs | 8 ++-- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 10 +++-- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c18cd31bfa..0631acc750 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Threading; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; @@ -92,19 +92,42 @@ namespace osu.Game.Database { } - public class RealmWriteUsage : InvokeOnDisposal + /// + /// A transaction used for making changes to realm data. + /// + public class RealmWriteUsage : IDisposable { - public readonly Realm Context; + public readonly Realm Realm; - public RealmWriteUsage(RealmContextFactory factory) - : base(factory, usageCompleted) + private readonly RealmContextFactory factory; + private readonly Transaction transaction; + + internal RealmWriteUsage(RealmContextFactory factory) { - Context = factory.createContext(); - Context.BeginWrite(); + this.factory = factory; + + Realm = factory.createContext(); + transaction = Realm.BeginWrite(); } - private static void usageCompleted(RealmContextFactory factory) + /// + /// Commit all changes made in this transaction. + /// + public void Commit() => transaction.Commit(); + + /// + /// Revert all changes made in this transaction. + /// + public void Rollback() => transaction.Rollback(); + + /// + /// Disposes this instance, calling the initially captured action. + /// + public virtual void Dispose() { + // rollback if not explicitly committed. + transaction?.Dispose(); + Monitor.Exit(factory.writeLock); pending_writes.Value--; } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 756df6434e..d55d2362fe 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -53,11 +53,8 @@ namespace osu.Game.Input { var instance = ruleset.CreateInstance(); - using (realmFactory.GetForWrite()) - { - foreach (var variant in instance.AvailableVariants) - insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); - } + foreach (var variant in instance.AvailableVariants) + insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); } private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) @@ -67,7 +64,7 @@ namespace osu.Game.Input // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = usage.Context.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)group.Key); + int count = usage.Realm.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -76,7 +73,7 @@ namespace osu.Game.Input foreach (var insertable in group.Skip(count).Take(aimCount - count)) { // insert any defaults which are missing. - usage.Context.Add(new RealmKeyBinding + usage.Realm.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), KeyCombinationString = insertable.KeyCombination.ToString(), @@ -86,6 +83,8 @@ namespace osu.Game.Input }); } } + + usage.Commit(); } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 192867b8c8..a755fdb379 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -330,16 +330,16 @@ namespace osu.Game private void migrateDataToRealm() { using (var db = contextFactory.GetForWrite()) - using (var realm = realmFactory.GetForWrite()) + using (var usage = realmFactory.GetForWrite()) { var existingBindings = db.Context.DatabasedKeyBinding; // only migrate data if the realm database is empty. - if (!realm.Context.All().Any()) + if (!usage.Realm.All().Any()) { foreach (var dkb in existingBindings) { - realm.Context.Add(new RealmKeyBinding + usage.Realm.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), KeyCombinationString = dkb.KeyCombination.ToString(), @@ -351,6 +351,8 @@ namespace osu.Game } db.Context.RemoveRange(existingBindings); + + usage.Commit(); } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 34cdfd18fa..bfabc8008d 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -129,10 +129,12 @@ namespace osu.Game.Overlays.KeyBinding var button = buttons[i++]; button.UpdateKeyCombination(d); - using (var write = realmFactory.GetForWrite()) + using (var usage = realmFactory.GetForWrite()) { - var binding = write.Context.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; + + usage.Commit(); } } } @@ -294,8 +296,10 @@ namespace osu.Game.Overlays.KeyBinding { using (var write = realmFactory.GetForWrite()) { - var binding = write.Context.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID); + var binding = write.Realm.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID); binding.KeyCombinationString = bindTarget.KeyBinding.KeyCombinationString; + + write.Commit(); } bindTarget.IsBinding = false; From e3c5a909e4a641c4d79aa0da889e1f921b063b31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:30:57 +0900 Subject: [PATCH 143/670] Fix known non-null variable --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 305a17126a..0f270cbece 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -223,9 +223,7 @@ namespace osu.Game.Overlays.Toolbar { if (realmKeyBinding != null) { - KeyCombination? binding = realmKeyBinding.KeyCombination; - - var keyBindingString = binding?.ReadableString(); + var keyBindingString = realmKeyBinding.KeyCombination.ReadableString(); if (!string.IsNullOrEmpty(keyBindingString)) keyBindingTooltip.Text = $" ({keyBindingString})"; From df08d964a5c3894224f59c1d0562e107767cb062 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:31:18 +0900 Subject: [PATCH 144/670] Mark the types which have been migrated in OsuDbContext --- osu.Game/Database/OsuDbContext.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 2ae07b3cf8..8ca65525db 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -24,13 +24,15 @@ namespace osu.Game.Database public DbSet BeatmapDifficulty { get; set; } public DbSet BeatmapMetadata { get; set; } public DbSet BeatmapSetInfo { get; set; } - public DbSet DatabasedKeyBinding { get; set; } public DbSet DatabasedSetting { get; set; } public DbSet FileInfo { get; set; } public DbSet RulesetInfo { get; set; } public DbSet SkinInfo { get; set; } public DbSet ScoreInfo { get; set; } + // migrated to realm + public DbSet DatabasedKeyBinding { get; set; } + private readonly string connectionString; private static readonly Lazy logger = new Lazy(() => new OsuDbLoggerFactory()); From 8d071f97fb1e193b64c379c82d35374b316f7226 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:33:03 +0900 Subject: [PATCH 145/670] Early return --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 0f270cbece..17bc913b43 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -163,21 +163,20 @@ namespace osu.Game.Overlays.Toolbar { base.LoadComplete(); - if (Hotkey != null) + if (Hotkey == null) return; + + realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); + + if (realmKeyBinding != null) { - realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); - - if (realmKeyBinding != null) + realmKeyBinding.PropertyChanged += (sender, args) => { - realmKeyBinding.PropertyChanged += (sender, args) => - { - if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) - updateKeyBindingTooltip(); - }; - } - - updateKeyBindingTooltip(); + if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) + updateKeyBindingTooltip(); + }; } + + updateKeyBindingTooltip(); } protected override bool OnMouseDown(MouseDownEvent e) => true; From fd582f521c567c4554b8b60a521189480a882920 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:33:55 +0900 Subject: [PATCH 146/670] Reduce lifetime of realm context usage in detach scenario --- .../KeyBinding/KeyBindingsSubsection.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index fae42f5492..1cd600a72d 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -37,21 +37,21 @@ namespace osu.Game.Overlays.KeyBinding { var rulesetId = Ruleset?.ID; + List bindings; + using (var realm = realmFactory.GetForRead()) + bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + + foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { - var bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + int intKey = (int)defaultGroup.Key; - foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) + // one row per valid action. + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList()) { - int intKey = (int)defaultGroup.Key; - - // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList()) - { - AllowMainMouseButtons = Ruleset != null, - Defaults = defaultGroup.Select(d => d.KeyCombination) - }); - } + AllowMainMouseButtons = Ruleset != null, + Defaults = defaultGroup.Select(d => d.KeyCombination) + }); } Add(new ResetButton From f26c6210f36c9e73d18cb971ff950823f060900b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:36:24 +0900 Subject: [PATCH 147/670] Remove unnecessary Take() call and refactor default group logic naming --- osu.Game/Input/RealmKeyBindingStore.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index d55d2362fe..f5c4b646c1 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -62,22 +62,21 @@ namespace osu.Game.Input using (var usage = realmFactory.GetForWrite()) { // compare counts in database vs defaults - foreach (var group in defaults.GroupBy(k => k.Action)) + foreach (var defaultsForAction in defaults.GroupBy(k => k.Action)) { - int count = usage.Realm.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)group.Key); - int aimCount = group.Count(); + int existingCount = usage.Realm.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key); - if (aimCount <= count) + if (defaultsForAction.Count() <= existingCount) continue; - foreach (var insertable in group.Skip(count).Take(aimCount - count)) + foreach (var k in defaultsForAction.Skip(existingCount)) { // insert any defaults which are missing. usage.Realm.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), - KeyCombinationString = insertable.KeyCombination.ToString(), - ActionInt = (int)insertable.Action, + KeyCombinationString = k.KeyCombination.ToString(), + ActionInt = (int)k.Action, RulesetID = rulesetId, Variant = variant }); From 693602513ea5d7b5fbefd8dfb5f2d43b37dbed2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 14:22:48 +0900 Subject: [PATCH 148/670] Update test to use GetForWrite --- osu.Game.Tests/Database/TestRealmKeyBindingStore.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 1a7e0d67c5..6c0811f633 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -11,6 +11,7 @@ using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; +using Realms; namespace osu.Game.Tests.Database { @@ -62,12 +63,15 @@ namespace osu.Game.Tests.Database Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); - var binding = backBinding; + var tsr = ThreadSafeReference.Create(backBinding); - realmContextFactory.Context.Write(() => + using (var usage = realmContextFactory.GetForWrite()) { + var binding = usage.Realm.ResolveReference(tsr); binding.KeyCombination = new KeyCombination(InputKey.BackSpace); - }); + + usage.Commit(); + } Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); From 3e366b1f1545ca0f44e7b28bac1a82985e36b5f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 14:26:06 +0900 Subject: [PATCH 149/670] Ensure the main realm context is closed when the factory is disposed --- osu.Game.Tests/Database/TestRealmKeyBindingStore.cs | 1 + osu.Game/Database/RealmContextFactory.cs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 6c0811f633..cac331451b 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -83,6 +83,7 @@ namespace osu.Game.Tests.Database [TearDown] public void TearDown() { + realmContextFactory.Dispose(); storage.DeleteDirectory(string.Empty); } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0631acc750..b76006cd88 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -36,6 +36,9 @@ namespace osu.Game.Database { get { + if (IsDisposed) + throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory"); + if (context == null) { context = createContext(); @@ -92,6 +95,14 @@ namespace osu.Game.Database { } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + context?.Dispose(); + context = null; + } + /// /// A transaction used for making changes to realm data. /// From ddc63662ba9da2635bd4d3b851b1ee8922eea453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Jan 2021 16:39:04 +0100 Subject: [PATCH 150/670] Dispose realm in RealmWriteUsage cleanup --- osu.Game/Database/RealmContextFactory.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index b76006cd88..f735098e88 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -138,6 +138,7 @@ namespace osu.Game.Database { // rollback if not explicitly committed. transaction?.Dispose(); + Realm?.Dispose(); Monitor.Exit(factory.writeLock); pending_writes.Value--; From 0f8f0434f95bce74cf9c0e3ec38e2ad4476b3f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Jan 2021 17:03:58 +0100 Subject: [PATCH 151/670] Remove EF store again after mis-merge Was originally deleted in 536e7229d0cb82504a39f0a18e120da91e0b0f12. --- osu.Game/Input/KeyBindingStore.cs | 108 ------------------------------ 1 file changed, 108 deletions(-) delete mode 100644 osu.Game/Input/KeyBindingStore.cs diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs deleted file mode 100644 index b25b00eb84..0000000000 --- a/osu.Game/Input/KeyBindingStore.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Input.Bindings; -using osu.Framework.Platform; -using osu.Game.Database; -using osu.Game.Input.Bindings; -using osu.Game.Rulesets; - -namespace osu.Game.Input -{ - public class KeyBindingStore : DatabaseBackedStore - { - public event Action KeyBindingChanged; - - public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) - : base(contextFactory, storage) - { - using (ContextFactory.GetForWrite()) - { - foreach (var info in rulesets.AvailableRulesets) - { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); - } - } - } - - public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); - - /// - /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. - /// - /// The action to lookup. - /// A set of display strings for all the user's key configuration for the action. - public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) - { - foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction)) - { - string str = action.KeyCombination.ReadableString(); - - // even if found, the readable string may be empty for an unbound action. - if (str.Length > 0) - yield return str; - } - } - - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) - { - using (var usage = ContextFactory.GetForWrite()) - { - // compare counts in database vs defaults - foreach (var group in defaults.GroupBy(k => k.Action)) - { - int count = Query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key); - int aimCount = group.Count(); - - if (aimCount <= count) - continue; - - foreach (var insertable in group.Skip(count).Take(aimCount - count)) - { - // insert any defaults which are missing. - usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding - { - KeyCombination = insertable.KeyCombination, - Action = insertable.Action, - RulesetID = rulesetId, - Variant = variant - }); - - // required to ensure stable insert order (https://github.com/dotnet/efcore/issues/11686) - usage.Context.SaveChanges(); - } - } - } - } - - /// - /// Retrieve s for a specified ruleset/variant content. - /// - /// The ruleset's internal ID. - /// An optional variant. - /// - public List Query(int? rulesetId = null, int? variant = null) => - ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); - - public void Update(KeyBinding keyBinding) - { - using (ContextFactory.GetForWrite()) - { - var dbKeyBinding = (DatabasedKeyBinding)keyBinding; - Refresh(ref dbKeyBinding); - - if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) - return; - - dbKeyBinding.KeyCombination = keyBinding.KeyCombination; - } - - KeyBindingChanged?.Invoke(); - } - } -} From cd8401c39c591955da4d2be52d4182dea6b6aa3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Jan 2021 17:20:33 +0100 Subject: [PATCH 152/670] Suppress nuget warning due to including beta realm --- Directory.Build.props | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2e1873a9ed..8e0693f8d5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,12 +33,16 @@ DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway. This is required due to https://github.com/NuGet/Home/issues/5740 + NU5104: + This is triggered on osu.Game due to using a beta/prerelease version of realm. + Warning suppression can be removed after migrating off of a beta release. + CA9998: Microsoft.CodeAnalysis.FxCopAnalyzers has been deprecated. The entire package will be able to be removed after migrating to .NET 5, as analysers are shipped as part of the .NET 5 SDK anyway. --> - $(NoWarn);NU1701;CA9998 + $(NoWarn);NU1701;NU5104;CA9998 false From 15db0e97d7f84729034652e2c63e988b51ac15cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Jan 2021 18:06:32 +0900 Subject: [PATCH 153/670] Update realm version --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e48c103700..7c49a250c4 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + From 68f2e7f61ae35e800fd34e1dff4e1bdf6a411164 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Jan 2021 18:22:23 +0900 Subject: [PATCH 154/670] Use realm support for Guid --- osu.Game/Database/IHasGuidPrimaryKey.cs | 12 +----------- osu.Game/Database/RealmContextFactory.cs | 11 ++++++++++- osu.Game/Input/Bindings/RealmKeyBinding.cs | 3 ++- osu.Game/Input/RealmKeyBindingStore.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index 3b0888c654..ca41d70210 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; using Realms; @@ -11,16 +10,7 @@ namespace osu.Game.Database public interface IHasGuidPrimaryKey { [JsonIgnore] - [Ignored] - public Guid Guid - { - get => new Guid(ID); - set => ID = value.ToString(); - } - - [JsonIgnore] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] [PrimaryKey] - string ID { get; set; } + public Guid ID { get; set; } } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index f735098e88..918ec1eb53 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -7,7 +7,9 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; +using osu.Game.Input.Bindings; using Realms; +using Realms.Schema; namespace osu.Game.Database { @@ -17,7 +19,7 @@ namespace osu.Game.Database private const string database_name = @"client"; - private const int schema_version = 5; + private const int schema_version = 6; /// /// Lock object which is held for the duration of a write operation (via ). @@ -93,6 +95,13 @@ namespace osu.Game.Database private void onMigration(Migration migration, ulong lastSchemaVersion) { + switch (lastSchemaVersion) + { + case 5: + // let's keep things simple. changing the type of the primary key is a bit involved. + migration.NewRealm.RemoveAll(); + break; + } } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index ecffc1fd62..d10cb6af83 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.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 osu.Framework.Input.Bindings; using osu.Game.Database; using Realms; @@ -11,7 +12,7 @@ namespace osu.Game.Input.Bindings public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { [PrimaryKey] - public string ID { get; set; } + public Guid ID { get; set; } public int? RulesetID { get; set; } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index f5c4b646c1..76e65c1c70 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -74,7 +74,7 @@ namespace osu.Game.Input // insert any defaults which are missing. usage.Realm.Add(new RealmKeyBinding { - ID = Guid.NewGuid().ToString(), + ID = Guid.NewGuid(), KeyCombinationString = k.KeyCombination.ToString(), ActionInt = (int)k.Action, RulesetID = rulesetId, diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index ae1ec97a4c..d98a20925a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -341,7 +341,7 @@ namespace osu.Game { usage.Realm.Add(new RealmKeyBinding { - ID = Guid.NewGuid().ToString(), + ID = Guid.NewGuid(), KeyCombinationString = dkb.KeyCombination.ToString(), ActionInt = (int)dkb.Action, RulesetID = dkb.RulesetID, From f6c20095094bdbf26c6ffe55dfdab12fe2f7e083 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Jan 2021 20:10:10 +0900 Subject: [PATCH 155/670] Remove unused using --- osu.Game/Database/RealmContextFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 918ec1eb53..d7e35f736e 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -9,7 +9,6 @@ using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Game.Input.Bindings; using Realms; -using Realms.Schema; namespace osu.Game.Database { From d2bf3a58057f1ea5917e16bdc2288e7e068282ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 19:01:58 +0900 Subject: [PATCH 156/670] Add ignore files to avoid copying realm management/pipes --- osu.Game/IO/OsuStorage.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 8097f61ea4..f7abd2a6f1 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -33,12 +33,17 @@ namespace osu.Game.IO private readonly StorageConfigManager storageConfig; private readonly Storage defaultStorage; - public override string[] IgnoreDirectories => new[] { "cache" }; + public override string[] IgnoreDirectories => new[] + { + "cache", + "client.realm.management" + }; public override string[] IgnoreFiles => new[] { "framework.ini", - "storage.ini" + "storage.ini", + "client.realm.note" }; public OsuStorage(GameHost host, Storage defaultStorage) From 34a7ce912eaf46cdf691d1525a8f4dfa105d4476 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 19:02:09 +0900 Subject: [PATCH 157/670] Correctly close context before attempting migration --- osu.Game/Database/RealmContextFactory.cs | 17 +++++++++++------ osu.Game/OsuGameBase.cs | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index d7e35f736e..35ff91adb2 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -1,6 +1,3 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - using System; using System.Threading; using osu.Framework.Graphics; @@ -77,7 +74,7 @@ namespace osu.Game.Database { base.Update(); - if (Context.Refresh()) + if (context?.Refresh() == true) refreshes.Value++; } @@ -107,8 +104,7 @@ namespace osu.Game.Database { base.Dispose(isDisposing); - context?.Dispose(); - context = null; + FlushConnections(); } /// @@ -152,5 +148,14 @@ namespace osu.Game.Database pending_writes.Value--; } } + + public void FlushConnections() + { + var previousContext = context; + context = null; + previousContext?.Dispose(); + while (previousContext?.IsClosed == false) + Thread.Sleep(50); + } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 759be79045..4bd4a6ae7f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -513,6 +513,7 @@ namespace osu.Game public void Migrate(string path) { contextFactory.FlushConnections(); + realmFactory.FlushConnections(); (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); } } From 47a9d2b1c2c49fe9c8bdb9024c00857786469734 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 20:53:16 +0900 Subject: [PATCH 158/670] Add missing licence header --- osu.Game/Database/RealmContextFactory.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 35ff91adb2..0ff4f857b9 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -1,3 +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 System.Threading; using osu.Framework.Graphics; From d480aa0e42187b35f819b7e6ad9b44baf1bf6579 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 22:57:55 +0900 Subject: [PATCH 159/670] Don't check for all ignored files being present in original folder (the realm exception is platform dependent) --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 045246e5ed..b0f9768e09 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -142,7 +142,8 @@ namespace osu.Game.Tests.NonVisual foreach (var file in osuStorage.IgnoreFiles) { - Assert.That(File.Exists(Path.Combine(originalDirectory, file))); + if (file.EndsWith(".ini", StringComparison.Ordinal)) + Assert.That(File.Exists(Path.Combine(originalDirectory, file))); Assert.That(storage.Exists(file), Is.False); } From d69a4914e05f5f5d1c2802fcad8551f9c1fe52c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Jan 2021 17:28:47 +0900 Subject: [PATCH 160/670] Add method to block all realm access during migration operation --- osu.Game/Database/IRealmFactory.cs | 2 +- osu.Game/Database/RealmContextFactory.cs | 87 ++++++++++++++----- osu.Game/OsuGameBase.cs | 8 +- .../KeyBinding/KeyBindingsSubsection.cs | 4 +- 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 4a92a5683b..025c44f440 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -15,7 +15,7 @@ namespace osu.Game.Database /// /// Get a fresh context for read usage. /// - Realm GetForRead(); + RealmContextFactory.RealmUsage GetForRead(); /// /// Request a context for write usage. diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0ff4f857b9..c5d0061143 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -3,6 +3,7 @@ using System; using System.Threading; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; @@ -30,6 +31,9 @@ namespace osu.Game.Database private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes"); + private static readonly GlobalStatistic active_usages = GlobalStatistics.Get("Realm", "Active usages"); + + private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true); private Realm context; @@ -57,10 +61,10 @@ namespace osu.Game.Database this.storage = storage; } - public Realm GetForRead() + public RealmUsage GetForRead() { reads.Value++; - return createContext(); + return new RealmUsage(this); } public RealmWriteUsage GetForWrite() @@ -83,6 +87,8 @@ namespace osu.Game.Database private Realm createContext() { + blockingResetEvent.Wait(); + contexts_created.Value++; return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) @@ -107,24 +113,69 @@ namespace osu.Game.Database { base.Dispose(isDisposing); - FlushConnections(); + BlockAllOperations(); + } + + public IDisposable BlockAllOperations() + { + blockingResetEvent.Reset(); + flushContexts(); + + return new InvokeOnDisposal(this, r => endBlockingSection()); + } + + private void endBlockingSection() + { + blockingResetEvent.Set(); + } + + private void flushContexts() + { + var previousContext = context; + context = null; + + // wait for all threaded usages to finish + while (active_usages.Value > 0) + Thread.Sleep(50); + + previousContext?.Dispose(); + } + + /// + /// A usage of realm from an arbitrary thread. + /// + public class RealmUsage : IDisposable + { + public readonly Realm Realm; + + protected readonly RealmContextFactory Factory; + + internal RealmUsage(RealmContextFactory factory) + { + Factory = factory; + Realm = factory.createContext(); + } + + /// + /// Disposes this instance, calling the initially captured action. + /// + public virtual void Dispose() + { + Realm?.Dispose(); + active_usages.Value--; + } } /// /// A transaction used for making changes to realm data. /// - public class RealmWriteUsage : IDisposable + public class RealmWriteUsage : RealmUsage { - public readonly Realm Realm; - - private readonly RealmContextFactory factory; private readonly Transaction transaction; internal RealmWriteUsage(RealmContextFactory factory) + : base(factory) { - this.factory = factory; - - Realm = factory.createContext(); transaction = Realm.BeginWrite(); } @@ -141,24 +192,16 @@ namespace osu.Game.Database /// /// Disposes this instance, calling the initially captured action. /// - public virtual void Dispose() + public override void Dispose() { // rollback if not explicitly committed. transaction?.Dispose(); - Realm?.Dispose(); - Monitor.Exit(factory.writeLock); + base.Dispose(); + + Monitor.Exit(Factory.writeLock); pending_writes.Value--; } } - - public void FlushConnections() - { - var previousContext = context; - context = null; - previousContext?.Dispose(); - while (previousContext?.IsClosed == false) - Thread.Sleep(50); - } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4bd4a6ae7f..7806a38cd9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -512,9 +512,11 @@ namespace osu.Game public void Migrate(string path) { - contextFactory.FlushConnections(); - realmFactory.FlushConnections(); - (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + using (realmFactory.BlockAllOperations()) + { + contextFactory.FlushConnections(); + (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + } } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index 1cd600a72d..ac77440dfa 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -39,8 +39,8 @@ namespace osu.Game.Overlays.KeyBinding List bindings; - using (var realm = realmFactory.GetForRead()) - bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + using (var usage = realmFactory.GetForRead()) + bindings = usage.Realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { From 4d976094d1c69613ef581828d638ffdb04a48984 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Mar 2021 20:07:53 +0900 Subject: [PATCH 161/670] Switch Guid implementation temporarily to avoid compile time error --- osu.Game/Input/Bindings/RealmKeyBinding.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index d10cb6af83..9abb749328 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -12,7 +12,14 @@ namespace osu.Game.Input.Bindings public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { [PrimaryKey] - public Guid ID { get; set; } + public string StringGuid { get; set; } + + [Ignored] + public Guid ID + { + get => Guid.Parse(StringGuid); + set => StringGuid = value.ToString(); + } public int? RulesetID { get; set; } From 015cf5f7eba34442e07fd888af802443e3999075 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 13:22:48 +0900 Subject: [PATCH 162/670] Fix tests using wrong ID lookup type --- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 0f9505beda..87a6fe00cc 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -131,7 +131,7 @@ namespace osu.Game.Overlays.KeyBinding using (var usage = realmFactory.GetForWrite()) { - var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID.ToString()); binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; usage.Commit(); @@ -296,7 +296,7 @@ namespace osu.Game.Overlays.KeyBinding { using (var write = realmFactory.GetForWrite()) { - var binding = write.Realm.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID); + var binding = write.Realm.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID.ToString()); binding.KeyCombinationString = bindTarget.KeyBinding.KeyCombinationString; write.Commit(); From 1281273dd32d7fb47ad4e943a46d7d8099b8b7e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 22:46:20 +0900 Subject: [PATCH 163/670] Add back automapper dependency --- osu.Game/osu.Game.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7d4ae6ff8f..97ae793f7f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,6 +18,7 @@ + From 37bf79e8a412d88b4796627b4447d78458bd56af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Mar 2021 15:10:03 +0900 Subject: [PATCH 164/670] Remove unused automapper setup for the time being --- osu.Game/Database/RealmExtensions.cs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 04b0820dd9..aee36e81c5 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -3,13 +3,7 @@ using System.Collections.Generic; using AutoMapper; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Input.Bindings; -using osu.Game.IO; -using osu.Game.Rulesets; -using osu.Game.Scoring; -using osu.Game.Skinning; using Realms; namespace osu.Game.Database @@ -21,22 +15,7 @@ namespace osu.Game.Database c.ShouldMapField = fi => false; c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - - c.CreateMap() - .ForMember(s => s.Beatmaps, d => d.MapFrom(s => s.Beatmaps)) - .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) - .MaxDepth(2); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); }).CreateMapper(); /// From ecde6137e0f907644eb426b37268de6e07d7cf31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Mar 2021 15:16:01 +0900 Subject: [PATCH 165/670] Add missing active usage counter increment --- osu.Game/Database/RealmContextFactory.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c5d0061143..ed5931dd2b 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -152,6 +152,7 @@ namespace osu.Game.Database internal RealmUsage(RealmContextFactory factory) { + active_usages.Value++; Factory = factory; Realm = factory.createContext(); } From 7e73af1c5a1a6a95b7d54c208e0de84b612015bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 31 Mar 2021 23:39:23 +0200 Subject: [PATCH 166/670] Revert "Suppress nuget warning due to including beta realm" This reverts commit cd8401c39c591955da4d2be52d4182dea6b6aa3a. Suppression is no longer necessary, as a normal realm release is used now. --- Directory.Build.props | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index a91d423043..53ad973e47 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,16 +33,12 @@ DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway. This is required due to https://github.com/NuGet/Home/issues/5740 - NU5104: - This is triggered on osu.Game due to using a beta/prerelease version of realm. - Warning suppression can be removed after migrating off of a beta release. - CA9998: Microsoft.CodeAnalysis.FxCopAnalyzers has been deprecated. The entire package will be able to be removed after migrating to .NET 5, as analysers are shipped as part of the .NET 5 SDK anyway. --> - $(NoWarn);NU1701;NU5104;CA9998 + $(NoWarn);NU1701;CA9998 false From cc9db90d11f6f6d3f0882d194efeaaaa4e696484 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 3 Apr 2021 18:58:25 +0900 Subject: [PATCH 167/670] Extract common implementation into private method --- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 87a6fe00cc..767852896b 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -129,13 +129,7 @@ namespace osu.Game.Overlays.KeyBinding var button = buttons[i++]; button.UpdateKeyCombination(d); - using (var usage = realmFactory.GetForWrite()) - { - var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID.ToString()); - binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; - - usage.Commit(); - } + updateStoreFromButton(button); } } @@ -294,13 +288,7 @@ namespace osu.Game.Overlays.KeyBinding { if (bindTarget != null) { - using (var write = realmFactory.GetForWrite()) - { - var binding = write.Realm.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID.ToString()); - binding.KeyCombinationString = bindTarget.KeyBinding.KeyCombinationString; - - write.Commit(); - } + updateStoreFromButton(bindTarget); bindTarget.IsBinding = false; Schedule(() => @@ -345,6 +333,17 @@ namespace osu.Game.Overlays.KeyBinding if (bindTarget != null) bindTarget.IsBinding = true; } + private void updateStoreFromButton(KeyButton button) + { + using (var usage = realmFactory.GetForWrite()) + { + var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID.ToString()); + binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; + + usage.Commit(); + } + } + private class CancelButton : TriangleButton { public CancelButton() From f9603eefe5fed7e02c040a0e12a4b27541e4aa60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Apr 2021 01:59:55 +0900 Subject: [PATCH 168/670] Revert "Switch Guid implementation temporarily to avoid compile time error" This reverts commit 4d976094d1c69613ef581828d638ffdb04a48984. --- osu.Game/Input/Bindings/RealmKeyBinding.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 9abb749328..d10cb6af83 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -12,14 +12,7 @@ namespace osu.Game.Input.Bindings public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { [PrimaryKey] - public string StringGuid { get; set; } - - [Ignored] - public Guid ID - { - get => Guid.Parse(StringGuid); - set => StringGuid = value.ToString(); - } + public Guid ID { get; set; } public int? RulesetID { get; set; } From 0fce0a420463f80b111cb9f976db6fdee7d1c3ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Apr 2021 02:04:42 +0900 Subject: [PATCH 169/670] Update to prerelease realm version --- osu.Game/FodyWeavers.xsd | 8 +++++++- osu.Game/osu.Game.csproj | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/FodyWeavers.xsd b/osu.Game/FodyWeavers.xsd index f526bddb09..447878c551 100644 --- a/osu.Game/FodyWeavers.xsd +++ b/osu.Game/FodyWeavers.xsd @@ -5,7 +5,13 @@ - + + + + Disables anonymized usage information from being sent on build. Read more about what data is being collected and why here: https://github.com/realm/realm-dotnet/blob/master/Realm/Realm.Fody/Common/Analytics.cs + + + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3bc0874b2d..81b89d587c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -32,7 +32,7 @@ - + From 6dd48f204c2dec42e6afb837bab65b06a15eb39e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Apr 2021 02:05:53 +0900 Subject: [PATCH 170/670] Remove unused store resolution --- osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 98b86d0b82..5f500d3023 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -26,9 +26,6 @@ namespace osu.Game.Input.Bindings private IDisposable realmSubscription; private IQueryable realmKeyBindings; - [Resolved] - private RealmKeyBindingStore store { get; set; } - [Resolved] private RealmContextFactory realmFactory { get; set; } From b9ee63ff891da7ce69245fc3f3bf66177064ef72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Apr 2021 02:13:29 +0900 Subject: [PATCH 171/670] Remove `public` keywords from interface implementations --- osu.Game/Database/IHasGuidPrimaryKey.cs | 2 +- osu.Game/Database/IRealmFactory.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index ca41d70210..c9cd9b257a 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -11,6 +11,6 @@ namespace osu.Game.Database { [JsonIgnore] [PrimaryKey] - public Guid ID { get; set; } + Guid ID { get; set; } } } diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 025c44f440..c79442134c 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -10,7 +10,7 @@ namespace osu.Game.Database /// /// The main realm context, bound to the update thread. /// - public Realm Context { get; } + Realm Context { get; } /// /// Get a fresh context for read usage. From 311cfe04bbc8842f98ad08c00a46dbf63fbfde2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Jan 2021 17:20:33 +0100 Subject: [PATCH 172/670] Suppress nuget warning due to including beta realm --- Directory.Build.props | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 53ad973e47..a91d423043 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,12 +33,16 @@ DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway. This is required due to https://github.com/NuGet/Home/issues/5740 + NU5104: + This is triggered on osu.Game due to using a beta/prerelease version of realm. + Warning suppression can be removed after migrating off of a beta release. + CA9998: Microsoft.CodeAnalysis.FxCopAnalyzers has been deprecated. The entire package will be able to be removed after migrating to .NET 5, as analysers are shipped as part of the .NET 5 SDK anyway. --> - $(NoWarn);NU1701;CA9998 + $(NoWarn);NU1701;NU5104;CA9998 false From 3bf462e4fad1855a538417171d7b1ffdd17dff7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 15:35:26 +0900 Subject: [PATCH 173/670] Add ignore rule for migrations for client.realm.lock --- osu.Game/IO/OsuStorage.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 1ed9a80a80..75130b0f9b 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -43,7 +43,8 @@ namespace osu.Game.IO { "framework.ini", "storage.ini", - "client.realm.note" + "client.realm.note", + "client.realm.lock", }; public OsuStorage(GameHost host, Storage defaultStorage) From 2c1422b4f90fd1abaa3552cd19730491d4b6b6b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 15:37:19 +0900 Subject: [PATCH 174/670] Add comment regarding teste edge case --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 6796344f24..a540ad7247 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -142,6 +142,8 @@ namespace osu.Game.Tests.NonVisual foreach (var file in osuStorage.IgnoreFiles) { + // avoid touching realm files which may be a pipe and break everything. + // this is also done locally inside OsuStorage via the IgnoreFiles list. if (file.EndsWith(".ini", StringComparison.Ordinal)) Assert.That(File.Exists(Path.Combine(originalDirectory, file))); Assert.That(storage.Exists(file), Is.False); From 8961203f0858b883783966a9b35ac6ce405d4cf9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 17:06:03 +0900 Subject: [PATCH 175/670] Move guid initialisation to database model itself --- osu.Game/Input/Bindings/RealmKeyBinding.cs | 2 +- osu.Game/Input/RealmKeyBindingStore.cs | 1 - osu.Game/OsuGameBase.cs | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index d10cb6af83..334d2da427 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -12,7 +12,7 @@ namespace osu.Game.Input.Bindings public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { [PrimaryKey] - public Guid ID { get; set; } + public Guid ID { get; set; } = Guid.NewGuid(); public int? RulesetID { get; set; } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 76e65c1c70..ca830286df 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -74,7 +74,6 @@ namespace osu.Game.Input // insert any defaults which are missing. usage.Realm.Add(new RealmKeyBinding { - ID = Guid.NewGuid(), KeyCombinationString = k.KeyCombination.ToString(), ActionInt = (int)k.Action, RulesetID = rulesetId, diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0bb4d57ec3..c0796f41a0 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -370,7 +370,6 @@ namespace osu.Game { usage.Realm.Add(new RealmKeyBinding { - ID = Guid.NewGuid(), KeyCombinationString = dkb.KeyCombination.ToString(), ActionInt = (int)dkb.Action, RulesetID = dkb.RulesetID, From 61f3cc9bc2cc843f50e6b1548a3e14929619e494 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 17:10:28 +0900 Subject: [PATCH 176/670] Fix update method not switched across to using `Guid` --- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 767852896b..d98fea8eec 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -337,7 +337,7 @@ namespace osu.Game.Overlays.KeyBinding { using (var usage = realmFactory.GetForWrite()) { - var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID.ToString()); + var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; usage.Commit(); From 253c66034d05011838b1a8bfc994e49517a9fbd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 17:45:43 +0900 Subject: [PATCH 177/670] Remove unused using statement --- osu.Game/Input/RealmKeyBindingStore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index ca830286df..547c79c209 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.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 System.Collections.Generic; using System.Linq; using osu.Framework.Input.Bindings; From 21b6adbf791a213c581aafddcd12a3da3d2ac19f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 17:52:17 +0900 Subject: [PATCH 178/670] Remove DI caching of `RealmKeyBindingStore` --- osu.Game/OsuGameBase.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index c0796f41a0..1e0c8f1332 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -290,8 +290,6 @@ namespace osu.Game migrateDataToRealm(); - dependencies.CacheAs(KeyBindingStore = new RealmKeyBindingStore(realmFactory)); - dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); From 9770c316e2bab93008785cfb880301f99e7a323c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 18:24:28 +0900 Subject: [PATCH 179/670] Add back the construction of the `KeyBindingStore` This reverts commit 21b6adbf791a213c581aafddcd12a3da3d2ac19f. --- osu.Game/OsuGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1e0c8f1332..4c5920c616 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -337,6 +337,7 @@ namespace osu.Game base.Content.Add(CreateScalingContainer().WithChildren(mainContent)); + KeyBindingStore = new RealmKeyBindingStore(realmFactory); KeyBindingStore.Register(globalBindings); foreach (var r in RulesetStore.AvailableRulesets) From 5f6fd9ba13a81a4f308a19ab3021e2bdaa525b26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 19:07:42 +0900 Subject: [PATCH 180/670] Remove outdated dependency requirement in test --- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index bcad8f2d3c..4e5e8517a4 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual typeof(FileStore), typeof(ScoreManager), typeof(BeatmapManager), - typeof(RealmKeyBindingStore), typeof(SettingsStore), typeof(RulesetConfigCache), typeof(OsuColour), From 2a87b3d74bdf0bbecaea497d6992fb6760b667d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 May 2021 13:43:55 +0900 Subject: [PATCH 181/670] Update realm package to latest beta version --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c2a3f997ce..6af7d82fdb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 9563b73ea645615630ab87daccfc806432f3af9f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 May 2021 14:13:58 +0900 Subject: [PATCH 182/670] Remove unnecessary using statement --- osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index d6ca839485..00180eae60 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Database; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings; From a7f50c5da66be62db20718ed540c5c7821735937 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 May 2021 17:13:25 +0900 Subject: [PATCH 183/670] Revert "Update realm package to latest beta version" This reverts commit 2a87b3d74bdf0bbecaea497d6992fb6760b667d5. --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6af7d82fdb..c2a3f997ce 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 88bdd8a7b767552c16f676e64f25585aecb41ff6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 May 2021 16:01:20 +0900 Subject: [PATCH 184/670] Update some out of date code pieces --- osu.Game/Configuration/OsuConfigManager.cs | 4 ++-- .../Settings/Sections/Online/AlertsAndPrivacySettings.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index e74ae1aeee..1c92c16333 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -61,8 +61,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ShowOnlineExplicitContent, false); - Set(OsuSetting.ChatHighlightName, true); - Set(OsuSetting.ChatMessageNotification, true); + SetDefault(OsuSetting.ChatHighlightName, true); + SetDefault(OsuSetting.ChatMessageNotification, true); // Audio SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs index 0898ce3b84..f9f5b927b7 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs @@ -19,12 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online new SettingsCheckbox { LabelText = "Show a notification popup when someone says your name", - Bindable = config.GetBindable(OsuSetting.ChatHighlightName) + Current = config.GetBindable(OsuSetting.ChatHighlightName) }, new SettingsCheckbox { LabelText = "Show private message notifications", - Bindable = config.GetBindable(OsuSetting.ChatMessageNotification) + Current = config.GetBindable(OsuSetting.ChatMessageNotification) }, }; } From d47370bac93ad869c7505c58bfdf003b133c2745 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 27 May 2021 00:59:29 +0200 Subject: [PATCH 185/670] Locally bind to LocalUser --- osu.Game/Online/Chat/MessageNotifier.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 05ffcb03a2..a8ade8e771 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -35,7 +35,7 @@ namespace osu.Game.Online.Chat private Bindable notifyOnMention; private Bindable notifyOnPM; - private IBindable localUser; + private IBindable localUser = new Bindable(); private readonly BindableList joinedChannels = new BindableList(); [BackgroundDependencyLoader] @@ -43,9 +43,9 @@ namespace osu.Game.Online.Chat { notifyOnMention = config.GetBindable(OsuSetting.ChatHighlightName); notifyOnPM = config.GetBindable(OsuSetting.ChatMessageNotification); - localUser = api.LocalUser; channelManager.JoinedChannels.BindTo(joinedChannels); + api.LocalUser.BindTo(localUser); // Listen for new messages joinedChannels.CollectionChanged += channelsChanged; From cf39e58ce733fdd2288bd88b8157fe3a74decef5 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 27 May 2021 01:00:08 +0200 Subject: [PATCH 186/670] Subscribe to CollectionChanged before binding to JoinedChannels --- osu.Game/Online/Chat/MessageNotifier.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index a8ade8e771..1f3fc0946b 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -43,12 +43,12 @@ namespace osu.Game.Online.Chat { notifyOnMention = config.GetBindable(OsuSetting.ChatHighlightName); notifyOnPM = config.GetBindable(OsuSetting.ChatMessageNotification); - - channelManager.JoinedChannels.BindTo(joinedChannels); api.LocalUser.BindTo(localUser); // Listen for new messages joinedChannels.CollectionChanged += channelsChanged; + + channelManager.JoinedChannels.BindTo(joinedChannels); } private void channelsChanged(object sender, NotifyCollectionChangedEventArgs e) From a679efac1c3b85e036965ca6cf7a8ba2558f9b77 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 27 May 2021 01:00:26 +0200 Subject: [PATCH 187/670] Reduce duplicate notification code by making a base class --- osu.Game/Online/Chat/MessageNotifier.cs | 39 +++++++++---------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 1f3fc0946b..47758673bb 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -142,12 +142,10 @@ namespace osu.Game.Online.Chat /// If the mentions the private static bool isMentioning(string message, string username) => message.IndexOf(username, StringComparison.OrdinalIgnoreCase) != -1 || message.IndexOf(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase) != -1; - public class PrivateMessageNotification : SimpleNotification + public class OpenChannelNotification : SimpleNotification { - public PrivateMessageNotification(string username, Channel channel) + public OpenChannelNotification(Channel channel) { - Icon = FontAwesome.Solid.Envelope; - Text = $"You received a private message from '{username}'. Click to read it!"; this.channel = channel; } @@ -171,32 +169,21 @@ namespace osu.Game.Online.Chat } } - public class MentionNotification : SimpleNotification + public class PrivateMessageNotification : OpenChannelNotification { - public MentionNotification(string username, Channel channel) + public PrivateMessageNotification(string username, Channel channel) : base(channel) + { + Icon = FontAwesome.Solid.Envelope; + Text = $"You received a private message from '{username}'. Click to read it!"; + } + } + + public class MentionNotification : OpenChannelNotification + { + public MentionNotification(string username, Channel channel) : base(channel) { Icon = FontAwesome.Solid.At; Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; - this.channel = channel; - } - - private readonly Channel channel; - - public override bool IsImportant => false; - - [BackgroundDependencyLoader] - private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay, ChannelManager channelManager) - { - IconBackgound.Colour = colours.PurpleDark; - - Activated = delegate - { - notificationOverlay.Hide(); - chatOverlay.Show(); - channelManager.CurrentChannel.Value = channel; - - return true; - }; } } } From 2166ab87c6a5a6d8d19c9b9c24eef6da8eebd0cf Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 27 May 2021 01:47:00 +0200 Subject: [PATCH 188/670] Change base type of tests Fixes missing API property --- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 5e0a3994e1..26b0063178 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -16,7 +16,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Online { - public class TestSceneMessageNotifier : ManualInputManagerTestScene + public class TestSceneMessageNotifier : OsuManualInputManagerTestScene { private User friend; private Channel publicChannel; From 0b17af81f186c2f248307d185fb760ec2688e2bf Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 27 May 2021 09:48:30 +0000 Subject: [PATCH 189/670] Use Contains instead of IndexOf Co-authored-by: Berkan Diler --- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 47758673bb..5b3293f7ee 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -140,7 +140,7 @@ namespace osu.Game.Online.Chat /// Checks if contains , if not, retries making spaces into underscores. /// /// If the mentions the - private static bool isMentioning(string message, string username) => message.IndexOf(username, StringComparison.OrdinalIgnoreCase) != -1 || message.IndexOf(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase) != -1; + private static bool isMentioning(string message, string username) => message.Contains(username, StringComparison.OrdinalIgnoreCase) || message.Contains(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase); public class OpenChannelNotification : SimpleNotification { From 13b2b7c14893d2e150085c32eacc27edbf0f3262 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 27 May 2021 21:58:54 +0200 Subject: [PATCH 190/670] Fix formatting --- osu.Game/Online/Chat/MessageNotifier.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 47758673bb..2e4dc7b0aa 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -35,7 +35,7 @@ namespace osu.Game.Online.Chat private Bindable notifyOnMention; private Bindable notifyOnPM; - private IBindable localUser = new Bindable(); + private readonly IBindable localUser = new Bindable(); private readonly BindableList joinedChannels = new BindableList(); [BackgroundDependencyLoader] @@ -171,7 +171,8 @@ namespace osu.Game.Online.Chat public class PrivateMessageNotification : OpenChannelNotification { - public PrivateMessageNotification(string username, Channel channel) : base(channel) + public PrivateMessageNotification(string username, Channel channel) + : base(channel) { Icon = FontAwesome.Solid.Envelope; Text = $"You received a private message from '{username}'. Click to read it!"; @@ -180,7 +181,8 @@ namespace osu.Game.Online.Chat public class MentionNotification : OpenChannelNotification { - public MentionNotification(string username, Channel channel) : base(channel) + public MentionNotification(string username, Channel channel) + : base(channel) { Icon = FontAwesome.Solid.At; Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; From b746fe7c03c3ca863ddd9825111b3b9b95d9de14 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 5 Jun 2021 11:03:49 +0200 Subject: [PATCH 191/670] Fix binding order --- osu.Game/Online/Chat/MessageNotifier.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 25d6795ffa..c70e678843 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -36,19 +36,18 @@ namespace osu.Game.Online.Chat private Bindable notifyOnMention; private Bindable notifyOnPM; private readonly IBindable localUser = new Bindable(); - private readonly BindableList joinedChannels = new BindableList(); + private readonly IBindableList joinedChannels = new BindableList(); [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { notifyOnMention = config.GetBindable(OsuSetting.ChatHighlightName); notifyOnPM = config.GetBindable(OsuSetting.ChatMessageNotification); - api.LocalUser.BindTo(localUser); + localUser.BindTo(api.LocalUser); // Listen for new messages joinedChannels.CollectionChanged += channelsChanged; - - channelManager.JoinedChannels.BindTo(joinedChannels); + joinedChannels.BindTo(channelManager.JoinedChannels); } private void channelsChanged(object sender, NotifyCollectionChangedEventArgs e) From 6e40af756b71bc9544b000ca310174be17bc8da6 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 5 Jun 2021 11:10:16 +0200 Subject: [PATCH 192/670] Add request handler for dummy API --- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 26b0063178..a1c68d34d1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -28,6 +29,14 @@ namespace osu.Game.Tests.Visual.Online [SetUp] public void Setup() { + // We blindly mark every request as success so that ChannelManager doesn't remove our channel again. + if (API is DummyAPIAccess daa) + { + daa.HandleRequest = (request) => { + return true; + }; + } + friend = new User { Id = 0, Username = "Friend" }; publicChannel = new Channel { Id = 1, Name = "osu" }; privateMessageChannel = new Channel(friend) { Id = 2, Name = friend.Username, Type = ChannelType.PM }; From ce4bcda8032d19d9364cad23b5882536d411886b Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 5 Jun 2021 14:02:48 +0200 Subject: [PATCH 193/670] Use separate method for fetching channel objects Resolves a pull request review --- osu.Game/Online/Chat/MessageNotifier.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index c70e678843..c52a1876b1 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -76,9 +76,18 @@ namespace osu.Game.Online.Chat HandleMessages(messages.First().ChannelId, messages); } + /// + /// Searches for a channel with the matching , returns when none found. + /// + private Channel fetchJoinedChannel(long channelId) + { + return channelManager.JoinedChannels.SingleOrDefault(c => c.Id == channelId); + } + public void HandleMessages(long channelId, IEnumerable messages) { - var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == channelId); + // Fetch channel object + var channel = fetchJoinedChannel(channelId); if (channel == null) { @@ -86,11 +95,6 @@ namespace osu.Game.Online.Chat return; } - HandleMessages(channel, messages); - } - - public void HandleMessages(Channel channel, IEnumerable messages) - { // Only send notifications, if ChatOverlay and the target channel aren't visible. if (chatOverlay?.IsPresent == true && channelManager.CurrentChannel.Value == channel) return; From 5e44329e0b0e7f5221c90e69baed3f73664084cc Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 5 Jun 2021 14:42:16 +0200 Subject: [PATCH 194/670] Add DummyAPIAccess request handler Make CreateChannelRequest.channel public --- .../Visual/Online/TestSceneMessageNotifier.cs | 34 ++++++++++++++++--- .../API/Requests/CreateChannelRequest.cs | 6 ++-- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index a1c68d34d1..fada645632 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.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 NUnit.Framework; using osu.Framework.Allocation; @@ -9,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; 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.Overlays; using osu.Game.Overlays.Notifications; @@ -29,12 +32,9 @@ namespace osu.Game.Tests.Visual.Online [SetUp] public void Setup() { - // We blindly mark every request as success so that ChannelManager doesn't remove our channel again. if (API is DummyAPIAccess daa) { - daa.HandleRequest = (request) => { - return true; - }; + daa.HandleRequest = dummyAPIHandleRequest; } friend = new User { Id = 0, Username = "Friend" }; @@ -52,6 +52,32 @@ namespace osu.Game.Tests.Visual.Online }); } + private bool dummyAPIHandleRequest(APIRequest request) + { + switch (request) + { + case GetMessagesRequest messagesRequest: + messagesRequest.TriggerSuccess(new List(0)); + return true; + + case CreateChannelRequest createChannelRequest: + var apiChatChannel = new APIChatChannel + { + RecentMessages = new List(0), + ChannelID = (int)createChannelRequest.Channel.Id + }; + createChannelRequest.TriggerSuccess(apiChatChannel); + return true; + + case ListChannelsRequest listChannelsRequest: + listChannelsRequest.TriggerSuccess(new List(1) { publicChannel }); + return true; + + default: + return false; + } + } + [Test] public void TestPublicChannelMention() { diff --git a/osu.Game/Online/API/Requests/CreateChannelRequest.cs b/osu.Game/Online/API/Requests/CreateChannelRequest.cs index 42cb201969..041ad26267 100644 --- a/osu.Game/Online/API/Requests/CreateChannelRequest.cs +++ b/osu.Game/Online/API/Requests/CreateChannelRequest.cs @@ -11,11 +11,11 @@ namespace osu.Game.Online.API.Requests { public class CreateChannelRequest : APIRequest { - private readonly Channel channel; + public readonly Channel Channel; public CreateChannelRequest(Channel channel) { - this.channel = channel; + Channel = channel; } protected override WebRequest CreateWebRequest() @@ -24,7 +24,7 @@ namespace osu.Game.Online.API.Requests req.Method = HttpMethod.Post; req.AddParameter("type", $"{ChannelType.PM}"); - req.AddParameter("target_id", $"{channel.Users.First().Id}"); + req.AddParameter("target_id", $"{Channel.Users.First().Id}"); return req; } From 248e90df6d492baebd808188b1fa47f96527f7f7 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 5 Jun 2021 15:55:58 +0200 Subject: [PATCH 195/670] Add more request handling code --- .../Visual/Online/TestSceneMessageNotifier.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index fada645632..28ec3e91a1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -73,6 +73,18 @@ namespace osu.Game.Tests.Visual.Online listChannelsRequest.TriggerSuccess(new List(1) { publicChannel }); return true; + case GetUpdatesRequest updatesRequest: + updatesRequest.TriggerSuccess(new GetUpdatesResponse + { + Messages = new List(0), + Presence = new List(0) + }); + return true; + + case JoinChannelRequest joinChannelRequest: + joinChannelRequest.TriggerSuccess(); + return true; + default: return false; } From 4925a7d59e36f10e3b089287e0700a09362df8ce Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 5 Jun 2021 15:57:14 +0200 Subject: [PATCH 196/670] Minor code quality changes --- osu.Game/Online/Chat/MessageNotifier.cs | 27 ++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index c52a1876b1..53bd3c61c3 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -113,30 +113,47 @@ namespace osu.Game.Online.Chat if (checkForPMs(channel, message)) continue; - // change output to bool again if another "message processor" is added. - checkForMentions(channel, message, localUser.Value.Username); + _ = checkForMentions(channel, message, localUser.Value.Username); } } + /// + /// Checks whether the user enabled private message notifications and whether specified is a direct message. + /// + /// The channel associated to the + /// The message to be checked private bool checkForPMs(Channel channel, Message message) { if (!notifyOnPM.Value || channel.Type != ChannelType.PM) return false; - var notification = new PrivateMessageNotification(message.Sender.Username, channel); + if (channel.Id != message.ChannelId) + throw new ArgumentException("The provided channel doesn't match with the channel id provided by the message parameter.", nameof(channel)); + var notification = new PrivateMessageNotification(message.Sender.Username, channel); notificationOverlay?.Post(notification); return true; } - private void checkForMentions(Channel channel, Message message, string username) + /// + /// Checks whether the user enabled mention notifications and whether specified mentions the provided . + /// + /// The channel associated to the + /// The message to be checked + /// The username that will be checked for + private bool checkForMentions(Channel channel, Message message, string username) { if (!notifyOnMention.Value || !isMentioning(message.Content, username)) - return; + return false; + + if (channel.Id != message.ChannelId) + throw new ArgumentException("The provided channel doesn't match with the channel id provided by the message parameter.", nameof(channel)); var notification = new MentionNotification(message.Sender.Username, channel); notificationOverlay?.Post(notification); + + return true; } /// From b97f31f31414a8cc6c284ce2239d2cdecaf64500 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 5 Jun 2021 19:03:11 +0200 Subject: [PATCH 197/670] Revert deletion of xmldoc summary line --- osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs index 001520b507..c0de093425 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -60,6 +60,7 @@ namespace osu.Game.Overlays.Chat.Tabs /// /// Adds a channel to the ChannelTabControl. + /// The first channel added will automaticly selected. /// /// The channel that is going to be added. public void AddChannel(Channel channel) From c452715bf190e7aaa8aef11612cd0f56e35b5561 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Sun, 6 Jun 2021 01:18:30 -0400 Subject: [PATCH 198/670] Allow skin elements to find closest anchor - Resolves ppy/osu#13252 - Add localisation strings for the context menu instead of using enum --- osu.Game/Localisation/SkinEditorStrings.cs | 74 ++++++ .../Skinning/Editor/SkinSelectionHandler.cs | 215 +++++++++++++++--- 2 files changed, 262 insertions(+), 27 deletions(-) create mode 100644 osu.Game/Localisation/SkinEditorStrings.cs diff --git a/osu.Game/Localisation/SkinEditorStrings.cs b/osu.Game/Localisation/SkinEditorStrings.cs new file mode 100644 index 0000000000..24a077963f --- /dev/null +++ b/osu.Game/Localisation/SkinEditorStrings.cs @@ -0,0 +1,74 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class SkinEditorStrings + { + private const string prefix = "osu.Game.Localisation.SkinEditor"; + + /// + /// "anchor" + /// + public static LocalisableString Anchor => new TranslatableString(getKey("anchor"), "anchor"); + + /// + /// "origin" + /// + public static LocalisableString Origin => new TranslatableString(getKey("origin"), "origin"); + + /// + /// "top-left" + /// + public static LocalisableString TopLeft => new TranslatableString(getKey("top_left"), "top-left"); + + /// + /// "top-centre" + /// + public static LocalisableString TopCentre => new TranslatableString(getKey("top_centre"), "top-centre"); + + /// + /// "top-right" + /// + public static LocalisableString TopRight => new TranslatableString(getKey("top_right"), "top-right"); + + /// + /// "centre-left" + /// + public static LocalisableString CentreLeft => new TranslatableString(getKey("centre_left"), "centre-left"); + + /// + /// "centre" + /// + public static LocalisableString Centre => new TranslatableString(getKey("centre"), "centre"); + + /// + /// "centre-right" + /// + public static LocalisableString CentreRight => new TranslatableString(getKey("centre_right"), "centre-right"); + + /// + /// "bottom-left" + /// + public static LocalisableString BottomLeft => new TranslatableString(getKey("bottom_left"), "bottom-left"); + + /// + /// "bottom-centre" + /// + public static LocalisableString BottomCentre => new TranslatableString(getKey("bottom_centre"), "bottom-centre"); + + /// + /// "bottom-right" + /// + public static LocalisableString BottomRight => new TranslatableString(getKey("bottom_right"), "bottom-right"); + + /// + /// "closest" + /// + public static LocalisableString Closest => new TranslatableString(getKey("closest"), "closest"); + + private static string getKey(string key) => $"{prefix}:{key}"; + } +} diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 9cca0ba2c7..956f6c79f9 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -4,22 +4,152 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using Humanizer; +using osu.Game.Localisation; namespace osu.Game.Skinning.Editor { public class SkinSelectionHandler : SelectionHandler { + /// + ///

Keeps track of whether a is using the closest anchor point within its parent, + /// or whether the user is overriding its anchor point.

+ ///

Each key is either a direct cast of an Anchor value, or it is equal to . This is done + /// because the "Closest" menu item is not a valid anchor, so something other than an anchor must be used.

+ ///

Each value is a . If the is , the user has + /// overridden the anchor point. + /// If , the closest anchor point is assigned to the Drawable when it is either dragged by the user via , or when "Closest" is assigned from + /// the anchor context menu via .

+ ///
+ /// + ///

A ConditionalWeakTable is preferable to a Dictionary because a Dictionary will keep + /// orphaned references to a Drawable forever, unless manually pruned

+ ///

is used as a thin wrapper around bool because ConditionalWeakTable requires a reference type as both a key and a value.

+ ///
+ private readonly ConditionalWeakTable isDrawableUsingClosestAnchorLookup = new ConditionalWeakTable(); + + /// + /// The hash code of the "Closest" menu item in the anchor point context menu. + /// + /// This does not need to change with locale; it need only be constant and distinct from any values. + private static readonly int hash_of_closest_anchor_menu_item = @"Closest".GetHashCode(); + + /// Used by to populate and . + private static readonly LocalisableString[] unbound_anchor_menu_items = + { + SkinEditorStrings.Closest, + SkinEditorStrings.TopLeft, + SkinEditorStrings.TopCentre, + SkinEditorStrings.TopRight, + SkinEditorStrings.CentreLeft, + SkinEditorStrings.Centre, + SkinEditorStrings.CentreRight, + SkinEditorStrings.BottomLeft, + SkinEditorStrings.BottomCentre, + SkinEditorStrings.BottomRight, + }; + + /// Used by to populate and . + private static readonly int[] anchor_menu_hashes = + new[] + { + Anchor.TopLeft, + Anchor.TopCentre, + Anchor.TopRight, + Anchor.CentreLeft, + Anchor.Centre, + Anchor.CentreRight, + Anchor.BottomLeft, + Anchor.BottomCentre, + Anchor.BottomRight, + } + .Cast() + .Prepend(hash_of_closest_anchor_menu_item) + .ToArray(); + + private Dictionary localisedAnchorMenuItems; + private Dictionary localisedOriginMenuItems; + private ILocalisedBindableString localisedAnchor; + private ILocalisedBindableString localisedOrigin; + + [BackgroundDependencyLoader] + private void load(LocalisationManager localisation) + { + localisedAnchor = localisation.GetLocalisedString(SkinEditorStrings.Anchor); + localisedOrigin = localisation.GetLocalisedString(SkinEditorStrings.Origin); + + var boundAnchorMenuItems = unbound_anchor_menu_items.Select(localisation.GetLocalisedString).ToArray(); + + var anchorPairs = anchor_menu_hashes.Zip(boundAnchorMenuItems, (k, v) => new KeyValuePair(k, v)).ToArray(); + localisedAnchorMenuItems = new Dictionary(anchorPairs); + var originPairs = anchorPairs.Where(pair => pair.Key != hash_of_closest_anchor_menu_item); + localisedOriginMenuItems = new Dictionary(originPairs); + } + + private Anchor getClosestAnchorForDrawable(Drawable drawable) + { + var parent = drawable.Parent; + if (parent == null) + return drawable.Anchor; + + // If there is a better way to get this information, let me know. Was taken from LogoTrackingContainer.ComputeLogoTrackingPosition + // I tried a lot of different things, such as just using Position / ChildSize, but none of them worked properly. + var screenPosition = getOriginPositionFromQuad(drawable.ScreenSpaceDrawQuad, drawable.Origin); + var absolutePosition = parent.ToLocalSpace(screenPosition); + var factor = parent.RelativeToAbsoluteFactor; + + var result = default(Anchor); + + static Anchor getTieredComponent(float component, Anchor tier0, Anchor tier1, Anchor tier2) => + component >= 2 / 3f + ? tier2 + : component >= 1 / 3f + ? tier1 + : tier0; + + result |= getTieredComponent(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2); + result |= getTieredComponent(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2); + + return result; + } + + private Vector2 getOriginPositionFromQuad(in Quad quad, Anchor origin) + { + var result = quad.TopLeft; + + if (origin.HasFlagFast(Anchor.x2)) + result.X += quad.Width; + else if (origin.HasFlagFast(Anchor.x1)) + result.X += quad.Width / 2f; + + if (origin.HasFlagFast(Anchor.y2)) + result.Y += quad.Height; + else if (origin.HasFlagFast(Anchor.y1)) + result.Y += quad.Height / 2f; + + return result; + } + + /// Defaults to , meaning anchors are closest by default. + private BindableBool isDrawableUsingClosestAnchor(Drawable drawable) => isDrawableUsingClosestAnchorLookup.GetValue(drawable, _ => new BindableBool(true)); + + // There may be a more generalised form of this somewhere in the codebase. If so, use that. + private static string getSentenceCaseLocalisedString(ILocalisedBindableString ls) => ls.Value.Transform(To.SentenceCase); + [Resolved] private SkinEditor skinEditor { get; set; } @@ -151,11 +281,24 @@ namespace osu.Game.Skinning.Editor { Drawable drawable = (Drawable)c.Item; drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); + + updateDrawableAnchorIfUsingClosest(drawable); } return true; } + private void updateDrawableAnchorIfUsingClosest(Drawable drawable) + { + if (!isDrawableUsingClosestAnchor(drawable).Value) return; + + var closestAnchor = getClosestAnchorForDrawable(drawable); + + if (closestAnchor == drawable.Anchor) return; + + updateDrawableAnchor(drawable, closestAnchor); + } + protected override void OnSelectionChanged() { base.OnSelectionChanged(); @@ -171,42 +314,35 @@ namespace osu.Game.Skinning.Editor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { - yield return new OsuMenuItem("Anchor") + int checkAnchor(Drawable drawable) => + isDrawableUsingClosestAnchor(drawable).Value + ? hash_of_closest_anchor_menu_item + : (int)drawable.Anchor; + + yield return new OsuMenuItem(getSentenceCaseLocalisedString(localisedAnchor)) { - Items = createAnchorItems(d => d.Anchor, applyAnchor).ToArray() + Items = createAnchorItems(localisedAnchorMenuItems, checkAnchor, applyAnchor).ToArray() }; - yield return new OsuMenuItem("Origin") + yield return new OsuMenuItem(getSentenceCaseLocalisedString(localisedOrigin)) { - Items = createAnchorItems(d => d.Origin, applyOrigin).ToArray() + // Origins can't be "closest" so we just cast to int + Items = createAnchorItems(localisedOriginMenuItems, d => (int)d.Origin, applyOrigin).ToArray() }; foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - IEnumerable createAnchorItems(Func checkFunction, Action applyFunction) - { - var displayableAnchors = new[] + IEnumerable createAnchorItems(IDictionary items, Func checkFunction, Action applyFunction) => + items.Select(pair => { - Anchor.TopLeft, - Anchor.TopCentre, - Anchor.TopRight, - Anchor.CentreLeft, - Anchor.Centre, - Anchor.CentreRight, - Anchor.BottomLeft, - Anchor.BottomCentre, - Anchor.BottomRight, - }; + var (hash, ls) = pair; - return displayableAnchors.Select(a => - { - return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a)) + return new TernaryStateRadioMenuItem(getSentenceCaseLocalisedString(ls), MenuItemType.Standard, _ => applyFunction(hash)) { - State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == a) } + State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == hash) } }; }); - } } private static void updateDrawablePosition(Drawable drawable, Vector2 screenSpacePosition) @@ -215,8 +351,10 @@ namespace osu.Game.Skinning.Editor drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition; } - private void applyOrigin(Anchor anchor) + private void applyOrigin(int hash) { + var anchor = (Anchor)hash; + foreach (var item in SelectedItems) { var drawable = (Drawable)item; @@ -224,6 +362,8 @@ namespace osu.Game.Skinning.Editor var previousOrigin = drawable.OriginPosition; drawable.Origin = anchor; drawable.Position += drawable.OriginPosition - previousOrigin; + + updateDrawableAnchorIfUsingClosest(drawable); } } @@ -234,18 +374,39 @@ namespace osu.Game.Skinning.Editor private Quad getSelectionQuad() => GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); - private void applyAnchor(Anchor anchor) + private void applyAnchor(int hash) { foreach (var item in SelectedItems) { var drawable = (Drawable)item; - var previousAnchor = drawable.AnchorPosition; - drawable.Anchor = anchor; - drawable.Position -= drawable.AnchorPosition - previousAnchor; + var anchor = getAnchorFromHashAndDrawableAndRecordWhetherUsingClosestAnchor(hash, drawable); + + updateDrawableAnchor(drawable, anchor); } } + private static void updateDrawableAnchor(Drawable drawable, Anchor anchor) + { + var previousAnchor = drawable.AnchorPosition; + drawable.Anchor = anchor; + drawable.Position -= drawable.AnchorPosition - previousAnchor; + } + + private Anchor getAnchorFromHashAndDrawableAndRecordWhetherUsingClosestAnchor(int hash, Drawable drawable) + { + var isUsingClosestAnchor = isDrawableUsingClosestAnchor(drawable); + + if (hash == hash_of_closest_anchor_menu_item) + { + isUsingClosestAnchor.Value = true; + return getClosestAnchorForDrawable(drawable); + } + + isUsingClosestAnchor.Value = false; + return (Anchor)hash; + } + private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) { // cancel out scale in axes we don't care about (based on which drag handle was used). From 4aee76456f8cdb8bade6d037d1a9bb02af125ee8 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Sun, 6 Jun 2021 05:34:32 -0400 Subject: [PATCH 199/670] Replace localised strings with static English --- osu.Game/Localisation/SkinEditorStrings.cs | 74 ------------- .../Skinning/Editor/SkinSelectionHandler.cs | 101 +++++++----------- 2 files changed, 36 insertions(+), 139 deletions(-) delete mode 100644 osu.Game/Localisation/SkinEditorStrings.cs diff --git a/osu.Game/Localisation/SkinEditorStrings.cs b/osu.Game/Localisation/SkinEditorStrings.cs deleted file mode 100644 index 24a077963f..0000000000 --- a/osu.Game/Localisation/SkinEditorStrings.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Localisation; - -namespace osu.Game.Localisation -{ - public static class SkinEditorStrings - { - private const string prefix = "osu.Game.Localisation.SkinEditor"; - - /// - /// "anchor" - /// - public static LocalisableString Anchor => new TranslatableString(getKey("anchor"), "anchor"); - - /// - /// "origin" - /// - public static LocalisableString Origin => new TranslatableString(getKey("origin"), "origin"); - - /// - /// "top-left" - /// - public static LocalisableString TopLeft => new TranslatableString(getKey("top_left"), "top-left"); - - /// - /// "top-centre" - /// - public static LocalisableString TopCentre => new TranslatableString(getKey("top_centre"), "top-centre"); - - /// - /// "top-right" - /// - public static LocalisableString TopRight => new TranslatableString(getKey("top_right"), "top-right"); - - /// - /// "centre-left" - /// - public static LocalisableString CentreLeft => new TranslatableString(getKey("centre_left"), "centre-left"); - - /// - /// "centre" - /// - public static LocalisableString Centre => new TranslatableString(getKey("centre"), "centre"); - - /// - /// "centre-right" - /// - public static LocalisableString CentreRight => new TranslatableString(getKey("centre_right"), "centre-right"); - - /// - /// "bottom-left" - /// - public static LocalisableString BottomLeft => new TranslatableString(getKey("bottom_left"), "bottom-left"); - - /// - /// "bottom-centre" - /// - public static LocalisableString BottomCentre => new TranslatableString(getKey("bottom_centre"), "bottom-centre"); - - /// - /// "bottom-right" - /// - public static LocalisableString BottomRight => new TranslatableString(getKey("bottom_right"), "bottom-right"); - - /// - /// "closest" - /// - public static LocalisableString Closest => new TranslatableString(getKey("closest"), "closest"); - - private static string getKey(string key) => $"{prefix}:{key}"; - } -} diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 956f6c79f9..fbe7cc6d91 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -11,15 +11,12 @@ using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; -using Humanizer; -using osu.Game.Localisation; namespace osu.Game.Skinning.Editor { @@ -28,7 +25,7 @@ namespace osu.Game.Skinning.Editor /// ///

Keeps track of whether a is using the closest anchor point within its parent, /// or whether the user is overriding its anchor point.

- ///

Each key is either a direct cast of an Anchor value, or it is equal to . This is done + ///

Each key is either a direct cast of an Anchor value, or it is equal to . This is done /// because the "Closest" menu item is not a valid anchor, so something other than an anchor must be used.

///

Each value is a . If the is , the user has /// overridden the anchor point. @@ -42,62 +39,39 @@ namespace osu.Game.Skinning.Editor /// private readonly ConditionalWeakTable isDrawableUsingClosestAnchorLookup = new ConditionalWeakTable(); + private const string closest_text = @"Closest"; + ///

/// The hash code of the "Closest" menu item in the anchor point context menu. /// - /// This does not need to change with locale; it need only be constant and distinct from any values. - private static readonly int hash_of_closest_anchor_menu_item = @"Closest".GetHashCode(); + /// Needs only be constant and distinct from any values. + private static readonly int closest_text_hash = closest_text.GetHashCode(); - /// Used by to populate and . - private static readonly LocalisableString[] unbound_anchor_menu_items = + private static readonly Dictionary anchor_menu_items; + private static readonly Dictionary origin_menu_items; + + static SkinSelectionHandler() { - SkinEditorStrings.Closest, - SkinEditorStrings.TopLeft, - SkinEditorStrings.TopCentre, - SkinEditorStrings.TopRight, - SkinEditorStrings.CentreLeft, - SkinEditorStrings.Centre, - SkinEditorStrings.CentreRight, - SkinEditorStrings.BottomLeft, - SkinEditorStrings.BottomCentre, - SkinEditorStrings.BottomRight, - }; + var anchorMenuSubset = new[] + { + Anchor.TopLeft, + Anchor.TopCentre, + Anchor.TopRight, + Anchor.CentreLeft, + Anchor.Centre, + Anchor.CentreRight, + Anchor.BottomLeft, + Anchor.BottomCentre, + Anchor.BottomRight, + }; - /// Used by to populate and . - private static readonly int[] anchor_menu_hashes = - new[] - { - Anchor.TopLeft, - Anchor.TopCentre, - Anchor.TopRight, - Anchor.CentreLeft, - Anchor.Centre, - Anchor.CentreRight, - Anchor.BottomLeft, - Anchor.BottomCentre, - Anchor.BottomRight, - } - .Cast() - .Prepend(hash_of_closest_anchor_menu_item) - .ToArray(); + var texts = anchorMenuSubset.Select(a => a.ToString()).Prepend(closest_text).ToArray(); + var hashes = anchorMenuSubset.Cast().Prepend(closest_text_hash).ToArray(); - private Dictionary localisedAnchorMenuItems; - private Dictionary localisedOriginMenuItems; - private ILocalisedBindableString localisedAnchor; - private ILocalisedBindableString localisedOrigin; - - [BackgroundDependencyLoader] - private void load(LocalisationManager localisation) - { - localisedAnchor = localisation.GetLocalisedString(SkinEditorStrings.Anchor); - localisedOrigin = localisation.GetLocalisedString(SkinEditorStrings.Origin); - - var boundAnchorMenuItems = unbound_anchor_menu_items.Select(localisation.GetLocalisedString).ToArray(); - - var anchorPairs = anchor_menu_hashes.Zip(boundAnchorMenuItems, (k, v) => new KeyValuePair(k, v)).ToArray(); - localisedAnchorMenuItems = new Dictionary(anchorPairs); - var originPairs = anchorPairs.Where(pair => pair.Key != hash_of_closest_anchor_menu_item); - localisedOriginMenuItems = new Dictionary(originPairs); + var anchorPairs = hashes.Zip(texts, (k, v) => new KeyValuePair(k, v)).ToArray(); + anchor_menu_items = new Dictionary(anchorPairs); + var originPairs = anchorPairs.Where(pair => pair.Key != closest_text_hash); + origin_menu_items = new Dictionary(originPairs); } private Anchor getClosestAnchorForDrawable(Drawable drawable) @@ -147,9 +121,6 @@ namespace osu.Game.Skinning.Editor /// Defaults to , meaning anchors are closest by default. private BindableBool isDrawableUsingClosestAnchor(Drawable drawable) => isDrawableUsingClosestAnchorLookup.GetValue(drawable, _ => new BindableBool(true)); - // There may be a more generalised form of this somewhere in the codebase. If so, use that. - private static string getSentenceCaseLocalisedString(ILocalisedBindableString ls) => ls.Value.Transform(To.SentenceCase); - [Resolved] private SkinEditor skinEditor { get; set; } @@ -316,29 +287,29 @@ namespace osu.Game.Skinning.Editor { int checkAnchor(Drawable drawable) => isDrawableUsingClosestAnchor(drawable).Value - ? hash_of_closest_anchor_menu_item + ? closest_text_hash : (int)drawable.Anchor; - yield return new OsuMenuItem(getSentenceCaseLocalisedString(localisedAnchor)) + yield return new OsuMenuItem(nameof(Anchor)) { - Items = createAnchorItems(localisedAnchorMenuItems, checkAnchor, applyAnchor).ToArray() + Items = createAnchorItems(anchor_menu_items, checkAnchor, applyAnchor).ToArray() }; - yield return new OsuMenuItem(getSentenceCaseLocalisedString(localisedOrigin)) + yield return new OsuMenuItem(nameof(Origin)) { // Origins can't be "closest" so we just cast to int - Items = createAnchorItems(localisedOriginMenuItems, d => (int)d.Origin, applyOrigin).ToArray() + Items = createAnchorItems(origin_menu_items, d => (int)d.Origin, applyOrigin).ToArray() }; foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - IEnumerable createAnchorItems(IDictionary items, Func checkFunction, Action applyFunction) => + IEnumerable createAnchorItems(IDictionary items, Func checkFunction, Action applyFunction) => items.Select(pair => { - var (hash, ls) = pair; + var (hash, text) = pair; - return new TernaryStateRadioMenuItem(getSentenceCaseLocalisedString(ls), MenuItemType.Standard, _ => applyFunction(hash)) + return new TernaryStateRadioMenuItem(text, MenuItemType.Standard, _ => applyFunction(hash)) { State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == hash) } }; @@ -397,7 +368,7 @@ namespace osu.Game.Skinning.Editor { var isUsingClosestAnchor = isDrawableUsingClosestAnchor(drawable); - if (hash == hash_of_closest_anchor_menu_item) + if (hash == closest_text_hash) { isUsingClosestAnchor.Value = true; return getClosestAnchorForDrawable(drawable); From c9f5808bf2cba4966f9fb5a5e1347a9a31e13bf5 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Sun, 6 Jun 2021 06:58:21 -0400 Subject: [PATCH 200/670] Move lookup logic to DrawableExtensions This is now a global lookup to be shared by serialization and editor. --- osu.Game/Extensions/DrawableExtensions.cs | 25 +++++++++++++++++ .../Skinning/Editor/SkinSelectionHandler.cs | 28 ++----------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 2ac6e6ff22..febad0d739 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -2,11 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Threading; using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Extensions @@ -63,5 +67,26 @@ namespace osu.Game.Extensions container.Add(child.CreateInstance()); } } + + /// + ///

A ConditionalWeakTable is preferable to a Dictionary because a Dictionary will keep + /// orphaned references to an forever, unless manually pruned.

+ ///

is used as a thin wrapper around bool because ConditionalWeakTable requires a reference type as both a key and a value.

+ ///

was chosen over because it is a common ancestor between (which is required for logic) + /// and (which is required for serialization via ).

+ ///

This collection is thread-safe according to the + /// documentation, + /// but the BindableBools are not unless leased.

+ ///
+ private static readonly ConditionalWeakTable is_drawable_using_closest_anchor_lookup = new ConditionalWeakTable(); + + /// + /// Gets or creates a representing whether is using the closest anchor point within its + /// parent. + /// + /// A whose is if the is using the closest anchor point, + /// otherwise . + public static BindableBool IsUsingClosestAnchor(this IDrawable drawable) => + is_drawable_using_closest_anchor_lookup.GetValue(drawable, _ => new BindableBool(true)); } } diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index fbe7cc6d91..5619a7ba19 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -4,9 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; @@ -22,23 +20,6 @@ namespace osu.Game.Skinning.Editor { public class SkinSelectionHandler : SelectionHandler { - /// - ///

Keeps track of whether a is using the closest anchor point within its parent, - /// or whether the user is overriding its anchor point.

- ///

Each key is either a direct cast of an Anchor value, or it is equal to . This is done - /// because the "Closest" menu item is not a valid anchor, so something other than an anchor must be used.

- ///

Each value is a . If the is , the user has - /// overridden the anchor point. - /// If , the closest anchor point is assigned to the Drawable when it is either dragged by the user via , or when "Closest" is assigned from - /// the anchor context menu via .

- ///
- /// - ///

A ConditionalWeakTable is preferable to a Dictionary because a Dictionary will keep - /// orphaned references to a Drawable forever, unless manually pruned

- ///

is used as a thin wrapper around bool because ConditionalWeakTable requires a reference type as both a key and a value.

- ///
- private readonly ConditionalWeakTable isDrawableUsingClosestAnchorLookup = new ConditionalWeakTable(); - private const string closest_text = @"Closest"; /// @@ -118,9 +99,6 @@ namespace osu.Game.Skinning.Editor return result; } - /// Defaults to , meaning anchors are closest by default. - private BindableBool isDrawableUsingClosestAnchor(Drawable drawable) => isDrawableUsingClosestAnchorLookup.GetValue(drawable, _ => new BindableBool(true)); - [Resolved] private SkinEditor skinEditor { get; set; } @@ -261,7 +239,7 @@ namespace osu.Game.Skinning.Editor private void updateDrawableAnchorIfUsingClosest(Drawable drawable) { - if (!isDrawableUsingClosestAnchor(drawable).Value) return; + if (!drawable.IsUsingClosestAnchor().Value) return; var closestAnchor = getClosestAnchorForDrawable(drawable); @@ -286,7 +264,7 @@ namespace osu.Game.Skinning.Editor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { int checkAnchor(Drawable drawable) => - isDrawableUsingClosestAnchor(drawable).Value + drawable.IsUsingClosestAnchor().Value ? closest_text_hash : (int)drawable.Anchor; @@ -366,7 +344,7 @@ namespace osu.Game.Skinning.Editor private Anchor getAnchorFromHashAndDrawableAndRecordWhetherUsingClosestAnchor(int hash, Drawable drawable) { - var isUsingClosestAnchor = isDrawableUsingClosestAnchor(drawable); + var isUsingClosestAnchor = drawable.IsUsingClosestAnchor(); if (hash == closest_text_hash) { From 11b1b8c633a3f1f04decb76c5ea49050ad8931ee Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Sun, 6 Jun 2021 07:18:08 -0400 Subject: [PATCH 201/670] Add serialization support via SkinnableInfo --- osu.Game/Extensions/DrawableExtensions.cs | 1 + osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index febad0d739..d8c627ea8e 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -60,6 +60,7 @@ namespace osu.Game.Extensions component.Scale = info.Scale; component.Anchor = info.Anchor; component.Origin = info.Origin; + component.IsUsingClosestAnchor().Value = !info.IsNotUsingClosestAnchor; if (component is Container container) { diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index e08044b14c..3e829f6d38 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -32,6 +32,13 @@ namespace osu.Game.Screens.Play.HUD public Anchor Origin { get; set; } + /// + /// if this 's is + /// automatically determined by proximity, if the user has overridden it. + /// + /// Stored this way because default(bool) is and we want the default behaviour to be "closest". + public bool IsNotUsingClosestAnchor { get; set; } + public List Children { get; } = new List(); [JsonConstructor] @@ -52,6 +59,7 @@ namespace osu.Game.Screens.Play.HUD Scale = component.Scale; Anchor = component.Anchor; Origin = component.Origin; + IsNotUsingClosestAnchor = !component.IsUsingClosestAnchor().Value; if (component is Container container) { From 63346f6b756521bdac240777bcb26716cb2106ae Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Sun, 6 Jun 2021 09:40:58 -0400 Subject: [PATCH 202/670] Refactor getTieredComponent --- .../Skinning/Editor/SkinSelectionHandler.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 5619a7ba19..b1d353b0af 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -69,19 +69,23 @@ namespace osu.Game.Skinning.Editor var result = default(Anchor); - static Anchor getTieredComponent(float component, Anchor tier0, Anchor tier1, Anchor tier2) => - component >= 2 / 3f - ? tier2 - : component >= 1 / 3f - ? tier1 - : tier0; - result |= getTieredComponent(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2); result |= getTieredComponent(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2); return result; } + private static Anchor getTieredComponent(float component, Anchor tier0, Anchor tier1, Anchor tier2) + { + if (component >= 2 / 3f) + return tier2; + + if (component >= 1 / 3f) + return tier1; + + return tier0; + } + private Vector2 getOriginPositionFromQuad(in Quad quad, Anchor origin) { var result = quad.TopLeft; From da1c38d5a907ae2e4122b4d133f38a60949d0435 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Sun, 6 Jun 2021 10:10:55 -0400 Subject: [PATCH 203/670] Uninvert logic of SkinnableInfo.UsingClosestAnchor Also rename "IsUsingClosestAnchor" to simply "UsingClosestAnchor". --- osu.Game/Extensions/DrawableExtensions.cs | 4 ++-- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 10 +++++----- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index d8c627ea8e..f66fe2b8b2 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -60,7 +60,7 @@ namespace osu.Game.Extensions component.Scale = info.Scale; component.Anchor = info.Anchor; component.Origin = info.Origin; - component.IsUsingClosestAnchor().Value = !info.IsNotUsingClosestAnchor; + component.UsingClosestAnchor().Value = info.UsingClosestAnchor; if (component is Container container) { @@ -87,7 +87,7 @@ namespace osu.Game.Extensions /// /// A whose is if the is using the closest anchor point, /// otherwise . - public static BindableBool IsUsingClosestAnchor(this IDrawable drawable) => + public static BindableBool UsingClosestAnchor(this IDrawable drawable) => is_drawable_using_closest_anchor_lookup.GetValue(drawable, _ => new BindableBool(true)); } } diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 3e829f6d38..8ca5f1ba2d 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -33,11 +33,11 @@ namespace osu.Game.Screens.Play.HUD public Anchor Origin { get; set; } /// - /// if this 's is - /// automatically determined by proximity, if the user has overridden it. + ///

if this 's is + /// automatically determined by proximity, if the user has overridden it.

+ ///

Corresponds to at runtime.

///
- /// Stored this way because default(bool) is and we want the default behaviour to be "closest". - public bool IsNotUsingClosestAnchor { get; set; } + public bool UsingClosestAnchor { get; set; } = true; public List Children { get; } = new List(); @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Play.HUD Scale = component.Scale; Anchor = component.Anchor; Origin = component.Origin; - IsNotUsingClosestAnchor = !component.IsUsingClosestAnchor().Value; + UsingClosestAnchor = component.UsingClosestAnchor().Value; if (component is Container container) { diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index b1d353b0af..f8d41d94fa 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -243,7 +243,7 @@ namespace osu.Game.Skinning.Editor private void updateDrawableAnchorIfUsingClosest(Drawable drawable) { - if (!drawable.IsUsingClosestAnchor().Value) return; + if (!drawable.UsingClosestAnchor().Value) return; var closestAnchor = getClosestAnchorForDrawable(drawable); @@ -268,7 +268,7 @@ namespace osu.Game.Skinning.Editor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { int checkAnchor(Drawable drawable) => - drawable.IsUsingClosestAnchor().Value + drawable.UsingClosestAnchor().Value ? closest_text_hash : (int)drawable.Anchor; @@ -348,7 +348,7 @@ namespace osu.Game.Skinning.Editor private Anchor getAnchorFromHashAndDrawableAndRecordWhetherUsingClosestAnchor(int hash, Drawable drawable) { - var isUsingClosestAnchor = drawable.IsUsingClosestAnchor(); + var isUsingClosestAnchor = drawable.UsingClosestAnchor(); if (hash == closest_text_hash) { From 888882ac63234f5372675218ea8449385f06bc33 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Sun, 6 Jun 2021 13:27:13 -0400 Subject: [PATCH 204/670] Remove first-person comment --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index f8d41d94fa..e07d4e351b 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -61,8 +61,6 @@ namespace osu.Game.Skinning.Editor if (parent == null) return drawable.Anchor; - // If there is a better way to get this information, let me know. Was taken from LogoTrackingContainer.ComputeLogoTrackingPosition - // I tried a lot of different things, such as just using Position / ChildSize, but none of them worked properly. var screenPosition = getOriginPositionFromQuad(drawable.ScreenSpaceDrawQuad, drawable.Origin); var absolutePosition = parent.ToLocalSpace(screenPosition); var factor = parent.RelativeToAbsoluteFactor; From 6a456e53f40a6a5f1b00d41957b0ba7b56907828 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Sun, 6 Jun 2021 13:28:17 -0400 Subject: [PATCH 205/670] Rename overly long method --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index e07d4e351b..5acc949182 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -331,7 +331,7 @@ namespace osu.Game.Skinning.Editor { var drawable = (Drawable)item; - var anchor = getAnchorFromHashAndDrawableAndRecordWhetherUsingClosestAnchor(hash, drawable); + var anchor = mapHashToAnchorSettings(hash, drawable); updateDrawableAnchor(drawable, anchor); } @@ -344,7 +344,7 @@ namespace osu.Game.Skinning.Editor drawable.Position -= drawable.AnchorPosition - previousAnchor; } - private Anchor getAnchorFromHashAndDrawableAndRecordWhetherUsingClosestAnchor(int hash, Drawable drawable) + private Anchor mapHashToAnchorSettings(int hash, Drawable drawable) { var isUsingClosestAnchor = drawable.UsingClosestAnchor(); From ce635af83edf43645cba2c6c6fcbe723faebe98a Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Sun, 6 Jun 2021 23:47:47 -0400 Subject: [PATCH 206/670] Add UsingClosestAnchor to ISkinnableDrawable Also implement it as an auto property in its inheritors. The auto properties default to true. --- osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs | 2 ++ osu.Game/Screens/Play/HUD/DefaultComboCounter.cs | 2 ++ osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs | 2 ++ osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs | 2 ++ osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs | 2 ++ osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 2 ++ osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 6 +----- osu.Game/Screens/Play/SongProgress.cs | 2 ++ osu.Game/Skinning/ISkinnableDrawable.cs | 6 ++++++ osu.Game/Skinning/LegacyAccuracyCounter.cs | 2 ++ osu.Game/Skinning/LegacyHealthDisplay.cs | 2 ++ osu.Game/Skinning/LegacyScoreCounter.cs | 2 ++ osu.Game/Skinning/SkinnableTargetComponentsContainer.cs | 2 ++ 13 files changed, 29 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index 45ba05e036..5c6bb7299a 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -12,6 +12,8 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } + public bool UsingClosestAnchor { get; set; } = true; + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index c4575c5ad0..97f33c0e07 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -17,6 +17,8 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } + public bool UsingClosestAnchor { get; set; } = true; + public DefaultComboCounter() { Current.Value = DisplayedCount = 0; diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index ed297f0ffc..82f014d1e3 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -72,6 +72,8 @@ namespace osu.Game.Screens.Play.HUD } } + public bool UsingClosestAnchor { get; set; } = true; + public DefaultHealthDisplay() { Size = new Vector2(1, 5); diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index 16e3642181..56eaab4408 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } + public bool UsingClosestAnchor { get; set; } = true; + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs index b0f9928b13..f263b5accc 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs @@ -22,6 +22,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [Resolved] private OsuColour colours { get; set; } + public bool UsingClosestAnchor { get; set; } = true; + [BackgroundDependencyLoader(true)] private void load(DrawableRuleset drawableRuleset) { diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 1737634e31..9640bf1d4d 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -59,6 +59,8 @@ namespace osu.Game.Screens.Play.HUD set => counterContainer.Alpha = value ? 1 : 0; } + public bool UsingClosestAnchor { get; set; } = true; + public LegacyComboCounter() { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 8ca5f1ba2d..5c733a9bc1 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -32,11 +32,7 @@ namespace osu.Game.Screens.Play.HUD public Anchor Origin { get; set; } - /// - ///

if this 's is - /// automatically determined by proximity, if the user has overridden it.

- ///

Corresponds to at runtime.

- ///
+ /// public bool UsingClosestAnchor { get; set; } = true; public List Children { get; } = new List(); diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index cab44c7473..e8687b9ab1 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -78,6 +78,8 @@ namespace osu.Game.Screens.Play private IClock referenceClock; + public bool UsingClosestAnchor { get; set; } = true; + public SongProgress() { RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Skinning/ISkinnableDrawable.cs b/osu.Game/Skinning/ISkinnableDrawable.cs index d42b6f71b0..a0c7ae43a2 100644 --- a/osu.Game/Skinning/ISkinnableDrawable.cs +++ b/osu.Game/Skinning/ISkinnableDrawable.cs @@ -14,5 +14,11 @@ namespace osu.Game.Skinning /// Whether this component should be editable by an end user. ///
bool IsEditable => true; + + /// + /// if this 's is automatically determined by proximity, + /// if the user has overridden it. + /// + bool UsingClosestAnchor { get; set; } } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 16562d9571..6c75ade365 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -12,6 +12,8 @@ namespace osu.Game.Skinning { public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable { + public bool UsingClosestAnchor { get; set; } = true; + public LegacyAccuracyCounter() { Anchor = Anchor.TopRight; diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index c601adc3a0..d70d672189 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -30,6 +30,8 @@ namespace osu.Game.Skinning private bool isNewStyle; + public bool UsingClosestAnchor { get; set; } = true; + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 64ea03d59c..944e42ed0e 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -13,6 +13,8 @@ namespace osu.Game.Skinning protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; + public bool UsingClosestAnchor { get; set; } = true; + public LegacyScoreCounter() : base(6) { diff --git a/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs b/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs index 2107ca7a8b..6acd917991 100644 --- a/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs @@ -17,6 +17,8 @@ namespace osu.Game.Skinning { public bool IsEditable => false; + public bool UsingClosestAnchor { get; set; } = true; + private readonly Action applyDefaults; /// From f28916e30fafab0ac8148cc795bce2d0ad891cf8 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Mon, 7 Jun 2021 00:04:53 -0400 Subject: [PATCH 207/670] Remove all UsingClosestAnchor() extension logic It is replaced with ISkinnableDrawable.UsingClosestAnchor. --- osu.Game/Extensions/DrawableExtensions.cs | 28 ++----------------- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 5 +++- .../Skinning/Editor/SkinSelectionHandler.cs | 26 ++++++----------- 3 files changed, 15 insertions(+), 44 deletions(-) diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index f66fe2b8b2..1199db587a 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -2,9 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; @@ -60,7 +57,9 @@ namespace osu.Game.Extensions component.Scale = info.Scale; component.Anchor = info.Anchor; component.Origin = info.Origin; - component.UsingClosestAnchor().Value = info.UsingClosestAnchor; + + if (component is ISkinnableDrawable skinnable) + skinnable.UsingClosestAnchor = info.UsingClosestAnchor; if (component is Container container) { @@ -68,26 +67,5 @@ namespace osu.Game.Extensions container.Add(child.CreateInstance()); } } - - /// - ///

A ConditionalWeakTable is preferable to a Dictionary because a Dictionary will keep - /// orphaned references to an forever, unless manually pruned.

- ///

is used as a thin wrapper around bool because ConditionalWeakTable requires a reference type as both a key and a value.

- ///

was chosen over because it is a common ancestor between (which is required for logic) - /// and (which is required for serialization via ).

- ///

This collection is thread-safe according to the - /// documentation, - /// but the BindableBools are not unless leased.

- ///
- private static readonly ConditionalWeakTable is_drawable_using_closest_anchor_lookup = new ConditionalWeakTable(); - - /// - /// Gets or creates a representing whether is using the closest anchor point within its - /// parent. - /// - /// A whose is if the is using the closest anchor point, - /// otherwise . - public static BindableBool UsingClosestAnchor(this IDrawable drawable) => - is_drawable_using_closest_anchor_lookup.GetValue(drawable, _ => new BindableBool(true)); } } diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 5c733a9bc1..a76634a1f0 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -55,7 +55,10 @@ namespace osu.Game.Screens.Play.HUD Scale = component.Scale; Anchor = component.Anchor; Origin = component.Origin; - UsingClosestAnchor = component.UsingClosestAnchor().Value; + + UsingClosestAnchor = + // true if it's not an ISkinnableDrawable + !(component is ISkinnableDrawable skinnable) || skinnable.UsingClosestAnchor; if (component is Container container) { diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 5acc949182..f4476b9de7 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -241,7 +241,7 @@ namespace osu.Game.Skinning.Editor private void updateDrawableAnchorIfUsingClosest(Drawable drawable) { - if (!drawable.UsingClosestAnchor().Value) return; + if (!(drawable is ISkinnableDrawable { UsingClosestAnchor: true })) return; var closestAnchor = getClosestAnchorForDrawable(drawable); @@ -265,8 +265,8 @@ namespace osu.Game.Skinning.Editor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { - int checkAnchor(Drawable drawable) => - drawable.UsingClosestAnchor().Value + static int checkAnchor(Drawable drawable) => + drawable is ISkinnableDrawable { UsingClosestAnchor: true } ? closest_text_hash : (int)drawable.Anchor; @@ -331,8 +331,12 @@ namespace osu.Game.Skinning.Editor { var drawable = (Drawable)item; - var anchor = mapHashToAnchorSettings(hash, drawable); + var (usingClosest, anchor) = + hash == closest_text_hash + ? (true, getClosestAnchorForDrawable(drawable)) + : (false, (Anchor)hash); + item.UsingClosestAnchor = usingClosest; updateDrawableAnchor(drawable, anchor); } } @@ -344,20 +348,6 @@ namespace osu.Game.Skinning.Editor drawable.Position -= drawable.AnchorPosition - previousAnchor; } - private Anchor mapHashToAnchorSettings(int hash, Drawable drawable) - { - var isUsingClosestAnchor = drawable.UsingClosestAnchor(); - - if (hash == closest_text_hash) - { - isUsingClosestAnchor.Value = true; - return getClosestAnchorForDrawable(drawable); - } - - isUsingClosestAnchor.Value = false; - return (Anchor)hash; - } - private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) { // cancel out scale in axes we don't care about (based on which drag handle was used). From 133d72a8c0b02bc921160c1d70b2e960b11cb335 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Mon, 7 Jun 2021 00:14:36 -0400 Subject: [PATCH 208/670] Rename UsingClosestAnchor It is now "OverridesClosestAnchor". The logic is inverted accordingly. --- osu.Game/Extensions/DrawableExtensions.cs | 2 +- .../Screens/Play/HUD/DefaultAccuracyCounter.cs | 2 +- osu.Game/Screens/Play/HUD/DefaultComboCounter.cs | 2 +- .../Screens/Play/HUD/DefaultHealthDisplay.cs | 2 +- osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs | 2 +- .../Play/HUD/HitErrorMeters/HitErrorMeter.cs | 2 +- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 2 +- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 8 +++----- osu.Game/Screens/Play/SongProgress.cs | 2 +- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 16 ++++++++-------- osu.Game/Skinning/ISkinnableDrawable.cs | 6 +++--- osu.Game/Skinning/LegacyAccuracyCounter.cs | 2 +- osu.Game/Skinning/LegacyHealthDisplay.cs | 2 +- osu.Game/Skinning/LegacyScoreCounter.cs | 2 +- .../SkinnableTargetComponentsContainer.cs | 2 +- 15 files changed, 26 insertions(+), 28 deletions(-) diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 1199db587a..11a8112ecf 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -59,7 +59,7 @@ namespace osu.Game.Extensions component.Origin = info.Origin; if (component is ISkinnableDrawable skinnable) - skinnable.UsingClosestAnchor = info.UsingClosestAnchor; + skinnable.OverridesClosestAnchor = info.OverridesClosestAnchor; if (component is Container container) { diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index 5c6bb7299a..31edab5bc2 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index 97f33c0e07..bf9bed3fe3 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } public DefaultComboCounter() { diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index 82f014d1e3..3488de70a9 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play.HUD } } - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } public DefaultHealthDisplay() { diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index 56eaab4408..de534f516a 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs index f263b5accc..a16adfebbc 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [Resolved] private OsuColour colours { get; set; } - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } [BackgroundDependencyLoader(true)] private void load(DrawableRuleset drawableRuleset) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 9640bf1d4d..aff98b843b 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Play.HUD set => counterContainer.Alpha = value ? 1 : 0; } - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } public LegacyComboCounter() { diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index a76634a1f0..207bea77ce 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -32,8 +32,8 @@ namespace osu.Game.Screens.Play.HUD public Anchor Origin { get; set; } - /// - public bool UsingClosestAnchor { get; set; } = true; + /// + public bool OverridesClosestAnchor { get; set; } public List Children { get; } = new List(); @@ -56,9 +56,7 @@ namespace osu.Game.Screens.Play.HUD Anchor = component.Anchor; Origin = component.Origin; - UsingClosestAnchor = - // true if it's not an ISkinnableDrawable - !(component is ISkinnableDrawable skinnable) || skinnable.UsingClosestAnchor; + OverridesClosestAnchor = component is ISkinnableDrawable { OverridesClosestAnchor: true }; if (component is Container container) { diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index e8687b9ab1..a00a2c0275 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play private IClock referenceClock; - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } public SongProgress() { diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index f4476b9de7..7ddcebd662 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -241,7 +241,7 @@ namespace osu.Game.Skinning.Editor private void updateDrawableAnchorIfUsingClosest(Drawable drawable) { - if (!(drawable is ISkinnableDrawable { UsingClosestAnchor: true })) return; + if (drawable is ISkinnableDrawable { OverridesClosestAnchor: true }) return; var closestAnchor = getClosestAnchorForDrawable(drawable); @@ -266,9 +266,9 @@ namespace osu.Game.Skinning.Editor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { static int checkAnchor(Drawable drawable) => - drawable is ISkinnableDrawable { UsingClosestAnchor: true } - ? closest_text_hash - : (int)drawable.Anchor; + drawable is ISkinnableDrawable { OverridesClosestAnchor: true } + ? (int)drawable.Anchor + : closest_text_hash; yield return new OsuMenuItem(nameof(Anchor)) { @@ -331,12 +331,12 @@ namespace osu.Game.Skinning.Editor { var drawable = (Drawable)item; - var (usingClosest, anchor) = + var (overridesClosest, anchor) = hash == closest_text_hash - ? (true, getClosestAnchorForDrawable(drawable)) - : (false, (Anchor)hash); + ? (false, getClosestAnchorForDrawable(drawable)) + : (true, (Anchor)hash); - item.UsingClosestAnchor = usingClosest; + item.OverridesClosestAnchor = overridesClosest; updateDrawableAnchor(drawable, anchor); } } diff --git a/osu.Game/Skinning/ISkinnableDrawable.cs b/osu.Game/Skinning/ISkinnableDrawable.cs index a0c7ae43a2..1b5c0df1b2 100644 --- a/osu.Game/Skinning/ISkinnableDrawable.cs +++ b/osu.Game/Skinning/ISkinnableDrawable.cs @@ -16,9 +16,9 @@ namespace osu.Game.Skinning bool IsEditable => true; /// - /// if this 's is automatically determined by proximity, - /// if the user has overridden it. + /// if this 's is automatically determined by proximity, + /// if the user has overridden it. /// - bool UsingClosestAnchor { get; set; } + bool OverridesClosestAnchor { get; set; } } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 6c75ade365..603f0ecffb 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -12,7 +12,7 @@ namespace osu.Game.Skinning { public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable { - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } public LegacyAccuracyCounter() { diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index d70d672189..a419f981b5 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -30,7 +30,7 @@ namespace osu.Game.Skinning private bool isNewStyle; - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 944e42ed0e..206e10943d 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -13,7 +13,7 @@ namespace osu.Game.Skinning protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } public LegacyScoreCounter() : base(6) diff --git a/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs b/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs index 6acd917991..b661a02ca5 100644 --- a/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs @@ -17,7 +17,7 @@ namespace osu.Game.Skinning { public bool IsEditable => false; - public bool UsingClosestAnchor { get; set; } = true; + public bool OverridesClosestAnchor { get; set; } private readonly Action applyDefaults; From 29fa4fdf573176d68dda7996b8ccd6b5314caa0c Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Mon, 7 Jun 2021 01:08:39 -0400 Subject: [PATCH 209/670] Refactor unacceptable syntax --- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 3 +- .../Skinning/Editor/SkinSelectionHandler.cs | 30 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 207bea77ce..1549ba47e0 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -56,7 +56,8 @@ namespace osu.Game.Screens.Play.HUD Anchor = component.Anchor; Origin = component.Origin; - OverridesClosestAnchor = component is ISkinnableDrawable { OverridesClosestAnchor: true }; + if (component is ISkinnableDrawable skinnable) + OverridesClosestAnchor = skinnable.OverridesClosestAnchor; if (component is Container container) { diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 7ddcebd662..74609a6a1a 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -233,15 +233,17 @@ namespace osu.Game.Skinning.Editor Drawable drawable = (Drawable)c.Item; drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); - updateDrawableAnchorIfUsingClosest(drawable); + updateDrawableAnchorIfUsingClosest(c.Item); } return true; } - private void updateDrawableAnchorIfUsingClosest(Drawable drawable) + private void updateDrawableAnchorIfUsingClosest(ISkinnableDrawable item) { - if (drawable is ISkinnableDrawable { OverridesClosestAnchor: true }) return; + if (item.OverridesClosestAnchor) return; + + var drawable = (Drawable)item; var closestAnchor = getClosestAnchorForDrawable(drawable); @@ -265,10 +267,16 @@ namespace osu.Game.Skinning.Editor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { - static int checkAnchor(Drawable drawable) => - drawable is ISkinnableDrawable { OverridesClosestAnchor: true } - ? (int)drawable.Anchor - : closest_text_hash; + static int checkAnchor(ISkinnableDrawable item) + { + if (item.OverridesClosestAnchor) + { + var drawable = (Drawable)item; + return (int)drawable.Anchor; + } + + return closest_text_hash; + } yield return new OsuMenuItem(nameof(Anchor)) { @@ -278,20 +286,20 @@ namespace osu.Game.Skinning.Editor yield return new OsuMenuItem(nameof(Origin)) { // Origins can't be "closest" so we just cast to int - Items = createAnchorItems(origin_menu_items, d => (int)d.Origin, applyOrigin).ToArray() + Items = createAnchorItems(origin_menu_items, d => (int)((Drawable)d).Origin, applyOrigin).ToArray() }; foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - IEnumerable createAnchorItems(IDictionary items, Func checkFunction, Action applyFunction) => + IEnumerable createAnchorItems(IDictionary items, Func checkFunction, Action applyFunction) => items.Select(pair => { var (hash, text) = pair; return new TernaryStateRadioMenuItem(text, MenuItemType.Standard, _ => applyFunction(hash)) { - State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == hash) } + State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item) == hash) } }; }); } @@ -314,7 +322,7 @@ namespace osu.Game.Skinning.Editor drawable.Origin = anchor; drawable.Position += drawable.OriginPosition - previousOrigin; - updateDrawableAnchorIfUsingClosest(drawable); + updateDrawableAnchorIfUsingClosest(item); } } From 6c9594ee350a1710cc9a9eb783a8a9bad27feff6 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Mon, 7 Jun 2021 02:40:15 -0400 Subject: [PATCH 210/670] Simplify and rearrange SkinSelectionHandler The file has been restructured and reworded such that there are as few differences as possible from b36b40cb3430e6bd511390f82c39ba23195dc8cb. --- .../Skinning/Editor/SkinSelectionHandler.cs | 205 ++++++++---------- 1 file changed, 90 insertions(+), 115 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 74609a6a1a..b7fe5beb5c 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -20,87 +20,6 @@ namespace osu.Game.Skinning.Editor { public class SkinSelectionHandler : SelectionHandler { - private const string closest_text = @"Closest"; - - /// - /// The hash code of the "Closest" menu item in the anchor point context menu. - /// - /// Needs only be constant and distinct from any values. - private static readonly int closest_text_hash = closest_text.GetHashCode(); - - private static readonly Dictionary anchor_menu_items; - private static readonly Dictionary origin_menu_items; - - static SkinSelectionHandler() - { - var anchorMenuSubset = new[] - { - Anchor.TopLeft, - Anchor.TopCentre, - Anchor.TopRight, - Anchor.CentreLeft, - Anchor.Centre, - Anchor.CentreRight, - Anchor.BottomLeft, - Anchor.BottomCentre, - Anchor.BottomRight, - }; - - var texts = anchorMenuSubset.Select(a => a.ToString()).Prepend(closest_text).ToArray(); - var hashes = anchorMenuSubset.Cast().Prepend(closest_text_hash).ToArray(); - - var anchorPairs = hashes.Zip(texts, (k, v) => new KeyValuePair(k, v)).ToArray(); - anchor_menu_items = new Dictionary(anchorPairs); - var originPairs = anchorPairs.Where(pair => pair.Key != closest_text_hash); - origin_menu_items = new Dictionary(originPairs); - } - - private Anchor getClosestAnchorForDrawable(Drawable drawable) - { - var parent = drawable.Parent; - if (parent == null) - return drawable.Anchor; - - var screenPosition = getOriginPositionFromQuad(drawable.ScreenSpaceDrawQuad, drawable.Origin); - var absolutePosition = parent.ToLocalSpace(screenPosition); - var factor = parent.RelativeToAbsoluteFactor; - - var result = default(Anchor); - - result |= getTieredComponent(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2); - result |= getTieredComponent(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2); - - return result; - } - - private static Anchor getTieredComponent(float component, Anchor tier0, Anchor tier1, Anchor tier2) - { - if (component >= 2 / 3f) - return tier2; - - if (component >= 1 / 3f) - return tier1; - - return tier0; - } - - private Vector2 getOriginPositionFromQuad(in Quad quad, Anchor origin) - { - var result = quad.TopLeft; - - if (origin.HasFlagFast(Anchor.x2)) - result.X += quad.Width; - else if (origin.HasFlagFast(Anchor.x1)) - result.X += quad.Width / 2f; - - if (origin.HasFlagFast(Anchor.y2)) - result.Y += quad.Height; - else if (origin.HasFlagFast(Anchor.y1)) - result.Y += quad.Height / 2f; - - return result; - } - [Resolved] private SkinEditor skinEditor { get; set; } @@ -267,41 +186,48 @@ namespace osu.Game.Skinning.Editor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { - static int checkAnchor(ISkinnableDrawable item) + var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchor()) { - if (item.OverridesClosestAnchor) - { - var drawable = (Drawable)item; - return (int)drawable.Anchor; - } - - return closest_text_hash; - } - - yield return new OsuMenuItem(nameof(Anchor)) - { - Items = createAnchorItems(anchor_menu_items, checkAnchor, applyAnchor).ToArray() + State = { Value = GetStateFromSelection(selection, c => !c.Item.OverridesClosestAnchor) } }; - yield return new OsuMenuItem(nameof(Origin)) + yield return new OsuMenuItem("Anchor") { - // Origins can't be "closest" so we just cast to int - Items = createAnchorItems(origin_menu_items, d => (int)((Drawable)d).Origin, applyOrigin).ToArray() + Items = createAnchorItems((i, a) => i.OverridesClosestAnchor && ((Drawable)i).Anchor == a, applyAnchor) + .Prepend(closestItem) + .ToArray() + }; + + yield return new OsuMenuItem("Origin") + { + Items = createAnchorItems((i, o) => ((Drawable)i).Origin == o, applyOrigin).ToArray() }; foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - IEnumerable createAnchorItems(IDictionary items, Func checkFunction, Action applyFunction) => - items.Select(pair => + IEnumerable createAnchorItems(Func checkFunction, Action applyFunction) + { + var displayableAnchors = new[] { - var (hash, text) = pair; - - return new TernaryStateRadioMenuItem(text, MenuItemType.Standard, _ => applyFunction(hash)) + Anchor.TopLeft, + Anchor.TopCentre, + Anchor.TopRight, + Anchor.CentreLeft, + Anchor.Centre, + Anchor.CentreRight, + Anchor.BottomLeft, + Anchor.BottomCentre, + Anchor.BottomRight, + }; + return displayableAnchors.Select(a => + { + return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a)) { - State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item) == hash) } + State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item, a)) } }; }); + } } private static void updateDrawablePosition(Drawable drawable, Vector2 screenSpacePosition) @@ -310,10 +236,8 @@ namespace osu.Game.Skinning.Editor drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition; } - private void applyOrigin(int hash) + private void applyOrigin(Anchor anchor) { - var anchor = (Anchor)hash; - foreach (var item in SelectedItems) { var drawable = (Drawable)item; @@ -321,8 +245,6 @@ namespace osu.Game.Skinning.Editor var previousOrigin = drawable.OriginPosition; drawable.Origin = anchor; drawable.Position += drawable.OriginPosition - previousOrigin; - - updateDrawableAnchorIfUsingClosest(item); } } @@ -333,22 +255,75 @@ namespace osu.Game.Skinning.Editor private Quad getSelectionQuad() => GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); - private void applyAnchor(int hash) + private void applyAnchor(Anchor anchor) { foreach (var item in SelectedItems) { var drawable = (Drawable)item; - var (overridesClosest, anchor) = - hash == closest_text_hash - ? (false, getClosestAnchorForDrawable(drawable)) - : (true, (Anchor)hash); - - item.OverridesClosestAnchor = overridesClosest; + item.OverridesClosestAnchor = true; updateDrawableAnchor(drawable, anchor); } } + private void applyClosestAnchor() + { + foreach (var item in SelectedItems) + { + var drawable = (Drawable)item; + + item.OverridesClosestAnchor = false; + updateDrawableAnchor(drawable, getClosestAnchorForDrawable(drawable)); + } + } + + private static Anchor getClosestAnchorForDrawable(Drawable drawable) + { + var parent = drawable.Parent; + + if (parent == null) + return drawable.Anchor; + + var screenPosition = getOriginPositionFromQuad(drawable.ScreenSpaceDrawQuad, drawable.Origin); + var absolutePosition = parent.ToLocalSpace(screenPosition); + var factor = parent.RelativeToAbsoluteFactor; + + var result = default(Anchor); + + result |= getTieredComponent(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2); + result |= getTieredComponent(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2); + + return result; + } + + private static Vector2 getOriginPositionFromQuad(in Quad quad, Anchor origin) + { + var result = quad.TopLeft; + + if (origin.HasFlagFast(Anchor.x2)) + result.X += quad.Width; + else if (origin.HasFlagFast(Anchor.x1)) + result.X += quad.Width / 2f; + + if (origin.HasFlagFast(Anchor.y2)) + result.Y += quad.Height; + else if (origin.HasFlagFast(Anchor.y1)) + result.Y += quad.Height / 2f; + + return result; + } + + private static Anchor getTieredComponent(float component, Anchor tier0, Anchor tier1, Anchor tier2) + { + if (component >= 2 / 3f) + return tier2; + + if (component >= 1 / 3f) + return tier1; + + return tier0; + } + private static void updateDrawableAnchor(Drawable drawable, Anchor anchor) { var previousAnchor = drawable.AnchorPosition; From 65f594f860b5e0be75e0c4b4eb09d50e859b6ba8 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Mon, 7 Jun 2021 05:08:18 -0400 Subject: [PATCH 211/670] Rename `applyAnchor` to `applyCustomAnchor` --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index b7fe5beb5c..118c3308b2 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -193,7 +193,7 @@ namespace osu.Game.Skinning.Editor yield return new OsuMenuItem("Anchor") { - Items = createAnchorItems((i, a) => i.OverridesClosestAnchor && ((Drawable)i).Anchor == a, applyAnchor) + Items = createAnchorItems((i, a) => i.OverridesClosestAnchor && ((Drawable)i).Anchor == a, applyCustomAnchor) .Prepend(closestItem) .ToArray() }; @@ -255,7 +255,7 @@ namespace osu.Game.Skinning.Editor private Quad getSelectionQuad() => GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); - private void applyAnchor(Anchor anchor) + private void applyCustomAnchor(Anchor anchor) { foreach (var item in SelectedItems) { From dc50ae40b9b604785c1093c3c5cdc9dbb6160876 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Tue, 8 Jun 2021 08:22:35 -0400 Subject: [PATCH 212/670] Rename `OverridesClosestAnchor` to `UsesFixedAnchor` --- osu.Game/Extensions/DrawableExtensions.cs | 2 +- osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs | 2 +- osu.Game/Screens/Play/HUD/DefaultComboCounter.cs | 2 +- osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs | 2 +- osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs | 2 +- .../Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs | 2 +- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 2 +- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 6 +++--- osu.Game/Screens/Play/SongProgress.cs | 2 +- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 10 +++++----- osu.Game/Skinning/ISkinnableDrawable.cs | 4 ++-- osu.Game/Skinning/LegacyAccuracyCounter.cs | 2 +- osu.Game/Skinning/LegacyHealthDisplay.cs | 2 +- osu.Game/Skinning/LegacyScoreCounter.cs | 2 +- .../Skinning/SkinnableTargetComponentsContainer.cs | 2 +- 15 files changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 11a8112ecf..52d8230fb6 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -59,7 +59,7 @@ namespace osu.Game.Extensions component.Origin = info.Origin; if (component is ISkinnableDrawable skinnable) - skinnable.OverridesClosestAnchor = info.OverridesClosestAnchor; + skinnable.UsesFixedAnchor = info.UsesFixedAnchor; if (component is Container container) { diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index 31edab5bc2..324e5d43b5 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index bf9bed3fe3..718ae24cf1 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } public DefaultComboCounter() { diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index 3488de70a9..4f93868a66 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play.HUD } } - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } public DefaultHealthDisplay() { diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index de534f516a..63de5c8de5 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs index a16adfebbc..ab8b5c6cb1 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [Resolved] private OsuColour colours { get; set; } - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader(true)] private void load(DrawableRuleset drawableRuleset) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index aff98b843b..acff949353 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Play.HUD set => counterContainer.Alpha = value ? 1 : 0; } - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } public LegacyComboCounter() { diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 1549ba47e0..b64e5ca98f 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -32,8 +32,8 @@ namespace osu.Game.Screens.Play.HUD public Anchor Origin { get; set; } - /// - public bool OverridesClosestAnchor { get; set; } + /// + public bool UsesFixedAnchor { get; set; } public List Children { get; } = new List(); @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Play.HUD Origin = component.Origin; if (component is ISkinnableDrawable skinnable) - OverridesClosestAnchor = skinnable.OverridesClosestAnchor; + UsesFixedAnchor = skinnable.UsesFixedAnchor; if (component is Container container) { diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index a00a2c0275..bd861dc598 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play private IClock referenceClock; - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } public SongProgress() { diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 118c3308b2..78173bfda1 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -160,7 +160,7 @@ namespace osu.Game.Skinning.Editor private void updateDrawableAnchorIfUsingClosest(ISkinnableDrawable item) { - if (item.OverridesClosestAnchor) return; + if (item.UsesFixedAnchor) return; var drawable = (Drawable)item; @@ -188,12 +188,12 @@ namespace osu.Game.Skinning.Editor { var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchor()) { - State = { Value = GetStateFromSelection(selection, c => !c.Item.OverridesClosestAnchor) } + State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) } }; yield return new OsuMenuItem("Anchor") { - Items = createAnchorItems((i, a) => i.OverridesClosestAnchor && ((Drawable)i).Anchor == a, applyCustomAnchor) + Items = createAnchorItems((i, a) => i.UsesFixedAnchor && ((Drawable)i).Anchor == a, applyCustomAnchor) .Prepend(closestItem) .ToArray() }; @@ -261,7 +261,7 @@ namespace osu.Game.Skinning.Editor { var drawable = (Drawable)item; - item.OverridesClosestAnchor = true; + item.UsesFixedAnchor = true; updateDrawableAnchor(drawable, anchor); } } @@ -272,7 +272,7 @@ namespace osu.Game.Skinning.Editor { var drawable = (Drawable)item; - item.OverridesClosestAnchor = false; + item.UsesFixedAnchor = false; updateDrawableAnchor(drawable, getClosestAnchorForDrawable(drawable)); } } diff --git a/osu.Game/Skinning/ISkinnableDrawable.cs b/osu.Game/Skinning/ISkinnableDrawable.cs index 1b5c0df1b2..9625a9eb6d 100644 --- a/osu.Game/Skinning/ISkinnableDrawable.cs +++ b/osu.Game/Skinning/ISkinnableDrawable.cs @@ -17,8 +17,8 @@ namespace osu.Game.Skinning /// /// if this 's is automatically determined by proximity, - /// if the user has overridden it. + /// if the user has chosen a fixed anchor point. /// - bool OverridesClosestAnchor { get; set; } + bool UsesFixedAnchor { get; set; } } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 603f0ecffb..fd5a9500d9 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -12,7 +12,7 @@ namespace osu.Game.Skinning { public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable { - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } public LegacyAccuracyCounter() { diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index a419f981b5..c8df99762b 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -30,7 +30,7 @@ namespace osu.Game.Skinning private bool isNewStyle; - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 206e10943d..a12defe87e 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -13,7 +13,7 @@ namespace osu.Game.Skinning protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } public LegacyScoreCounter() : base(6) diff --git a/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs b/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs index b661a02ca5..67114de948 100644 --- a/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetComponentsContainer.cs @@ -17,7 +17,7 @@ namespace osu.Game.Skinning { public bool IsEditable => false; - public bool OverridesClosestAnchor { get; set; } + public bool UsesFixedAnchor { get; set; } private readonly Action applyDefaults; From 6b127f50f21afb89a5bbc2ff8f5364a4bd1f9763 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Tue, 8 Jun 2021 09:14:04 -0400 Subject: [PATCH 213/670] Inline updateDrawableAnchorIfUsingClosest --- .../Skinning/Editor/SkinSelectionHandler.cs | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 78173bfda1..7c904d5007 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -152,25 +152,18 @@ namespace osu.Game.Skinning.Editor Drawable drawable = (Drawable)c.Item; drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); - updateDrawableAnchorIfUsingClosest(c.Item); + if (c.Item.UsesFixedAnchor) continue; + + var closestAnchor = getClosestAnchorForDrawable(drawable); + + if (closestAnchor == drawable.Anchor) continue; + + updateDrawableAnchor(drawable, closestAnchor); } return true; } - private void updateDrawableAnchorIfUsingClosest(ISkinnableDrawable item) - { - if (item.UsesFixedAnchor) return; - - var drawable = (Drawable)item; - - var closestAnchor = getClosestAnchorForDrawable(drawable); - - if (closestAnchor == drawable.Anchor) return; - - updateDrawableAnchor(drawable, closestAnchor); - } - protected override void OnSelectionChanged() { base.OnSelectionChanged(); From 01da73daf211047392b85717621dba921d09d7b5 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Tue, 8 Jun 2021 09:25:49 -0400 Subject: [PATCH 214/670] Refactor `updateDrawableAnchorIfUsingClosest` --- .../Skinning/Editor/SkinSelectionHandler.cs | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 7c904d5007..4aa6e79fb8 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -149,21 +149,29 @@ namespace osu.Game.Skinning.Editor { foreach (var c in SelectedBlueprints) { - Drawable drawable = (Drawable)c.Item; + var skinnableDrawable = c.Item; + Drawable drawable = (Drawable)skinnableDrawable; drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); - if (c.Item.UsesFixedAnchor) continue; - - var closestAnchor = getClosestAnchorForDrawable(drawable); - - if (closestAnchor == drawable.Anchor) continue; - - updateDrawableAnchor(drawable, closestAnchor); + checkAndApplyClosestAnchor(skinnableDrawable); } return true; } + private static void checkAndApplyClosestAnchor(ISkinnableDrawable item) + { + if (item.UsesFixedAnchor) return; + + var drawable = (Drawable)item; + + var closestAnchor = getClosestAnchorForDrawable(drawable); + + if (closestAnchor == drawable.Anchor) return; + + updateDrawableAnchor(drawable, closestAnchor); + } + protected override void OnSelectionChanged() { base.OnSelectionChanged(); @@ -263,10 +271,8 @@ namespace osu.Game.Skinning.Editor { foreach (var item in SelectedItems) { - var drawable = (Drawable)item; - item.UsesFixedAnchor = false; - updateDrawableAnchor(drawable, getClosestAnchorForDrawable(drawable)); + checkAndApplyClosestAnchor(item); } } From 529a80871b3faaae3ae04a8787a7756d1287c48f Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Tue, 8 Jun 2021 09:44:42 -0400 Subject: [PATCH 215/670] Rename some methods for clarity Methods which operate on a collection of `ISkinnableDrawable`s are now plural; ones which take a single item are singular. This also allows cutting down the name of `getClosestAnchorForDrawable` to just `getClosestAnchor`. --- .../Skinning/Editor/SkinSelectionHandler.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 4aa6e79fb8..206f39da5c 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -165,11 +165,11 @@ namespace osu.Game.Skinning.Editor var drawable = (Drawable)item; - var closestAnchor = getClosestAnchorForDrawable(drawable); + var closestAnchor = getClosestAnchor(drawable); if (closestAnchor == drawable.Anchor) return; - updateDrawableAnchor(drawable, closestAnchor); + applyAnchor(drawable, closestAnchor); } protected override void OnSelectionChanged() @@ -187,21 +187,21 @@ namespace osu.Game.Skinning.Editor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { - var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchor()) + var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors()) { State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) } }; yield return new OsuMenuItem("Anchor") { - Items = createAnchorItems((i, a) => i.UsesFixedAnchor && ((Drawable)i).Anchor == a, applyCustomAnchor) + Items = createAnchorItems((i, a) => i.UsesFixedAnchor && ((Drawable)i).Anchor == a, applyCustomAnchors) .Prepend(closestItem) .ToArray() }; yield return new OsuMenuItem("Origin") { - Items = createAnchorItems((i, o) => ((Drawable)i).Origin == o, applyOrigin).ToArray() + Items = createAnchorItems((i, o) => ((Drawable)i).Origin == o, applyOrigins).ToArray() }; foreach (var item in base.GetContextMenuItemsForSelection(selection)) @@ -237,7 +237,7 @@ namespace osu.Game.Skinning.Editor drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition; } - private void applyOrigin(Anchor anchor) + private void applyOrigins(Anchor anchor) { foreach (var item in SelectedItems) { @@ -256,18 +256,18 @@ namespace osu.Game.Skinning.Editor private Quad getSelectionQuad() => GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); - private void applyCustomAnchor(Anchor anchor) + private void applyCustomAnchors(Anchor anchor) { foreach (var item in SelectedItems) { var drawable = (Drawable)item; item.UsesFixedAnchor = true; - updateDrawableAnchor(drawable, anchor); + applyAnchor(drawable, anchor); } } - private void applyClosestAnchor() + private void applyClosestAnchors() { foreach (var item in SelectedItems) { @@ -276,7 +276,7 @@ namespace osu.Game.Skinning.Editor } } - private static Anchor getClosestAnchorForDrawable(Drawable drawable) + private static Anchor getClosestAnchor(Drawable drawable) { var parent = drawable.Parent; @@ -323,7 +323,7 @@ namespace osu.Game.Skinning.Editor return tier0; } - private static void updateDrawableAnchor(Drawable drawable, Anchor anchor) + private static void applyAnchor(Drawable drawable, Anchor anchor) { var previousAnchor = drawable.AnchorPosition; drawable.Anchor = anchor; From f22cc981d15554665b54bf72956f7397eb7c42f6 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Tue, 8 Jun 2021 09:51:39 -0400 Subject: [PATCH 216/670] Move guard clause from `checkAndApplyClosestAnchor` to `applyAnchor` --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 206f39da5c..a9b985fda2 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -165,11 +165,7 @@ namespace osu.Game.Skinning.Editor var drawable = (Drawable)item; - var closestAnchor = getClosestAnchor(drawable); - - if (closestAnchor == drawable.Anchor) return; - - applyAnchor(drawable, closestAnchor); + applyAnchor(drawable, getClosestAnchor(drawable)); } protected override void OnSelectionChanged() @@ -325,6 +321,8 @@ namespace osu.Game.Skinning.Editor private static void applyAnchor(Drawable drawable, Anchor anchor) { + if (anchor == drawable.Anchor) return; + var previousAnchor = drawable.AnchorPosition; drawable.Anchor = anchor; drawable.Position -= drawable.AnchorPosition - previousAnchor; From 2c88e6df8de34ac1bc5e637bc4d1564d48699dd1 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Tue, 8 Jun 2021 10:08:02 -0400 Subject: [PATCH 217/670] Simplify `applyClosestAnchor` to one line by moving another guard clause --- .../Skinning/Editor/SkinSelectionHandler.cs | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index a9b985fda2..500020ca46 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -149,24 +149,20 @@ namespace osu.Game.Skinning.Editor { foreach (var c in SelectedBlueprints) { - var skinnableDrawable = c.Item; - Drawable drawable = (Drawable)skinnableDrawable; + var item = c.Item; + Drawable drawable = (Drawable)item; + drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); - checkAndApplyClosestAnchor(skinnableDrawable); + if (item.UsesFixedAnchor) continue; + + applyClosestAnchor(drawable); } return true; } - private static void checkAndApplyClosestAnchor(ISkinnableDrawable item) - { - if (item.UsesFixedAnchor) return; - - var drawable = (Drawable)item; - - applyAnchor(drawable, getClosestAnchor(drawable)); - } + private static void applyClosestAnchor(Drawable drawable) => applyAnchor(drawable, getClosestAnchor(drawable)); protected override void OnSelectionChanged() { @@ -268,7 +264,7 @@ namespace osu.Game.Skinning.Editor foreach (var item in SelectedItems) { item.UsesFixedAnchor = false; - checkAndApplyClosestAnchor(item); + applyClosestAnchor((Drawable)item); } } From d212918d67059ecd111155f93c039cf50435a2a8 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Tue, 8 Jun 2021 10:14:07 -0400 Subject: [PATCH 218/670] Rename `applyCustomAnchors` to `applyFixedAnchors` for consistency with `UsesFixedAnchor` --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 500020ca46..e52b1c68d4 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -186,7 +186,7 @@ namespace osu.Game.Skinning.Editor yield return new OsuMenuItem("Anchor") { - Items = createAnchorItems((i, a) => i.UsesFixedAnchor && ((Drawable)i).Anchor == a, applyCustomAnchors) + Items = createAnchorItems((i, a) => i.UsesFixedAnchor && ((Drawable)i).Anchor == a, applyFixedAnchors) .Prepend(closestItem) .ToArray() }; @@ -248,7 +248,7 @@ namespace osu.Game.Skinning.Editor private Quad getSelectionQuad() => GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray())); - private void applyCustomAnchors(Anchor anchor) + private void applyFixedAnchors(Anchor anchor) { foreach (var item in SelectedItems) { From 10b6b7290923a112dfee58c8a8c820dc32934611 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Tue, 8 Jun 2021 10:29:11 -0400 Subject: [PATCH 219/670] Add guard clause to `applyOrigins` and rename parameter --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index e52b1c68d4..ccfb0cb6e0 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -229,14 +229,16 @@ namespace osu.Game.Skinning.Editor drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition; } - private void applyOrigins(Anchor anchor) + private void applyOrigins(Anchor origin) { foreach (var item in SelectedItems) { var drawable = (Drawable)item; + if (origin == drawable.Origin) continue; + var previousOrigin = drawable.OriginPosition; - drawable.Origin = anchor; + drawable.Origin = origin; drawable.Position += drawable.OriginPosition - previousOrigin; } } From cf40282f1f30fb2d7d7a9c588157a0429b967e43 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 9 Jun 2021 13:34:42 +0300 Subject: [PATCH 220/670] Convert `LegacySkinTransformer`s to accept raw `ISkin`s rather than a full `ISkinSource` --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- .../Legacy/CatchLegacySkinTransformer.cs | 14 +++--- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Legacy/ManiaLegacySkinTransformer.cs | 32 ++++++------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Legacy/OsuLegacySkinTransformer.cs | 47 +++++++------------ .../Legacy/TaikoLegacySkinTransformer.cs | 20 +++----- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 6 +-- osu.Game/Rulesets/Ruleset.cs | 2 +- osu.Game/Skinning/LegacySkinTransformer.cs | 25 ++++------ 11 files changed, 61 insertions(+), 93 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 23ce444560..ec4c5dfe4c 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Catch public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap); - public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source); + public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin); public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score); diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 8c9e602cd4..9be47b3550 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy ///
private bool providesComboCounter => this.HasFont(LegacyFont.Combo); - public CatchLegacySkinTransformer(ISkinSource source) - : base(source) + public CatchLegacySkinTransformer(ISkin skin) + : base(skin) { } @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy switch (targetComponent.Target) { case SkinnableTarget.MainHUDComponents: - var components = Source.GetDrawableComponent(component) as SkinnableTargetComponentsContainer; + var components = Skin.GetDrawableComponent(component) as SkinnableTargetComponentsContainer; if (providesComboCounter && components != null) { @@ -79,13 +79,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy case CatchSkinComponents.CatchComboCounter: if (providesComboCounter) - return new LegacyCatchComboCounter(Source); + return new LegacyCatchComboCounter(Skin); return null; } } - return Source.GetDrawableComponent(component); + return Skin.GetDrawableComponent(component); } public override IBindable GetConfig(TLookup lookup) @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy switch (lookup) { case CatchSkinColour colour: - var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour)); + var result = (Bindable)Skin.GetConfig(new SkinCustomColourLookup(colour)); if (result == null) return null; @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return (IBindable)result; } - return Source.GetConfig(lookup); + return Skin.GetConfig(lookup); } } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index fbb9b3c466..fe736766d9 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); - public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new ManiaLegacySkinTransformer(source, beatmap); + public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap); public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 962a13ebea..7d4d303bc9 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -50,29 +50,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { HitResult.Miss, "mania-hit0" } }; - private Lazy isLegacySkin; + private readonly Lazy isLegacySkin; /// /// Whether texture for the keys exists. /// Used to determine if the mania ruleset is skinned. /// - private Lazy hasKeyTexture; + private readonly Lazy hasKeyTexture; - public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap) - : base(source) + public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap) + : base(skin) { this.beatmap = (ManiaBeatmap)beatmap; - Source.SourceChanged += sourceChanged; - sourceChanged(); - } - - private void sourceChanged() - { - isLegacySkin = new Lazy(() => FindProvider(s => s.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null) != null); - hasKeyTexture = new Lazy(() => FindProvider(s => s.GetAnimation( - s.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value - ?? "mania-key1", true, true) != null) != null); + isLegacySkin = new Lazy(() => skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); + hasKeyTexture = new Lazy(() => + { + var keyImage = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1"; + return skin.GetAnimation(keyImage, true, true) != null; + }); } public override Drawable GetDrawableComponent(ISkinComponent component) @@ -125,7 +121,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy break; } - return Source.GetDrawableComponent(component); + return Skin.GetDrawableComponent(component); } private Drawable getResult(HitResult result) @@ -146,15 +142,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered) return new SampleVirtual(); - return Source.GetSample(sampleInfo); + return Skin.GetSample(sampleInfo); } public override IBindable GetConfig(TLookup lookup) { if (lookup is ManiaSkinConfigurationLookup maniaLookup) - return Source.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn)); + return Skin.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn)); - return Source.GetConfig(lookup); + return Skin.GetConfig(lookup); } } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index b50d3ad2b4..69e22dc45d 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Osu public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this); - public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new OsuLegacySkinTransformer(source); + public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new OsuLegacySkinTransformer(skin); public int LegacyID => 0; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 3267b48ebf..3ad3b7d30b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public class OsuLegacySkinTransformer : LegacySkinTransformer { - private Lazy hasHitCircle; + private readonly Lazy hasHitCircle; /// /// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc. @@ -20,16 +20,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy /// public const float LEGACY_CIRCLE_RADIUS = 64 - 5; - public OsuLegacySkinTransformer(ISkinSource source) - : base(source) + public OsuLegacySkinTransformer(ISkin skin) + : base(skin) { - Source.SourceChanged += sourceChanged; - sourceChanged(); - } - - private void sourceChanged() - { - hasHitCircle = new Lazy(() => FindProvider(s => s.GetTexture("hitcircle") != null) != null); + hasHitCircle = new Lazy(() => Skin.GetTexture("hitcircle") != null); } public override Drawable GetDrawableComponent(ISkinComponent component) @@ -49,16 +43,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return followCircle; case OsuSkinComponents.SliderBall: - // specular and nd layers must come from the same source as the ball texure. - var ballProvider = Source.FindProvider(s => s.GetTexture("sliderb") != null || s.GetTexture("sliderb0") != null); - - var sliderBallContent = ballProvider.GetAnimation("sliderb", true, true, animationSeparator: ""); + var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: ""); // todo: slider ball has a custom frame delay based on velocity // Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME); if (sliderBallContent != null) - return new LegacySliderBall(sliderBallContent, ballProvider); + return new LegacySliderBall(sliderBallContent, this); return null; @@ -87,18 +78,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; case OsuSkinComponents.Cursor: - var cursorProvider = Source.FindProvider(s => s.GetTexture("cursor") != null); - - if (cursorProvider != null) - return new LegacyCursor(cursorProvider); + if (GetTexture("cursor") != null) + return new LegacyCursor(this); return null; case OsuSkinComponents.CursorTrail: - var trailProvider = Source.FindProvider(s => s.GetTexture("cursortrail") != null); - - if (trailProvider != null) - return new LegacyCursorTrail(trailProvider); + if (GetTexture("cursortrail") != null) + return new LegacyCursorTrail(this); return null; @@ -113,9 +100,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy }; case OsuSkinComponents.SpinnerBody: - bool hasBackground = Source.GetTexture("spinner-background") != null; + bool hasBackground = GetTexture("spinner-background") != null; - if (Source.GetTexture("spinner-top") != null && !hasBackground) + if (GetTexture("spinner-top") != null && !hasBackground) return new LegacyNewStyleSpinner(); else if (hasBackground) return new LegacyOldStyleSpinner(); @@ -124,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } } - return Source.GetDrawableComponent(component); + return Skin.GetDrawableComponent(component); } public override IBindable GetConfig(TLookup lookup) @@ -132,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy switch (lookup) { case OsuSkinColour colour: - return Source.GetConfig(new SkinCustomColourLookup(colour)); + return Skin.GetConfig(new SkinCustomColourLookup(colour)); case OsuSkinConfiguration osuLookup: switch (osuLookup) @@ -146,14 +133,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy case OsuSkinConfiguration.HitCircleOverlayAboveNumber: // See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D // HitCircleOverlayAboveNumer (with typo) should still be supported for now. - return Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? - Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); + return Skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? + Skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); } break; } - return Source.GetConfig(lookup); + return Skin.GetConfig(lookup); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 7ce0f6b93b..0122f9a1cd 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -15,18 +15,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class TaikoLegacySkinTransformer : LegacySkinTransformer { - private Lazy hasExplosion; + private readonly Lazy hasExplosion; - public TaikoLegacySkinTransformer(ISkinSource source) - : base(source) + public TaikoLegacySkinTransformer(ISkin skin) + : base(skin) { - Source.SourceChanged += sourceChanged; - sourceChanged(); - } - - private void sourceChanged() - { - hasExplosion = new Lazy(() => Source.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null); + hasExplosion = new Lazy(() => Skin.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null); } public override Drawable GetDrawableComponent(ISkinComponent component) @@ -132,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } } - return Source.GetDrawableComponent(component); + return Skin.GetDrawableComponent(component); } private string getHitName(TaikoSkinComponents component) @@ -155,12 +149,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public override ISample GetSample(ISampleInfo sampleInfo) { if (sampleInfo is HitSampleInfo hitSampleInfo) - return Source.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo)); + return Skin.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo)); return base.GetSample(sampleInfo); } - public override IBindable GetConfig(TLookup lookup) => Source.GetConfig(lookup); + public override IBindable GetConfig(TLookup lookup) => Skin.GetConfig(lookup); private class LegacyTaikoSampleInfo : HitSampleInfo { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 5854d4770c..ab5fcf6336 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this); - public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TaikoLegacySkinTransformer(source); + public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TaikoLegacySkinTransformer(skin); public const string SHORT_NAME = "taiko"; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index cc53e50884..13e84e335d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -116,12 +116,12 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestOsuRuleset : OsuRuleset { - public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(source); + public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(skin); private class TestOsuLegacySkinTransformer : OsuLegacySkinTransformer { - public TestOsuLegacySkinTransformer(ISkinSource source) - : base(source) + public TestOsuLegacySkinTransformer(ISkin skin) + : base(skin) { } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 7bdf84ace4..9f9f42eda4 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets [CanBeNull] public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().FirstOrDefault(); - public virtual ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => null; + public virtual ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => null; protected Ruleset() { diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index 651fdddb1b..cd896ab51e 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.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 osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -16,16 +15,16 @@ namespace osu.Game.Skinning /// /// Transformer used to handle support of legacy features for individual rulesets. /// - public abstract class LegacySkinTransformer : ISkinSource + public abstract class LegacySkinTransformer : ISkin { /// - /// Source of the which is being transformed. + /// The which is being transformed. /// - protected ISkinSource Source { get; } + protected ISkin Skin { get; } - protected LegacySkinTransformer(ISkinSource source) + protected LegacySkinTransformer(ISkin skin) { - Source = source; + Skin = skin; } public abstract Drawable GetDrawableComponent(ISkinComponent component); @@ -33,28 +32,20 @@ namespace osu.Game.Skinning public Texture GetTexture(string componentName) => GetTexture(componentName, default, default); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) - => Source.GetTexture(componentName, wrapModeS, wrapModeT); + => Skin.GetTexture(componentName, wrapModeS, wrapModeT); public virtual ISample GetSample(ISampleInfo sampleInfo) { if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample)) - return Source.GetSample(sampleInfo); + return Skin.GetSample(sampleInfo); var playLayeredHitSounds = GetConfig(LegacySetting.LayeredHitSounds); if (legacySample.IsLayered && playLayeredHitSounds?.Value == false) return new SampleVirtual(); - return Source.GetSample(sampleInfo); + return Skin.GetSample(sampleInfo); } public abstract IBindable GetConfig(TLookup lookup); - - public ISkin FindProvider(Func lookupFunction) => Source.FindProvider(lookupFunction); - - public event Action SourceChanged - { - add { throw new NotSupportedException(); } - remove { } - } } } From 6538d44708646a2b32b4aa0b112d7d31d82d38ff Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 9 Jun 2021 20:36:34 +0300 Subject: [PATCH 221/670] Make `SkinProvidingContainer` able to perform lookup on multiple skins Currently `protected` functionality for use in custom `SkinProvidingContainer`s, can be exposed to public constructors if it need to later on, but I'm not sure about doing that opposed to just nesting multiple `SkinProvidingContainer`. --- .../Gameplay/TestSceneSkinnableDrawable.cs | 2 +- .../Skinning/BeatmapSkinProvidingContainer.cs | 6 +- osu.Game/Skinning/SkinProvidingContainer.cs | 94 ++++++++++++------- 3 files changed, 64 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index 96418f6d28..77966e925a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void Disable() { allow = false; - TriggerSourceChanged(); + OnSourceChanged(); } public SwitchableSkinProvidingContainer(ISkin skin) diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs index 57c08a903f..f12f44e347 100644 --- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs +++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs @@ -83,9 +83,9 @@ namespace osu.Game.Skinning [BackgroundDependencyLoader] private void load() { - beatmapSkins.BindValueChanged(_ => TriggerSourceChanged()); - beatmapColours.BindValueChanged(_ => TriggerSourceChanged()); - beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged()); + beatmapSkins.BindValueChanged(_ => OnSourceChanged()); + beatmapColours.BindValueChanged(_ => OnSourceChanged()); + beatmapHitsounds.BindValueChanged(_ => OnSourceChanged()); } } } diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 0e16cf43ee..cc9d8d0e8d 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -21,8 +22,10 @@ namespace osu.Game.Skinning { public event Action SourceChanged; - [CanBeNull] - private readonly ISkin skin; + /// + /// The list of skins provided by this . + /// + protected readonly List SkinLayers = new List(); [CanBeNull] private ISkinSource fallbackSource; @@ -38,23 +41,30 @@ namespace osu.Game.Skinning protected virtual bool AllowColourLookup => true; public SkinProvidingContainer(ISkin skin) + : this() { - this.skin = skin; + SkinLayers.Add(skin); + } + protected SkinProvidingContainer() + { RelativeSizeAxes = Axes.Both; } public ISkin FindProvider(Func lookupFunction) { - if (skin is ISkinSource source) + foreach (var skin in SkinLayers) { - if (source.FindProvider(lookupFunction) is ISkin found) - return found; - } - else if (skin != null) - { - if (lookupFunction(skin)) - return skin; + if (skin is ISkinSource source) + { + if (source.FindProvider(lookupFunction) is ISkin found) + return found; + } + else if (skin != null) + { + if (lookupFunction(skin)) + return skin; + } } return fallbackSource?.FindProvider(lookupFunction); @@ -62,57 +72,73 @@ namespace osu.Game.Skinning public Drawable GetDrawableComponent(ISkinComponent component) { - Drawable sourceDrawable; - if (AllowDrawableLookup(component) && (sourceDrawable = skin?.GetDrawableComponent(component)) != null) - return sourceDrawable; + if (AllowDrawableLookup(component)) + { + foreach (var skin in SkinLayers) + { + Drawable sourceDrawable; + if ((sourceDrawable = skin?.GetDrawableComponent(component)) != null) + return sourceDrawable; + } + } return fallbackSource?.GetDrawableComponent(component); } public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { - Texture sourceTexture; - if (AllowTextureLookup(componentName) && (sourceTexture = skin?.GetTexture(componentName, wrapModeS, wrapModeT)) != null) - return sourceTexture; + if (AllowTextureLookup(componentName)) + { + foreach (var skin in SkinLayers) + { + Texture sourceTexture; + if ((sourceTexture = skin?.GetTexture(componentName, wrapModeS, wrapModeT)) != null) + return sourceTexture; + } + } return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT); } public ISample GetSample(ISampleInfo sampleInfo) { - ISample sourceChannel; - if (AllowSampleLookup(sampleInfo) && (sourceChannel = skin?.GetSample(sampleInfo)) != null) - return sourceChannel; + if (AllowSampleLookup(sampleInfo)) + { + foreach (var skin in SkinLayers) + { + ISample sourceSample; + if ((sourceSample = skin?.GetSample(sampleInfo)) != null) + return sourceSample; + } + } return fallbackSource?.GetSample(sampleInfo); } public IBindable GetConfig(TLookup lookup) { - if (skin != null) - { - if (lookup is GlobalSkinColours || lookup is SkinCustomColourLookup) - return lookupWithFallback(lookup, AllowColourLookup); + if (lookup is GlobalSkinColours || lookup is SkinCustomColourLookup) + return lookupWithFallback(lookup, AllowColourLookup); - return lookupWithFallback(lookup, AllowConfigurationLookup); - } - - return fallbackSource?.GetConfig(lookup); + return lookupWithFallback(lookup, AllowConfigurationLookup); } private IBindable lookupWithFallback(TLookup lookup, bool canUseSkinLookup) { if (canUseSkinLookup) { - var bindable = skin?.GetConfig(lookup); - if (bindable != null) - return bindable; + foreach (var skin in SkinLayers) + { + IBindable bindable; + if ((bindable = skin?.GetConfig(lookup)) != null) + return bindable; + } } return fallbackSource?.GetConfig(lookup); } - protected virtual void TriggerSourceChanged() => SourceChanged?.Invoke(); + protected virtual void OnSourceChanged() => SourceChanged?.Invoke(); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -120,7 +146,7 @@ namespace osu.Game.Skinning fallbackSource = dependencies.Get(); if (fallbackSource != null) - fallbackSource.SourceChanged += TriggerSourceChanged; + fallbackSource.SourceChanged += OnSourceChanged; dependencies.CacheAs(this); @@ -135,7 +161,7 @@ namespace osu.Game.Skinning base.Dispose(isDisposing); if (fallbackSource != null) - fallbackSource.SourceChanged -= TriggerSourceChanged; + fallbackSource.SourceChanged -= OnSourceChanged; } } } From 9e652715ced79d2a6f22180c03d3e6024a10da8d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 9 Jun 2021 20:40:47 +0300 Subject: [PATCH 222/670] Expose the skin lookup layers of `SkinManager` to a property --- osu.Game/Skinning/SkinManager.cs | 44 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 48d6b9254f..134156e44f 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -42,6 +42,24 @@ namespace osu.Game.Skinning public readonly Bindable CurrentSkin = new Bindable(); public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; + /// + /// The skin layers of the currently selected user skin for performing lookups on, + /// in order of preference (user skin first, then fallback skins). + /// + public IEnumerable CurrentSkinLayers + { + get + { + yield return CurrentSkin.Value; + + // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. + // When attempting to address this, we may want to move the full DefaultLegacySkin fallback logic to within Player itself (to better allow + // for beatmap skin visibility). + if (CurrentSkin.Value is LegacySkin) + yield return defaultLegacySkin; + } + } + public override IEnumerable HandledExtensions => new[] { ".osk" }; protected override string[] HashableFileTypes => new[] { ".ini" }; @@ -220,11 +238,11 @@ namespace osu.Game.Skinning public ISkin FindProvider(Func lookupFunction) { - if (lookupFunction(CurrentSkin.Value)) - return CurrentSkin.Value; - - if (CurrentSkin.Value is LegacySkin && lookupFunction(defaultLegacySkin)) - return defaultLegacySkin; + foreach (var skin in CurrentSkinLayers) + { + if (lookupFunction(skin)) + return skin; + } return null; } @@ -232,16 +250,12 @@ namespace osu.Game.Skinning private T lookupWithFallback(Func func) where T : class { - var selectedSkin = func(CurrentSkin.Value); - - if (selectedSkin != null) - return selectedSkin; - - // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. - // When attempting to address this, we may want to move the full DefaultLegacySkin fallback logic to within Player itself (to better allow - // for beatmap skin visibility). - if (CurrentSkin.Value is LegacySkin) - return func(defaultLegacySkin); + foreach (var skin in CurrentSkinLayers) + { + var result = func(skin); + if (result != null) + return result; + } return null; } From 33a9cac398f446bb01b5e9e9bd9463f81edd62ae Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 9 Jun 2021 20:41:16 +0300 Subject: [PATCH 223/670] Add special `RulesetSkinProvidingContainer` managing ruleset-compatible skin setup --- .../Skinning/RulesetSkinProvidingContainer.cs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 osu.Game/Skinning/RulesetSkinProvidingContainer.cs diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs new file mode 100644 index 0000000000..dcf6281e38 --- /dev/null +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -0,0 +1,59 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; + +namespace osu.Game.Skinning +{ + /// + /// A type of that provides access to the beatmap skin and user skin, + /// each transformed with the ruleset's own skin transformer individually. + /// + public class RulesetSkinProvidingContainer : SkinProvidingContainer + { + private readonly Ruleset ruleset; + private readonly IBeatmap beatmap; + + protected override Container Content { get; } + + public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, ISkin beatmapSkin) + { + this.ruleset = ruleset; + this.beatmap = beatmap; + + InternalChild = new BeatmapSkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkin, beatmap)) + { + Child = Content = new Container + { + RelativeSizeAxes = Axes.Both, + } + }; + } + + [Resolved] + private SkinManager skinManager { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + updateSkins(); + } + + protected override void OnSourceChanged() + { + updateSkins(); + base.OnSourceChanged(); + } + + private void updateSkins() + { + SkinLayers.Clear(); + SkinLayers.AddRange(skinManager.CurrentSkinLayers.Select(s => ruleset.CreateLegacySkinProvider(s, beatmap))); + } + } +} From e30f6581b384c35aa7dbe5e9d04ce254bb084e2d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 9 Jun 2021 21:30:26 +0300 Subject: [PATCH 224/670] Wrap gameplay content within a `RulesetSkinProvidingContainer` --- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 10 +--------- osu.Game/Screens/Play/Player.cs | 14 ++++---------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 61056aeced..b56f9bee14 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -73,15 +73,7 @@ namespace osu.Game.Screens.Edit.Compose { Debug.Assert(ruleset != null); - var beatmapSkinProvider = new BeatmapSkinProvidingContainer(beatmap.Value.Skin); - - // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation - // full access to all skin sources. - var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, EditorBeatmap.PlayableBeatmap)); - - // load the skinning hierarchy first. - // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. - return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content)); + return new RulesetSkinProvidingContainer(ruleset, EditorBeatmap.PlayableBeatmap, beatmap.Value.Skin).WithChild(content); } #region Input Handling diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f9036780aa..47c91cfc4d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -234,29 +234,23 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(GameplayBeatmap); - var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); - - // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation - // full access to all skin sources. - var rulesetSkinProvider = new SkinProvidingContainer(GameplayRuleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap)); + var rulesetSkinProvider = new RulesetSkinProvidingContainer(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin); // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. - GameplayClockContainer.Add(beatmapSkinProvider.WithChild(rulesetSkinProvider)); + GameplayClockContainer.Add(rulesetSkinProvider); rulesetSkinProvider.AddRange(new[] { - // underlay and gameplay should have access the to skinning sources. + // underlay and gameplay should have access to the skinning sources. createUnderlayComponents(), createGameplayComponents(Beatmap.Value, playableBeatmap) }); // also give the HUD a ruleset container to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. - var hudRulesetContainer = new SkinProvidingContainer(GameplayRuleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap)); - // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. - GameplayClockContainer.Add(hudRulesetContainer.WithChild(createOverlayComponents(Beatmap.Value))); + rulesetSkinProvider.Add(createOverlayComponents(Beatmap.Value)); if (!DrawableRuleset.AllowGameplayOverlays) { From 1aaad7bfd442d74cfc28903fb4f4b35eb178edb7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 9 Jun 2021 22:24:53 +0300 Subject: [PATCH 225/670] Apply few adjustments to skinning overlays comment --- osu.Game/Screens/Play/Player.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 47c91cfc4d..bec5181efe 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -247,9 +247,9 @@ namespace osu.Game.Screens.Play createGameplayComponents(Beatmap.Value, playableBeatmap) }); - // also give the HUD a ruleset container to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) - // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. + // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) + // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. rulesetSkinProvider.Add(createOverlayComponents(Beatmap.Value)); if (!DrawableRuleset.AllowGameplayOverlays) From 18edbdd135dc31e989fe8f801b612a267ee6e415 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 11:55:22 +0300 Subject: [PATCH 226/670] Remove mentioning of "layer" in skin providers `SkinSources` sounds better. --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 4 ++-- osu.Game/Skinning/SkinProvidingContainer.cs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index dcf6281e38..1fe86d2873 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -52,8 +52,8 @@ namespace osu.Game.Skinning private void updateSkins() { - SkinLayers.Clear(); - SkinLayers.AddRange(skinManager.CurrentSkinLayers.Select(s => ruleset.CreateLegacySkinProvider(s, beatmap))); + SkinSources.Clear(); + SkinSources.AddRange(skinManager.CurrentSkinLayers.Select(s => ruleset.CreateLegacySkinProvider(s, beatmap))); } } } diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index cc9d8d0e8d..6686583a6f 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -25,7 +25,7 @@ namespace osu.Game.Skinning /// /// The list of skins provided by this . /// - protected readonly List SkinLayers = new List(); + protected readonly List SkinSources = new List(); [CanBeNull] private ISkinSource fallbackSource; @@ -43,7 +43,7 @@ namespace osu.Game.Skinning public SkinProvidingContainer(ISkin skin) : this() { - SkinLayers.Add(skin); + SkinSources.Add(skin); } protected SkinProvidingContainer() @@ -53,7 +53,7 @@ namespace osu.Game.Skinning public ISkin FindProvider(Func lookupFunction) { - foreach (var skin in SkinLayers) + foreach (var skin in SkinSources) { if (skin is ISkinSource source) { @@ -74,7 +74,7 @@ namespace osu.Game.Skinning { if (AllowDrawableLookup(component)) { - foreach (var skin in SkinLayers) + foreach (var skin in SkinSources) { Drawable sourceDrawable; if ((sourceDrawable = skin?.GetDrawableComponent(component)) != null) @@ -89,7 +89,7 @@ namespace osu.Game.Skinning { if (AllowTextureLookup(componentName)) { - foreach (var skin in SkinLayers) + foreach (var skin in SkinSources) { Texture sourceTexture; if ((sourceTexture = skin?.GetTexture(componentName, wrapModeS, wrapModeT)) != null) @@ -104,7 +104,7 @@ namespace osu.Game.Skinning { if (AllowSampleLookup(sampleInfo)) { - foreach (var skin in SkinLayers) + foreach (var skin in SkinSources) { ISample sourceSample; if ((sourceSample = skin?.GetSample(sampleInfo)) != null) @@ -127,7 +127,7 @@ namespace osu.Game.Skinning { if (canUseSkinLookup) { - foreach (var skin in SkinLayers) + foreach (var skin in SkinSources) { IBindable bindable; if ((bindable = skin?.GetConfig(lookup)) != null) From 530026b6755c6e5891969dd1d0c594da67c3ffd2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 11:56:13 +0300 Subject: [PATCH 227/670] Add simple xmldoc to ctors explaining their deal with `SkinSources` --- osu.Game/Skinning/SkinProvidingContainer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 6686583a6f..a7bc3ba379 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -40,12 +40,19 @@ namespace osu.Game.Skinning protected virtual bool AllowColourLookup => true; + /// + /// Constructs a new with a single skin added to the protected list. + /// public SkinProvidingContainer(ISkin skin) : this() { SkinSources.Add(skin); } + /// + /// Constructs a new with no sources. + /// Up to the implementation for adding to the list. + /// protected SkinProvidingContainer() { RelativeSizeAxes = Axes.Both; From 58cca9da06ecd1081b33f03ea57ea9cea8e88c0c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 11:57:28 +0300 Subject: [PATCH 228/670] Revert "Expose the skin lookup layers of `SkinManager` to a property" This reverts commit 9e652715ced79d2a6f22180c03d3e6024a10da8d. --- osu.Game/Skinning/SkinManager.cs | 44 +++++++++++--------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 134156e44f..48d6b9254f 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -42,24 +42,6 @@ namespace osu.Game.Skinning public readonly Bindable CurrentSkin = new Bindable(); public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; - /// - /// The skin layers of the currently selected user skin for performing lookups on, - /// in order of preference (user skin first, then fallback skins). - /// - public IEnumerable CurrentSkinLayers - { - get - { - yield return CurrentSkin.Value; - - // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. - // When attempting to address this, we may want to move the full DefaultLegacySkin fallback logic to within Player itself (to better allow - // for beatmap skin visibility). - if (CurrentSkin.Value is LegacySkin) - yield return defaultLegacySkin; - } - } - public override IEnumerable HandledExtensions => new[] { ".osk" }; protected override string[] HashableFileTypes => new[] { ".ini" }; @@ -238,11 +220,11 @@ namespace osu.Game.Skinning public ISkin FindProvider(Func lookupFunction) { - foreach (var skin in CurrentSkinLayers) - { - if (lookupFunction(skin)) - return skin; - } + if (lookupFunction(CurrentSkin.Value)) + return CurrentSkin.Value; + + if (CurrentSkin.Value is LegacySkin && lookupFunction(defaultLegacySkin)) + return defaultLegacySkin; return null; } @@ -250,12 +232,16 @@ namespace osu.Game.Skinning private T lookupWithFallback(Func func) where T : class { - foreach (var skin in CurrentSkinLayers) - { - var result = func(skin); - if (result != null) - return result; - } + var selectedSkin = func(CurrentSkin.Value); + + if (selectedSkin != null) + return selectedSkin; + + // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. + // When attempting to address this, we may want to move the full DefaultLegacySkin fallback logic to within Player itself (to better allow + // for beatmap skin visibility). + if (CurrentSkin.Value is LegacySkin) + return func(defaultLegacySkin); return null; } From 59be3588eb281d916ba68297f19e6eb4a5c362ed Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 12:17:51 +0300 Subject: [PATCH 229/670] Change `SkinSources` to a bindable list for binding `SourceChanged` events --- osu.Game/Skinning/SkinProvidingContainer.cs | 44 +++++++++++---------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 3739172367..fa0780ff29 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -2,7 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -25,7 +26,7 @@ namespace osu.Game.Skinning /// /// The list of skins provided by this . /// - protected readonly List SkinSources = new List(); + protected readonly BindableList SkinSources = new BindableList(); [CanBeNull] private ISkinSource fallbackSource; @@ -61,8 +62,23 @@ namespace osu.Game.Skinning noFallbackLookupProxy = new NoFallbackProxy(this); - if (skin is ISkinSource source) - source.SourceChanged += TriggerSourceChanged; + SkinSources.BindCollectionChanged(((_, args) => + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Remove: + foreach (var source in args.OldItems.Cast().OfType()) + source.SourceChanged -= OnSourceChanged; + + break; + + case NotifyCollectionChangedAction.Add: + foreach (var source in args.NewItems.Cast().OfType()) + source.SourceChanged += OnSourceChanged; + + break; + } + }), true); } public ISkin FindProvider(Func lookupFunction) @@ -146,21 +162,9 @@ namespace osu.Game.Skinning public IBindable GetConfig(TLookup lookup, bool fallback) { if (lookup is GlobalSkinColours || lookup is SkinCustomColourLookup) - return lookupWithFallback(lookup, AllowColourLookup); + return lookupWithFallback(lookup, AllowColourLookup, fallback); - return lookupWithFallback(lookup, AllowConfigurationLookup); - if (skin != null) - { - if (lookup is GlobalSkinColours || lookup is SkinCustomColourLookup) - return lookupWithFallback(lookup, AllowColourLookup, fallback); - - return lookupWithFallback(lookup, AllowConfigurationLookup, fallback); - } - - if (!fallback) - return null; - - return fallbackSource?.GetConfig(lookup); + return lookupWithFallback(lookup, AllowConfigurationLookup, fallback); } private IBindable lookupWithFallback(TLookup lookup, bool canUseSkinLookup, bool canUseFallback) @@ -205,10 +209,8 @@ namespace osu.Game.Skinning if (fallbackSource != null) fallbackSource.SourceChanged -= OnSourceChanged; - fallbackSource.SourceChanged -= TriggerSourceChanged; - if (skin is ISkinSource source) - source.SourceChanged -= TriggerSourceChanged; + SkinSources.Clear(); } private class NoFallbackProxy : ISkinSource From c3a2f2c2a4559b3740368065b78564c1306dd3cf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 13:04:34 +0300 Subject: [PATCH 230/670] Expose default `SkinManager` providers for use in `RulesetSkinProvidingContainer` --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 9 +++++++-- osu.Game/Skinning/SkinManager.cs | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 1fe86d2873..eadf0e05b9 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.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.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -53,7 +52,13 @@ namespace osu.Game.Skinning private void updateSkins() { SkinSources.Clear(); - SkinSources.AddRange(skinManager.CurrentSkinLayers.Select(s => ruleset.CreateLegacySkinProvider(s, beatmap))); + + SkinSources.Add(ruleset.CreateLegacySkinProvider(skinManager.CurrentSkin.Value, beatmap)); + + if (skinManager.CurrentSkin.Value is LegacySkin) + SkinSources.Add(ruleset.CreateLegacySkinProvider(skinManager.DefaultLegacySkin, beatmap)); + + SkinSources.Add(ruleset.CreateLegacySkinProvider(skinManager.DefaultSkin, beatmap)); } } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 9e274227a2..89f166dc2a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -48,9 +48,19 @@ namespace osu.Game.Skinning protected override string ImportFromStablePath => "Skins"; + private readonly Skin defaultSkin; + + /// + /// An providing the resources of the default skin. + /// + public ISkin DefaultSkin => defaultSkin; + private readonly Skin defaultLegacySkin; - private readonly Skin defaultSkin; + /// + /// An providing the resources of the default legacy skin. + /// + public ISkin DefaultLegacySkin => defaultLegacySkin; public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore resources, AudioManager audio) : base(storage, contextFactory, new SkinStore(contextFactory, storage), host) @@ -84,7 +94,7 @@ namespace osu.Game.Skinning { var userSkins = GetAllUserSkins(); userSkins.Insert(0, SkinInfo.Default); - userSkins.Insert(1, DefaultLegacySkin.Info); + userSkins.Insert(1, Skinning.DefaultLegacySkin.Info); return userSkins; } From 26cdcc8d78f4bb29d2730c7ad291db0651014177 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 13:05:44 +0300 Subject: [PATCH 231/670] Remove stale access to `Source` from master merge --- .../Skinning/Legacy/CatchLegacySkinTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index a196ebedd7..a5a1d1504f 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return null; case CatchSkinComponents.Catcher: - var version = Source.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1; + var version = GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1; if (version < 2.3m) { From 5c9c424a0d032ec412d7be58953176d86886b233 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 13:15:18 +0300 Subject: [PATCH 232/670] Switch state case placements for consistency Tickled me. --- osu.Game/Skinning/SkinProvidingContainer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index fa0780ff29..74a465a91f 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -66,17 +66,17 @@ namespace osu.Game.Skinning { switch (args.Action) { - case NotifyCollectionChangedAction.Remove: - foreach (var source in args.OldItems.Cast().OfType()) - source.SourceChanged -= OnSourceChanged; - - break; - case NotifyCollectionChangedAction.Add: foreach (var source in args.NewItems.Cast().OfType()) source.SourceChanged += OnSourceChanged; break; + + case NotifyCollectionChangedAction.Remove: + foreach (var source in args.OldItems.Cast().OfType()) + source.SourceChanged -= OnSourceChanged; + + break; } }), true); } From 5a2e71009512bb1b9d01c870942003d47e947fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Jun 2021 13:55:34 +0200 Subject: [PATCH 233/670] Split common method for metadata textbox creation --- .../Screens/Edit/Setup/MetadataSection.cs | 40 ++++++------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index c79888b9d1..3b2ac0d187 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -24,40 +24,24 @@ namespace osu.Game.Screens.Edit.Setup { Children = new Drawable[] { - artistTextBox = new LabelledTextBox - { - Label = "Artist", - FixedLabelWidth = LABEL_WIDTH, - Current = { Value = Beatmap.Metadata.Artist }, - TabbableContentContainer = this - }, - titleTextBox = new LabelledTextBox - { - Label = "Title", - FixedLabelWidth = LABEL_WIDTH, - Current = { Value = Beatmap.Metadata.Title }, - TabbableContentContainer = this - }, - creatorTextBox = new LabelledTextBox - { - Label = "Creator", - FixedLabelWidth = LABEL_WIDTH, - Current = { Value = Beatmap.Metadata.AuthorString }, - TabbableContentContainer = this - }, - difficultyTextBox = new LabelledTextBox - { - Label = "Difficulty Name", - FixedLabelWidth = LABEL_WIDTH, - Current = { Value = Beatmap.BeatmapInfo.Version }, - TabbableContentContainer = this - }, + artistTextBox = createTextBox("Artist", Beatmap.Metadata.Artist), + titleTextBox = createTextBox("Title", Beatmap.Metadata.Title), + creatorTextBox = createTextBox("Creator", Beatmap.Metadata.AuthorString), + difficultyTextBox = createTextBox("Difficulty Name", Beatmap.BeatmapInfo.Version) }; foreach (var item in Children.OfType()) item.OnCommit += onCommit; } + private LabelledTextBox createTextBox(string label, string initialValue) => new LabelledTextBox + { + Label = label, + FixedLabelWidth = LABEL_WIDTH, + Current = { Value = initialValue }, + TabbableContentContainer = this + }; + protected override void LoadComplete() { base.LoadComplete(); From 252fe0a6ccf1d2412e358563569d1a518cd92be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Jun 2021 14:02:17 +0200 Subject: [PATCH 234/670] Add source and tags text boxes to metadata section --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 14 +++++++++++--- osu.Game/Screens/Edit/Setup/SetupSection.cs | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 3b2ac0d187..6d14f6a66f 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; @@ -14,20 +13,26 @@ namespace osu.Game.Screens.Edit.Setup { private LabelledTextBox artistTextBox; private LabelledTextBox titleTextBox; + private LabelledTextBox creatorTextBox; private LabelledTextBox difficultyTextBox; + private LabelledTextBox sourceTextBox; + private LabelledTextBox tagsTextBox; public override LocalisableString Title => "Metadata"; [BackgroundDependencyLoader] private void load() { - Children = new Drawable[] + Children = new[] { artistTextBox = createTextBox("Artist", Beatmap.Metadata.Artist), titleTextBox = createTextBox("Title", Beatmap.Metadata.Title), + Empty(), creatorTextBox = createTextBox("Creator", Beatmap.Metadata.AuthorString), - difficultyTextBox = createTextBox("Difficulty Name", Beatmap.BeatmapInfo.Version) + difficultyTextBox = createTextBox("Difficulty Name", Beatmap.BeatmapInfo.Version), + sourceTextBox = createTextBox("Source", Beatmap.Metadata.Source), + tagsTextBox = createTextBox("Tags", Beatmap.Metadata.Tags) }; foreach (var item in Children.OfType()) @@ -58,8 +63,11 @@ namespace osu.Game.Screens.Edit.Setup // after switching database engines we can reconsider if switching to bindables is a good direction. Beatmap.Metadata.Artist = artistTextBox.Current.Value; Beatmap.Metadata.Title = titleTextBox.Current.Value; + Beatmap.Metadata.AuthorString = creatorTextBox.Current.Value; Beatmap.BeatmapInfo.Version = difficultyTextBox.Current.Value; + Beatmap.Metadata.Source = sourceTextBox.Current.Value; + Beatmap.Metadata.Tags = tagsTextBox.Current.Value; } } } diff --git a/osu.Game/Screens/Edit/Setup/SetupSection.cs b/osu.Game/Screens/Edit/Setup/SetupSection.cs index 560e6fff67..1f988d62e2 100644 --- a/osu.Game/Screens/Edit/Setup/SetupSection.cs +++ b/osu.Game/Screens/Edit/Setup/SetupSection.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Edit.Setup { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(20), + Spacing = new Vector2(10), Direction = FillDirection.Vertical, } } From 09a2d008d226c8d0448067ce6b37d8d4cce86f87 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 13:41:41 +0300 Subject: [PATCH 235/670] Refrain from attempting to transform null skins --- osu.Game/Rulesets/Ruleset.cs | 2 +- osu.Game/Skinning/LegacySkinTransformer.cs | 7 +++++-- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 9f9f42eda4..9fdaca88fd 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets [CanBeNull] public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().FirstOrDefault(); - public virtual ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => null; + public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null; protected Ruleset() { diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index cd896ab51e..fedd63c7de 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -20,11 +22,12 @@ namespace osu.Game.Skinning /// /// The which is being transformed. /// + [NotNull] protected ISkin Skin { get; } - protected LegacySkinTransformer(ISkin skin) + protected LegacySkinTransformer([NotNull] ISkin skin) { - Skin = skin; + Skin = skin ?? throw new ArgumentNullException(nameof(skin)); } public abstract Drawable GetDrawableComponent(ISkinComponent component); diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index eadf0e05b9..18399cbdb4 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -20,12 +21,12 @@ namespace osu.Game.Skinning protected override Container Content { get; } - public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, ISkin beatmapSkin) + public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin) { this.ruleset = ruleset; this.beatmap = beatmap; - InternalChild = new BeatmapSkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkin, beatmap)) + InternalChild = new BeatmapSkinProvidingContainer(beatmapSkin == null ? null : ruleset.CreateLegacySkinProvider(beatmapSkin, beatmap)) { Child = Content = new Container { From ef2c4fd0d8ec68e8754aa12cf5cf381a5f839ed9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 15:05:08 +0300 Subject: [PATCH 236/670] Make `RulesetSkinProvidingContainer` able to be overriden for testing purposes --- osu.Game/Screens/Play/Player.cs | 4 +++- .../Skinning/RulesetSkinProvidingContainer.cs | 20 +++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bec5181efe..fbcc7ea96f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -234,7 +234,7 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(GameplayBeatmap); - var rulesetSkinProvider = new RulesetSkinProvidingContainer(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin); + var rulesetSkinProvider = CreateRulesetSkinProvider(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin); // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. @@ -315,6 +315,8 @@ namespace osu.Game.Screens.Play protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); + protected virtual RulesetSkinProvidingContainer CreateRulesetSkinProvider(Ruleset ruleset, IBeatmap beatmap, ISkin beatmapSkin) => new RulesetSkinProvidingContainer(ruleset, beatmap, beatmapSkin); + private Drawable createUnderlayComponents() => DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 18399cbdb4..8087043230 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -16,15 +16,15 @@ namespace osu.Game.Skinning ///
public class RulesetSkinProvidingContainer : SkinProvidingContainer { - private readonly Ruleset ruleset; - private readonly IBeatmap beatmap; + protected readonly Ruleset Ruleset; + protected readonly IBeatmap Beatmap; protected override Container Content { get; } public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin) { - this.ruleset = ruleset; - this.beatmap = beatmap; + Ruleset = ruleset; + Beatmap = beatmap; InternalChild = new BeatmapSkinProvidingContainer(beatmapSkin == null ? null : ruleset.CreateLegacySkinProvider(beatmapSkin, beatmap)) { @@ -41,25 +41,25 @@ namespace osu.Game.Skinning [BackgroundDependencyLoader] private void load() { - updateSkins(); + UpdateSkins(); } protected override void OnSourceChanged() { - updateSkins(); + UpdateSkins(); base.OnSourceChanged(); } - private void updateSkins() + protected virtual void UpdateSkins() { SkinSources.Clear(); - SkinSources.Add(ruleset.CreateLegacySkinProvider(skinManager.CurrentSkin.Value, beatmap)); + SkinSources.Add(Ruleset.CreateLegacySkinProvider(skinManager.CurrentSkin.Value, Beatmap)); if (skinManager.CurrentSkin.Value is LegacySkin) - SkinSources.Add(ruleset.CreateLegacySkinProvider(skinManager.DefaultLegacySkin, beatmap)); + SkinSources.Add(Ruleset.CreateLegacySkinProvider(skinManager.DefaultLegacySkin, Beatmap)); - SkinSources.Add(ruleset.CreateLegacySkinProvider(skinManager.DefaultSkin, beatmap)); + SkinSources.Add(Ruleset.CreateLegacySkinProvider(skinManager.DefaultSkin, Beatmap)); } } } From 23d6c366acc34ee9f92a116ce2cace40008f3b04 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 15:36:53 +0300 Subject: [PATCH 237/670] Add method for assigning arbitrary skins to player in test scenes --- osu.Game/Tests/Visual/PlayerTestScene.cs | 8 +++++++ osu.Game/Tests/Visual/TestPlayer.cs | 28 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 088e997de9..e42a043eec 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -10,6 +10,7 @@ using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Skinning; namespace osu.Game.Tests.Visual { @@ -78,6 +79,8 @@ namespace osu.Game.Tests.Visual } Player = CreatePlayer(ruleset); + Player.Skin = GetPlayerSkin(); + LoadScreen(Player); } @@ -93,6 +96,11 @@ namespace osu.Game.Tests.Visual [NotNull] protected abstract Ruleset CreatePlayerRuleset(); + /// + /// Creates an to be put inside the 's ruleset skin providing container. + /// + protected virtual ISkin GetPlayerSkin() => null; + protected sealed override Ruleset CreateRuleset() => CreatePlayerRuleset(); protected virtual TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false); diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 09da4db952..eecf8a2f6e 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -3,13 +3,17 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; +using osu.Game.Skinning; namespace osu.Game.Tests.Visual { @@ -18,6 +22,8 @@ namespace osu.Game.Tests.Visual ///
public class TestPlayer : Player { + public ISkin Skin { get; set; } + protected override bool PauseOnFocusLost { get; } public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; @@ -74,5 +80,27 @@ namespace osu.Game.Tests.Visual { ScoreProcessor.NewJudgement += r => Results.Add(r); } + + protected override RulesetSkinProvidingContainer CreateRulesetSkinProvider(Ruleset ruleset, IBeatmap beatmap, ISkin beatmapSkin) + => new TestSkinProvidingContainer(Skin, ruleset, beatmap, beatmapSkin); + + private class TestSkinProvidingContainer : RulesetSkinProvidingContainer + { + private readonly ISkin skin; + + public TestSkinProvidingContainer(ISkin skin, Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin) + : base(ruleset, beatmap, beatmapSkin) + { + this.skin = skin; + } + + protected override void UpdateSkins() + { + base.UpdateSkins(); + + if (skin != null) + SkinSources.Insert(0, Ruleset.CreateLegacySkinProvider(skin, Beatmap)); + } + } } } From 680791301f70b8476bb40af982e6adba8254bcb5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 16:36:27 +0300 Subject: [PATCH 238/670] Consume new method rather than caching skin sources on top of `Player` --- .../TestSceneLegacyBeatmapSkin.cs | 7 +--- .../TestSceneSkinFallbacks.cs | 26 +----------- .../Tests/Beatmaps/HitObjectSampleTest.cs | 42 ++----------------- .../Beatmaps/LegacyBeatmapSkinColourTest.cs | 32 ++++---------- .../Tests/Visual/LegacySkinPlayerTestScene.cs | 17 +------- 5 files changed, 16 insertions(+), 108 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs index bc3daca16f..0077ff9e3c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs @@ -101,15 +101,10 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("is custom hyper dash fruit colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashFruitColour == TestSkin.HYPER_DASH_FRUIT_COLOUR); } - protected override ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new CatchExposedPlayer(userHasCustomColours); + protected override ExposedPlayer CreateTestPlayer() => new CatchExposedPlayer(); private class CatchExposedPlayer : ExposedPlayer { - public CatchExposedPlayer(bool userHasCustomColours) - : base(userHasCustomColours) - { - } - public Color4 UsableHyperDashColour => GameplayClockContainer.ChildrenOfType() .First() diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index fd523fffcb..2b45818aa9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -21,7 +21,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osu.Game.Storyboards; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { @@ -99,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private AudioManager audio { get; set; } - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin); + protected override ISkin GetPlayerSkin() => testUserSkin; protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, audio, testBeatmapSkin); @@ -116,27 +115,6 @@ namespace osu.Game.Rulesets.Osu.Tests protected override ISkin GetSkin() => skin; } - public class SkinProvidingPlayer : TestPlayer - { - private readonly TestSource userSkin; - - public SkinProvidingPlayer(TestSource userSkin) - { - this.userSkin = userSkin; - } - - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - dependencies.CacheAs(userSkin); - - return dependencies; - } - } - public class TestSource : ISkinSource { private readonly string identifier; @@ -164,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Tests public ISample GetSample(ISampleInfo sampleInfo) => null; - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => default; public IBindable GetConfig(TLookup lookup) => null; + public ISkin FindProvider(Func lookupFunction) => null; public event Action SourceChanged; diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 7ee6c519b7..7af0397726 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -47,15 +47,11 @@ namespace osu.Game.Tests.Beatmaps private readonly TestResourceStore userSkinResourceStore = new TestResourceStore(); private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore(); - private SkinSourceDependencyContainer dependencies; private IBeatmap currentTestBeatmap; protected sealed override bool HasCustomSteps => true; protected override bool Autoplay => true; - protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent))); - protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap; protected sealed override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) @@ -63,6 +59,8 @@ namespace osu.Game.Tests.Beatmaps protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false); + protected override ISkin GetPlayerSkin() => Skin; + protected void CreateTestWithBeatmap(string filename) { CreateTest(() => @@ -109,8 +107,7 @@ namespace osu.Game.Tests.Beatmaps } }; - // Need to refresh the cached skin source to refresh the skin resource store. - dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this)); + Skin = new LegacySkin(userSkinInfo, this); }); } @@ -132,39 +129,6 @@ namespace osu.Game.Tests.Beatmaps #endregion - private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer - { - public ISkinSource SkinSource; - - private readonly IReadOnlyDependencyContainer fallback; - - public SkinSourceDependencyContainer(IReadOnlyDependencyContainer fallback) - { - this.fallback = fallback; - } - - public object Get(Type type) - { - if (type == typeof(ISkinSource)) - return SkinSource; - - return fallback.Get(type); - } - - public object Get(Type type, CacheInfo info) - { - if (type == typeof(ISkinSource)) - return SkinSource; - - return fallback.Get(type, info); - } - - public void Inject(T instance) where T : class - { - // Never used directly - } - } - private class TestResourceStore : IResourceStore { public readonly List PerformedLookups = new List(); diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index 2540b6d7da..1feb3eebbf 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -4,13 +4,11 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Screens.Play; using osu.Game.Skinning; using osu.Game.Tests.Visual; using osuTK.Graphics; @@ -49,36 +47,24 @@ namespace osu.Game.Tests.Beatmaps protected virtual ExposedPlayer LoadBeatmap(bool userHasCustomColours) { - ExposedPlayer player; - Beatmap.Value = testBeatmap; - LoadScreen(player = CreateTestPlayer(userHasCustomColours)); + ExposedPlayer player = CreateTestPlayer(); + + player.Skin = new TestSkin(userHasCustomColours); + + LoadScreen(player); return player; } - protected virtual ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new ExposedPlayer(userHasCustomColours); + protected virtual ExposedPlayer CreateTestPlayer() => new ExposedPlayer(); - protected class ExposedPlayer : Player + protected class ExposedPlayer : TestPlayer { - protected readonly bool UserHasCustomColours; - - public ExposedPlayer(bool userHasCustomColours) - : base(new PlayerConfiguration - { - AllowPause = false, - ShowResults = false, - }) + public ExposedPlayer() + : base(false, false) { - UserHasCustomColours = userHasCustomColours; - } - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new TestSkin(UserHasCustomColours)); - return dependencies; } public IReadOnlyList UsableComboColours => diff --git a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs index b810bbf6ae..14a928d3c1 100644 --- a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs +++ b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs @@ -5,7 +5,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; -using osu.Game.Rulesets; using osu.Game.Skinning; namespace osu.Game.Tests.Visual @@ -15,15 +14,12 @@ namespace osu.Game.Tests.Visual { protected LegacySkin LegacySkin { get; private set; } - private ISkinSource legacySkinSource; - - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(legacySkinSource); + protected override ISkin GetPlayerSkin() => LegacySkin; [BackgroundDependencyLoader] private void load(SkinManager skins) { LegacySkin = new DefaultLegacySkin(skins); - legacySkinSource = new SkinProvidingContainer(LegacySkin); } [SetUpSteps] @@ -48,16 +44,5 @@ namespace osu.Game.Tests.Visual t.Reload(); })); } - - public class SkinProvidingPlayer : TestPlayer - { - [Cached(typeof(ISkinSource))] - private readonly ISkinSource skinSource; - - public SkinProvidingPlayer(ISkinSource skinSource) - { - this.skinSource = skinSource; - } - } } } From 2240e2c39c7b1bf3ebe846b3d8c40119086b0e58 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Jun 2021 17:23:15 +0300 Subject: [PATCH 239/670] Refrain from attempting to clear skin sources in disposal `Drawable.Dispose` is usually in an asynchronous context (async disposals stuff) and therefore this could cause a "collection was modified; enumeration opeartion may not execute" exception. --- osu.Game/Skinning/SkinProvidingContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 74a465a91f..078c666472 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -210,7 +210,8 @@ namespace osu.Game.Skinning if (fallbackSource != null) fallbackSource.SourceChanged -= OnSourceChanged; - SkinSources.Clear(); + foreach (var source in SkinSources.OfType()) + source.SourceChanged -= OnSourceChanged; } private class NoFallbackProxy : ISkinSource From f65f074131b2d2eb545e2614dc6b1f4ccab56fe3 Mon Sep 17 00:00:00 2001 From: ilsubyeega-desu <37479424+ilsubyeega@users.noreply.github.com> Date: Fri, 11 Jun 2021 02:46:29 +0900 Subject: [PATCH 240/670] Add star keyword to FilterQueryParser criteria --- osu.Game/Screens/Select/FilterQueryParser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index ea7f233bea..db2803d29a 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -37,6 +37,7 @@ namespace osu.Game.Screens.Select { switch (key) { + case "star": case "stars": return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2); From e41a5a0fcd2694e575027c69f8bb32c03188d6c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Jun 2021 15:38:56 +0200 Subject: [PATCH 241/670] Add romanised author & title fields --- osu.Game/Beatmaps/MetadataUtils.cs | 47 +++++++++++ .../UserInterfaceV2/LabelledTextBox.cs | 15 ++-- .../Edit/Setup/LabelledRomanisedTextBox.cs | 20 +++++ .../Screens/Edit/Setup/MetadataSection.cs | 78 +++++++++++++++---- 4 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 osu.Game/Beatmaps/MetadataUtils.cs create mode 100644 osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs diff --git a/osu.Game/Beatmaps/MetadataUtils.cs b/osu.Game/Beatmaps/MetadataUtils.cs new file mode 100644 index 0000000000..106de43493 --- /dev/null +++ b/osu.Game/Beatmaps/MetadataUtils.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System.Linq; +using System.Text; + +namespace osu.Game.Beatmaps +{ + /// + /// Groups utility methods used to handle beatmap metadata. + /// + public static class MetadataUtils + { + /// + /// Returns if the character can be used in and fields. + /// Characters not matched by this method can be placed in and . + /// + public static bool IsRomanised(char c) => c <= 0xFF; + + /// + /// Returns if the string can be used in and fields. + /// Strings not matched by this method can be placed in and . + /// + public static bool IsRomanised(string? str) => str == null || str.All(IsRomanised); + + /// + /// Returns a copy of with all characters that do not match removed. + /// + public static string StripNonRomanisedCharacters(string? str) + { + if (string.IsNullOrEmpty(str)) + return string.Empty; + + var stringBuilder = new StringBuilder(str.Length); + + foreach (var c in str) + { + if (IsRomanised(c)) + stringBuilder.Append(c); + } + + return stringBuilder.ToString().Trim(); + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 266eb11319..cf2249685d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -45,14 +45,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Component.BorderColour = colours.Blue; } - protected virtual OsuTextBox CreateTextBox() => new OsuTextBox - { - CommitOnFocusLost = true, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - CornerRadius = CORNER_RADIUS, - }; + protected virtual OsuTextBox CreateTextBox() => new OsuTextBox(); public override bool AcceptsFocus => true; @@ -64,6 +57,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override OsuTextBox CreateComponent() => CreateTextBox().With(t => { + t.CommitOnFocusLost = true; + t.Anchor = Anchor.Centre; + t.Origin = Anchor.Centre; + t.RelativeSizeAxes = Axes.X; + t.CornerRadius = CORNER_RADIUS; + t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText); }); } diff --git a/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs b/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs new file mode 100644 index 0000000000..ee9d86029e --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Edit.Setup +{ + internal class LabelledRomanisedTextBox : LabelledTextBox + { + protected override OsuTextBox CreateTextBox() => new RomanisedTextBox(); + + private class RomanisedTextBox : OsuTextBox + { + protected override bool CanAddCharacter(char character) + => MetadataUtils.IsRomanised(character); + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 6d14f6a66f..c543242957 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -5,6 +5,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Screens.Edit.Setup @@ -12,7 +13,10 @@ namespace osu.Game.Screens.Edit.Setup internal class MetadataSection : SetupSection { private LabelledTextBox artistTextBox; + private LabelledTextBox romanisedArtistTextBox; + private LabelledTextBox titleTextBox; + private LabelledTextBox romanisedTitleTextBox; private LabelledTextBox creatorTextBox; private LabelledTextBox difficultyTextBox; @@ -24,28 +28,43 @@ namespace osu.Game.Screens.Edit.Setup [BackgroundDependencyLoader] private void load() { + var metadata = Beatmap.Metadata; + Children = new[] { - artistTextBox = createTextBox("Artist", Beatmap.Metadata.Artist), - titleTextBox = createTextBox("Title", Beatmap.Metadata.Title), + artistTextBox = createTextBox("Artist", + !string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist), + romanisedArtistTextBox = createTextBox("Romanised Artist", + !string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), + Empty(), - creatorTextBox = createTextBox("Creator", Beatmap.Metadata.AuthorString), - difficultyTextBox = createTextBox("Difficulty Name", Beatmap.BeatmapInfo.Version), - sourceTextBox = createTextBox("Source", Beatmap.Metadata.Source), - tagsTextBox = createTextBox("Tags", Beatmap.Metadata.Tags) + + titleTextBox = createTextBox("Title", + !string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title), + romanisedTitleTextBox = createTextBox("Romanised Title", + !string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), + + Empty(), + + creatorTextBox = createTextBox("Creator", metadata.AuthorString), + difficultyTextBox = createTextBox("Difficulty Name", Beatmap.BeatmapInfo.Version), + sourceTextBox = createTextBox("Source", metadata.Source), + tagsTextBox = createTextBox("Tags", metadata.Tags) }; foreach (var item in Children.OfType()) item.OnCommit += onCommit; } - private LabelledTextBox createTextBox(string label, string initialValue) => new LabelledTextBox - { - Label = label, - FixedLabelWidth = LABEL_WIDTH, - Current = { Value = initialValue }, - TabbableContentContainer = this - }; + private TTextBox createTextBox(string label, string initialValue) + where TTextBox : LabelledTextBox, new() + => new TTextBox + { + Label = label, + FixedLabelWidth = LABEL_WIDTH, + Current = { Value = initialValue }, + TabbableContentContainer = this + }; protected override void LoadComplete() { @@ -53,16 +72,43 @@ namespace osu.Game.Screens.Edit.Setup if (string.IsNullOrEmpty(artistTextBox.Current.Value)) GetContainingInputManager().ChangeFocus(artistTextBox); + + artistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, romanisedArtistTextBox)); + titleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, romanisedTitleTextBox)); + updateReadOnlyState(); + } + + private void transferIfRomanised(string value, LabelledTextBox target) + { + if (MetadataUtils.IsRomanised(value)) + target.Current.Value = value; + + updateReadOnlyState(); + updateMetadata(); + } + + private void updateReadOnlyState() + { + romanisedArtistTextBox.ReadOnly = MetadataUtils.IsRomanised(artistTextBox.Current.Value); + romanisedTitleTextBox.ReadOnly = MetadataUtils.IsRomanised(titleTextBox.Current.Value); } private void onCommit(TextBox sender, bool newText) { if (!newText) return; - // for now, update these on commit rather than making BeatmapMetadata bindables. + // for now, update on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. - Beatmap.Metadata.Artist = artistTextBox.Current.Value; - Beatmap.Metadata.Title = titleTextBox.Current.Value; + updateMetadata(); + } + + private void updateMetadata() + { + Beatmap.Metadata.ArtistUnicode = artistTextBox.Current.Value; + Beatmap.Metadata.Artist = romanisedArtistTextBox.Current.Value; + + Beatmap.Metadata.TitleUnicode = titleTextBox.Current.Value; + Beatmap.Metadata.Title = romanisedTitleTextBox.Current.Value; Beatmap.Metadata.AuthorString = creatorTextBox.Current.Value; Beatmap.BeatmapInfo.Version = difficultyTextBox.Current.Value; From 417aaacc533375855e74b6af788d0f6b5112c74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Jun 2021 20:34:52 +0200 Subject: [PATCH 242/670] Add test coverage for romanised data transfer --- .../Editing/TestSceneMetadataSection.cs | 144 ++++++++++++++++++ .../UserInterfaceV2/LabelledTextBox.cs | 1 + .../Screens/Edit/Setup/MetadataSection.cs | 36 ++--- 3 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs new file mode 100644 index 0000000000..19081f3281 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs @@ -0,0 +1,144 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Setup; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneMetadataSection : OsuTestScene + { + [Cached] + private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap()); + + private TestMetadataSection metadataSection; + + [Test] + public void TestMinimalMetadata() + { + AddStep("set metadata", () => + { + editorBeatmap.Metadata.Artist = "Example Artist"; + editorBeatmap.Metadata.ArtistUnicode = null; + + editorBeatmap.Metadata.Title = "Example Title"; + editorBeatmap.Metadata.TitleUnicode = null; + }); + + createSection(); + + assertArtist("Example Artist"); + assertRomanisedArtist("Example Artist", false); + + assertTitle("Example Title"); + assertRomanisedTitle("Example Title", false); + } + + [Test] + public void TestInitialisationFromNonRomanisedVariant() + { + AddStep("set metadata", () => + { + editorBeatmap.Metadata.ArtistUnicode = "*なみりん"; + editorBeatmap.Metadata.Artist = null; + + editorBeatmap.Metadata.TitleUnicode = "コイシテイク・プラネット"; + editorBeatmap.Metadata.Title = null; + }); + + createSection(); + + assertArtist("*なみりん"); + assertRomanisedArtist(string.Empty, true); + + assertTitle("コイシテイク・プラネット"); + assertRomanisedTitle(string.Empty, true); + } + + [Test] + public void TestInitialisationPreservesOriginalValues() + { + AddStep("set metadata", () => + { + editorBeatmap.Metadata.ArtistUnicode = "*なみりん"; + editorBeatmap.Metadata.Artist = "*namirin"; + + editorBeatmap.Metadata.TitleUnicode = "コイシテイク・プラネット"; + editorBeatmap.Metadata.Title = "Koishiteiku Planet"; + }); + + createSection(); + + assertArtist("*なみりん"); + assertRomanisedArtist("*namirin", true); + + assertTitle("コイシテイク・プラネット"); + assertRomanisedTitle("Koishiteiku Planet", true); + } + + [Test] + public void TestValueTransfer() + { + AddStep("set metadata", () => + { + editorBeatmap.Metadata.ArtistUnicode = "*なみりん"; + editorBeatmap.Metadata.Artist = null; + + editorBeatmap.Metadata.TitleUnicode = "コイシテイク・プラネット"; + editorBeatmap.Metadata.Title = null; + }); + + createSection(); + + AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin"); + assertArtist("*namirin"); + assertRomanisedArtist("*namirin", false); + + AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん"); + assertArtist("*なみりん"); + assertRomanisedArtist("*namirin", true); + + AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori"); + assertTitle("Hitokoto no kyori"); + assertRomanisedTitle("Hitokoto no kyori", false); + + AddStep("set native title", () => metadataSection.TitleTextBox.Current.Value = "ヒトコトの距離"); + assertTitle("ヒトコトの距離"); + assertRomanisedTitle("Hitokoto no kyori", true); + } + + private void createSection() + => AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection()); + + private void assertArtist(string expected) + => AddAssert($"artist is {expected}", () => metadataSection.ArtistTextBox.Current.Value == expected); + + private void assertRomanisedArtist(string expected, bool editable) + { + AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value == expected); + AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable); + } + + private void assertTitle(string expected) + => AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value == expected); + + private void assertRomanisedTitle(string expected, bool editable) + { + AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value == expected); + AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable); + } + + private class TestMetadataSection : MetadataSection + { + public new LabelledTextBox ArtistTextBox => base.ArtistTextBox; + public new LabelledTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox; + + public new LabelledTextBox TitleTextBox => base.TitleTextBox; + public new LabelledTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox; + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index cf2249685d..4da8d6a554 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -21,6 +21,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public bool ReadOnly { + get => Component.ReadOnly; set => Component.ReadOnly = value; } diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index c543242957..9e93b0b038 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -12,11 +12,11 @@ namespace osu.Game.Screens.Edit.Setup { internal class MetadataSection : SetupSection { - private LabelledTextBox artistTextBox; - private LabelledTextBox romanisedArtistTextBox; + protected LabelledTextBox ArtistTextBox; + protected LabelledTextBox RomanisedArtistTextBox; - private LabelledTextBox titleTextBox; - private LabelledTextBox romanisedTitleTextBox; + protected LabelledTextBox TitleTextBox; + protected LabelledTextBox RomanisedTitleTextBox; private LabelledTextBox creatorTextBox; private LabelledTextBox difficultyTextBox; @@ -32,16 +32,16 @@ namespace osu.Game.Screens.Edit.Setup Children = new[] { - artistTextBox = createTextBox("Artist", + ArtistTextBox = createTextBox("Artist", !string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist), - romanisedArtistTextBox = createTextBox("Romanised Artist", + RomanisedArtistTextBox = createTextBox("Romanised Artist", !string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), Empty(), - titleTextBox = createTextBox("Title", + TitleTextBox = createTextBox("Title", !string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title), - romanisedTitleTextBox = createTextBox("Romanised Title", + RomanisedTitleTextBox = createTextBox("Romanised Title", !string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), Empty(), @@ -70,11 +70,11 @@ namespace osu.Game.Screens.Edit.Setup { base.LoadComplete(); - if (string.IsNullOrEmpty(artistTextBox.Current.Value)) - GetContainingInputManager().ChangeFocus(artistTextBox); + if (string.IsNullOrEmpty(ArtistTextBox.Current.Value)) + GetContainingInputManager().ChangeFocus(ArtistTextBox); - artistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, romanisedArtistTextBox)); - titleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, romanisedTitleTextBox)); + ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); + TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); updateReadOnlyState(); } @@ -89,8 +89,8 @@ namespace osu.Game.Screens.Edit.Setup private void updateReadOnlyState() { - romanisedArtistTextBox.ReadOnly = MetadataUtils.IsRomanised(artistTextBox.Current.Value); - romanisedTitleTextBox.ReadOnly = MetadataUtils.IsRomanised(titleTextBox.Current.Value); + RomanisedArtistTextBox.ReadOnly = MetadataUtils.IsRomanised(ArtistTextBox.Current.Value); + RomanisedTitleTextBox.ReadOnly = MetadataUtils.IsRomanised(TitleTextBox.Current.Value); } private void onCommit(TextBox sender, bool newText) @@ -104,11 +104,11 @@ namespace osu.Game.Screens.Edit.Setup private void updateMetadata() { - Beatmap.Metadata.ArtistUnicode = artistTextBox.Current.Value; - Beatmap.Metadata.Artist = romanisedArtistTextBox.Current.Value; + Beatmap.Metadata.ArtistUnicode = ArtistTextBox.Current.Value; + Beatmap.Metadata.Artist = RomanisedArtistTextBox.Current.Value; - Beatmap.Metadata.TitleUnicode = titleTextBox.Current.Value; - Beatmap.Metadata.Title = romanisedTitleTextBox.Current.Value; + Beatmap.Metadata.TitleUnicode = TitleTextBox.Current.Value; + Beatmap.Metadata.Title = RomanisedTitleTextBox.Current.Value; Beatmap.Metadata.AuthorString = creatorTextBox.Current.Value; Beatmap.BeatmapInfo.Version = difficultyTextBox.Current.Value; From 24c249b17eda95ce51255057753bdcda949131d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Jun 2021 22:40:49 +0200 Subject: [PATCH 243/670] Add test coverage --- .../NonVisual/Filtering/FilterQueryParserTest.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 49389e67aa..9bd262a569 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -33,10 +33,11 @@ namespace osu.Game.Tests.NonVisual.Filtering * outside of the range. */ - [Test] - public void TestApplyStarQueries() + [TestCase("star")] + [TestCase("stars")] + public void TestApplyStarQueries(string variant) { - const string query = "stars<4 easy"; + string query = $"{variant}<4 easy"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.AreEqual("easy", filterCriteria.SearchText.Trim()); From 38bf04d7ff15df0575d9831baefb608f837d66a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 13:25:09 +0900 Subject: [PATCH 244/670] Give more space for time values to allow for negative offsets --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index fe63138d28..7a98cf63c3 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private EditorClock clock { get; set; } - public const float TIMING_COLUMN_WIDTH = 220; + public const float TIMING_COLUMN_WIDTH = 230; public IEnumerable ControlGroups { @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Edit.Timing { Text = group.Time.ToEditorFormattedString(), Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold), - Width = 60, + Width = 70, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, From 375f64ffd1b44fff175b736ad2d4ff4e30419af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Jun 2021 06:38:53 +0200 Subject: [PATCH 245/670] Check empty string more explicitly in `IsRomanised()` Co-authored-by: Dan Balasescu --- osu.Game/Beatmaps/MetadataUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/MetadataUtils.cs b/osu.Game/Beatmaps/MetadataUtils.cs index 106de43493..56f5e3fe35 100644 --- a/osu.Game/Beatmaps/MetadataUtils.cs +++ b/osu.Game/Beatmaps/MetadataUtils.cs @@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps /// Returns if the string can be used in and fields. /// Strings not matched by this method can be placed in and . ///
- public static bool IsRomanised(string? str) => str == null || str.All(IsRomanised); + public static bool IsRomanised(string? str) => string.IsNullOrEmpty(str) || str.All(IsRomanised); /// /// Returns a copy of with all characters that do not match removed. From bc3b7233ab3ea19389f5ce449b95631a3f74ca3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 14:17:29 +0900 Subject: [PATCH 246/670] Show osu!taiko centre/rim colouring in editor timeline Closes #13443. --- osu.Game.Rulesets.Taiko/Objects/Hit.cs | 17 +++++- .../Objects/Types/IHasAccentColour.cs | 19 +++++++ .../Timeline/TimelineHitObjectBlueprint.cs | 53 +++++++++++++------ 3 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 osu.Game/Rulesets/Objects/Types/IHasAccentColour.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index b4ed242893..bbee54d139 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -3,14 +3,19 @@ using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Game.Audio; +using osu.Game.Rulesets.Objects.Types; +using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Objects { - public class Hit : TaikoStrongableHitObject + public class Hit : TaikoStrongableHitObject, IHasDisplayColour { public readonly Bindable TypeBindable = new Bindable(); + public Bindable DisplayColour { get; } = new Bindable(colour_centre); + /// /// The that actuates this . /// @@ -20,9 +25,17 @@ namespace osu.Game.Rulesets.Taiko.Objects set => TypeBindable.Value = value; } + private static readonly Color4 colour_centre = Color4Extensions.FromHex(@"bb1177"); + private static readonly Color4 colour_rim = Color4Extensions.FromHex(@"2299bb"); + public Hit() { - TypeBindable.BindValueChanged(_ => updateSamplesFromType()); + TypeBindable.BindValueChanged(_ => + { + updateSamplesFromType(); + DisplayColour.Value = Type == HitType.Centre ? colour_centre : colour_rim; + }); + SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples()); } diff --git a/osu.Game/Rulesets/Objects/Types/IHasAccentColour.cs b/osu.Game/Rulesets/Objects/Types/IHasAccentColour.cs new file mode 100644 index 0000000000..8807b802d8 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasAccentColour.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Objects.Types +{ + /// + /// A HitObject which has a preferred display colour. Will be used for editor timeline display. + /// + public interface IHasDisplayColour + { + /// + /// The current display colour of this hit object. + /// + Bindable DisplayColour { get; } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index dbe689be2f..377c37c4c7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -39,6 +39,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Bindable indexInCurrentComboBindable; private Bindable comboIndexBindable; + private Bindable displayColourBindable; private readonly ExtendableCircle circle; private readonly Border border; @@ -108,44 +109,64 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadComplete(); - if (Item is IHasComboInformation comboInfo) + switch (Item) { - indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); - indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true); + case IHasDisplayColour displayColour: + displayColourBindable = displayColour.DisplayColour.GetBoundCopy(); + displayColourBindable.BindValueChanged(_ => updateColour(), true); + break; - comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy(); - comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); + case IHasComboInformation comboInfo: + indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); + indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true); - skin.SourceChanged += updateComboColour; + comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy(); + comboIndexBindable.BindValueChanged(_ => updateColour(), true); + + skin.SourceChanged += updateColour; + break; } } protected override void OnSelected() { // base logic hides selected blueprints when not selected, but timeline doesn't do that. - updateComboColour(); + updateColour(); } protected override void OnDeselected() { // base logic hides selected blueprints when not selected, but timeline doesn't do that. - updateComboColour(); + updateColour(); } private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString(); - private void updateComboColour() + private void updateColour() { - if (!(Item is IHasComboInformation combo)) - return; + Color4 colour; - var comboColours = skin.GetConfig>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty(); - var comboColour = combo.GetComboColour(comboColours); + switch (Item) + { + case IHasDisplayColour displayColour: + colour = displayColour.DisplayColour.Value; + break; + + case IHasComboInformation combo: + { + var comboColours = skin.GetConfig>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty(); + colour = combo.GetComboColour(comboColours); + break; + } + + default: + return; + } if (IsSelected) { border.Show(); - comboColour = comboColour.Lighten(0.3f); + colour = colour.Lighten(0.3f); } else { @@ -153,9 +174,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } if (Item is IHasDuration duration && duration.Duration > 0) - circle.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f)); + circle.Colour = ColourInfo.GradientHorizontal(colour, colour.Lighten(0.4f)); else - circle.Colour = comboColour; + circle.Colour = colour; var col = circle.Colour.TopLeft.Linear; colouredComponents.Colour = OsuColour.ForegroundTextColourFor(col); From 9c34cb07778b19c843172ead6fdc1cef62c1a617 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 14:20:08 +0900 Subject: [PATCH 247/670] Share colour constants with default drawable piece implementations --- osu.Game.Rulesets.Taiko/Objects/Hit.cs | 8 ++++---- .../Skinning/Default/CentreHitCirclePiece.cs | 3 ++- .../Skinning/Default/RimHitCirclePiece.cs | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index bbee54d139..2038da9344 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Objects { public readonly Bindable TypeBindable = new Bindable(); - public Bindable DisplayColour { get; } = new Bindable(colour_centre); + public Bindable DisplayColour { get; } = new Bindable(COLOUR_CENTRE); /// /// The that actuates this . @@ -25,15 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Objects set => TypeBindable.Value = value; } - private static readonly Color4 colour_centre = Color4Extensions.FromHex(@"bb1177"); - private static readonly Color4 colour_rim = Color4Extensions.FromHex(@"2299bb"); + public static readonly Color4 COLOUR_CENTRE = Color4Extensions.FromHex(@"bb1177"); + public static readonly Color4 COLOUR_RIM = Color4Extensions.FromHex(@"2299bb"); public Hit() { TypeBindable.BindValueChanged(_ => { updateSamplesFromType(); - DisplayColour.Value = Type == HitType.Centre ? colour_centre : colour_rim; + DisplayColour.Value = Type == HitType.Centre ? COLOUR_CENTRE : COLOUR_RIM; }); SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples()); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs index f65bb54726..455b2fc596 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; +using osu.Game.Rulesets.Taiko.Objects; using osuTK; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default [BackgroundDependencyLoader] private void load(OsuColour colours) { - AccentColour = colours.PinkDarker; + AccentColour = Hit.COLOUR_CENTRE; } /// diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs index ca2ab301be..bd21d511b1 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; +using osu.Game.Rulesets.Taiko.Objects; using osuTK; using osuTK.Graphics; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default [BackgroundDependencyLoader] private void load(OsuColour colours) { - AccentColour = colours.BlueDarker; + AccentColour = Hit.COLOUR_RIM; } /// From 562cfe8703bdafb09bbc3292975a8fef7817d969 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 14:34:18 +0900 Subject: [PATCH 248/670] Fix filename not matching type rename --- .../Objects/Types/{IHasAccentColour.cs => IHasDisplayColour.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game/Rulesets/Objects/Types/{IHasAccentColour.cs => IHasDisplayColour.cs} (100%) diff --git a/osu.Game/Rulesets/Objects/Types/IHasAccentColour.cs b/osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs similarity index 100% rename from osu.Game/Rulesets/Objects/Types/IHasAccentColour.cs rename to osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs From b9050f91a440c8d10cd3637d0fd179c38298a9c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 14:49:35 +0900 Subject: [PATCH 249/670] Expose as `Skin`s and consume `SkinInfo` from instances --- osu.Game/Skinning/SkinManager.cs | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 89f166dc2a..ffdbadf54c 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -48,19 +48,15 @@ namespace osu.Game.Skinning protected override string ImportFromStablePath => "Skins"; - private readonly Skin defaultSkin; - /// /// An providing the resources of the default skin. /// - public ISkin DefaultSkin => defaultSkin; - - private readonly Skin defaultLegacySkin; + public Skin DefaultSkin { get; } /// /// An providing the resources of the default legacy skin. /// - public ISkin DefaultLegacySkin => defaultLegacySkin; + public Skin DefaultLegacySkin { get; } public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore resources, AudioManager audio) : base(storage, contextFactory, new SkinStore(contextFactory, storage), host) @@ -69,12 +65,12 @@ namespace osu.Game.Skinning this.host = host; this.resources = resources; - defaultLegacySkin = new DefaultLegacySkin(this); - defaultSkin = new DefaultSkin(this); + DefaultLegacySkin = new DefaultLegacySkin(this); + DefaultSkin = new DefaultSkin(this); CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue); - CurrentSkin.Value = defaultSkin; + CurrentSkin.Value = DefaultSkin; CurrentSkin.ValueChanged += skin => { if (skin.NewValue.SkinInfo != CurrentSkinInfo.Value) @@ -93,8 +89,8 @@ namespace osu.Game.Skinning public List GetAllUsableSkins() { var userSkins = GetAllUserSkins(); - userSkins.Insert(0, SkinInfo.Default); - userSkins.Insert(1, Skinning.DefaultLegacySkin.Info); + userSkins.Insert(0, DefaultSkin.SkinInfo); + userSkins.Insert(1, DefaultLegacySkin.SkinInfo); return userSkins; } @@ -236,11 +232,11 @@ namespace osu.Game.Skinning if (lookupFunction(CurrentSkin.Value)) return CurrentSkin.Value; - if (CurrentSkin.Value is LegacySkin && lookupFunction(defaultLegacySkin)) - return defaultLegacySkin; + if (CurrentSkin.Value is LegacySkin && lookupFunction(DefaultLegacySkin)) + return DefaultLegacySkin; - if (lookupFunction(defaultSkin)) - return defaultSkin; + if (lookupFunction(DefaultSkin)) + return DefaultSkin; return null; } @@ -254,11 +250,11 @@ namespace osu.Game.Skinning // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. // When attempting to address this, we may want to move the full DefaultLegacySkin fallback logic to within Player itself (to better allow // for beatmap skin visibility). - if (CurrentSkin.Value is LegacySkin && lookupFunction(defaultLegacySkin) is T legacySourced) + if (CurrentSkin.Value is LegacySkin && lookupFunction(DefaultLegacySkin) is T legacySourced) return legacySourced; // Finally fall back to the (non-legacy) default. - return lookupFunction(defaultSkin); + return lookupFunction(DefaultSkin); } #region IResourceStorageProvider From debd359d2e5d36361757d37188b4498c530bdee0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 14:50:21 +0900 Subject: [PATCH 250/670] Update xmldoc --- osu.Game/Skinning/SkinManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index ffdbadf54c..660f44772c 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -49,12 +49,12 @@ namespace osu.Game.Skinning protected override string ImportFromStablePath => "Skins"; /// - /// An providing the resources of the default skin. + /// The default skin. /// public Skin DefaultSkin { get; } /// - /// An providing the resources of the default legacy skin. + /// The default legacy skin. /// public Skin DefaultLegacySkin { get; } From 8d0840020b4048839bcfd5f7676cd79ef24bd1be Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 11 Jun 2021 15:33:13 +0900 Subject: [PATCH 251/670] Specify legacy skin version of old-skin testing skin Old-style catcher sprite is not supported for all versions --- osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini new file mode 100644 index 0000000000..1596c95912 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini @@ -0,0 +1,2 @@ +[General] +Version: 1.0 From 7f7c2c73e002177f7518ca876e75b735024ebaea Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 11 Jun 2021 15:39:06 +0900 Subject: [PATCH 252/670] Move catcher movement logic of `Catcher` to `CatcherArea` --- osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs | 6 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 76 ++++-------------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 78 +++++++++++++++++-- osu.Game.Rulesets.Catch/UI/Direction.cs | 11 +++ 4 files changed, 99 insertions(+), 72 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/UI/Direction.cs diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs index 1e42c6a240..73b60f51a4 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs @@ -33,13 +33,13 @@ namespace osu.Game.Rulesets.Catch.Mods private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition { - private readonly Catcher catcher; + private readonly CatcherArea catcherArea; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public MouseInputHelper(CatchPlayfield playfield) { - catcher = playfield.CatcherArea.MovableCatcher; + catcherArea = playfield.CatcherArea; RelativeSizeAxes = Axes.Both; } @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods protected override bool OnMouseMove(MouseMoveEvent e) { - catcher.UpdatePosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH); + catcherArea.SetCatcherPosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH); return base.OnMouseMove(e); } } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 4af2243ed4..ee2986c73c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Textures; -using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -26,7 +25,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { - public class Catcher : SkinReloadableDrawable, IKeyBindingHandler + public class Catcher : SkinReloadableDrawable { /// /// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail @@ -54,6 +53,11 @@ namespace osu.Game.Rulesets.Catch.UI /// public const double BASE_SPEED = 1.0; + /// + /// The current speed of the catcher. + /// + public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier; + /// /// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught". /// @@ -96,7 +100,7 @@ namespace osu.Game.Rulesets.Catch.UI public bool Dashing { get => dashing; - protected set + set { if (value == dashing) return; @@ -106,6 +110,12 @@ namespace osu.Game.Rulesets.Catch.UI } } + public Direction VisualDirection + { + get => Scale.X > 0 ? Direction.Right : Direction.Left; + set => Scale = new Vector2((value == Direction.Right ? 1 : -1) * Math.Abs(Scale.X), Scale.Y); + } + /// /// Width of the area that can be used to attempt catches during gameplay. /// @@ -116,8 +126,6 @@ namespace osu.Game.Rulesets.Catch.UI private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR; private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR; - private int currentDirection; - private double hyperDashModifier = 1; private int hyperDashDirection; private float hyperDashTargetPosition; @@ -315,55 +323,6 @@ namespace osu.Game.Rulesets.Catch.UI } } - public void UpdatePosition(float position) - { - position = Math.Clamp(position, 0, CatchPlayfield.WIDTH); - - if (position == X) - return; - - Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y); - X = position; - } - - public bool OnPressed(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection--; - return true; - - case CatchAction.MoveRight: - currentDirection++; - return true; - - case CatchAction.Dash: - Dashing = true; - return true; - } - - return false; - } - - public void OnReleased(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection++; - break; - - case CatchAction.MoveRight: - currentDirection--; - break; - - case CatchAction.Dash: - Dashing = false; - break; - } - } - /// /// Drop any fruit off the plate. /// @@ -405,15 +364,6 @@ namespace osu.Game.Rulesets.Catch.UI { base.Update(); - if (currentDirection == 0) return; - - var direction = Math.Sign(currentDirection); - - var dashModifier = Dashing ? 1 : 0.5; - var speed = BASE_SPEED * dashModifier * hyperDashModifier; - - UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed)); - // Correct overshooting. if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || (hyperDashDirection < 0 && hyperDashTargetPosition > X)) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 44adbd5512..cdb15c2b4c 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -1,8 +1,10 @@ // 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.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects.Drawables; @@ -14,13 +16,20 @@ using osuTK; namespace osu.Game.Rulesets.Catch.UI { - public class CatcherArea : Container + public class CatcherArea : Container, IKeyBindingHandler { public const float CATCHER_SIZE = 106.75f; public readonly Catcher MovableCatcher; private readonly CatchComboDisplay comboDisplay; + /// + /// -1 when only left button is pressed. + /// 1 when only right button is pressed. + /// 0 when none or both left and right buttons are pressed. + /// + private int currentDirection; + public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); @@ -63,16 +72,73 @@ namespace osu.Game.Rulesets.Catch.UI MovableCatcher.OnRevertResult(hitObject, result); } + protected override void Update() + { + base.Update(); + + var replayState = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState; + + SetCatcherPosition( + replayState?.CatcherX ?? + (float)(MovableCatcher.X + MovableCatcher.Speed * currentDirection * Clock.ElapsedFrameTime)); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - var state = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState; - - if (state?.CatcherX != null) - MovableCatcher.X = state.CatcherX.Value; - comboDisplay.X = MovableCatcher.X; } + + public void SetCatcherPosition(float X) + { + float lastPosition = MovableCatcher.X; + float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH); + + MovableCatcher.X = newPosition; + + if (lastPosition < newPosition) + MovableCatcher.VisualDirection = Direction.Right; + else if (lastPosition > newPosition) + MovableCatcher.VisualDirection = Direction.Left; + } + + public bool OnPressed(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection--; + return true; + + case CatchAction.MoveRight: + currentDirection++; + return true; + + case CatchAction.Dash: + MovableCatcher.Dashing = true; + return true; + } + + return false; + } + + public void OnReleased(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection++; + break; + + case CatchAction.MoveRight: + currentDirection--; + break; + + case CatchAction.Dash: + MovableCatcher.Dashing = false; + break; + } + } } } diff --git a/osu.Game.Rulesets.Catch/UI/Direction.cs b/osu.Game.Rulesets.Catch/UI/Direction.cs new file mode 100644 index 0000000000..65f064b7fb --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/Direction.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Catch.UI +{ + public enum Direction + { + Right = 1, + Left = -1 + } +} From 33aec57238bd02076635d3894d0e1dc5f6bd4747 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 15:45:34 +0900 Subject: [PATCH 253/670] Replace 1.0 version in old skin test assets with none --- osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini | 2 +- osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini | 4 ++-- osu.Game.Tests/Resources/old-skin/skin.ini | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini index 1596c95912..94c6b5b58d 100644 --- a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini +++ b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini @@ -1,2 +1,2 @@ [General] -Version: 1.0 +// no version specified means v1 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini index 89bcd68343..06dfa6b7be 100644 --- a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini +++ b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini @@ -1,6 +1,6 @@ [General] -Version: 1.0 +// no version specified means v1 [Fonts] HitCircleOverlap: 3 -ScoreOverlap: 3 \ No newline at end of file +ScoreOverlap: 3 diff --git a/osu.Game.Tests/Resources/old-skin/skin.ini b/osu.Game.Tests/Resources/old-skin/skin.ini index 5369de24e9..94c6b5b58d 100644 --- a/osu.Game.Tests/Resources/old-skin/skin.ini +++ b/osu.Game.Tests/Resources/old-skin/skin.ini @@ -1,2 +1,2 @@ [General] -Version: 1.0 \ No newline at end of file +// no version specified means v1 From 46b379899e0d7299ff8ebb37d3de83701414b9aa Mon Sep 17 00:00:00 2001 From: Ivan Pavluk Date: Fri, 11 Jun 2021 14:07:38 +0700 Subject: [PATCH 254/670] add taiko hd mod (2nd attempt) --- .../Mods/TaikoModHidden.cs | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 7739ecaf5b..eeebe66b77 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -1,23 +1,95 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModHidden : ModHidden + public class TaikoModHidden : ModHidden, IApplicableToDifficulty { public override string Description => @"Beats fade out before you hit them!"; public override double ScoreMultiplier => 1.06; - public override bool HasImplementation => false; + + // In stable Taiko, hit position is 160, so playfield is essentially 160 pixels shorter + // than actual screen width. Normalized screen height is 480, so on a 4:3 screen the + // playfield ratio will actually be (640 - 160) / 480 = 1 + // For 16:9 resolutions, screen width with normalized height becomes 853.33333 instead, + // meaning 16:9 playfield ratio is (853.333 - 160) / 480 = 1.444444. + // Thus, 4:3 ratio / 16:9 ratio = 1 / 1.4444 = 9 / 13 + private const double hd_sv_scale = 9.0 / 13.0; + private BeatmapDifficulty difficulty; + private ControlPointInfo controlPointInfo; + + [SettingSource("Hide Time", "Multiplies the time the note stays hidden")] + public BindableNumber VisibilityMod { get; } = new BindableDouble + { + MinValue = 0.5, + // Max visibility is only to be used with max playfield size + MaxValue = 1.5, + Default = 1.0, + Value = 1.0, + Precision = 0.01, + }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { + ApplyNormalVisibilityState(hitObject, state); + } + + protected double MultiplierAt(double position) + { + var beatLength = controlPointInfo.TimingPointAt(position)?.BeatLength; + var speedMultiplier = controlPointInfo.DifficultyPointAt(position)?.SpeedMultiplier; + return difficulty.SliderMultiplier * (speedMultiplier ?? 1.0) * TimingControlPoint.DEFAULT_BEAT_LENGTH / (beatLength ?? TimingControlPoint.DEFAULT_BEAT_LENGTH); } protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { + switch (hitObject) + { + case DrawableDrumRollTick _: + break; + + case DrawableHit _: + break; + + default: + return; + } + + // I *think* it's like this because stable's default velocity multiplier is 1.4 + var preempt = 14000 / MultiplierAt(hitObject.HitObject.StartTime) * VisibilityMod.Value; + var start = hitObject.HitObject.StartTime - preempt * 0.6; + var duration = preempt * 0.3; + + using (hitObject.BeginAbsoluteSequence(start)) + { + // With 0 opacity the object is dying, and if I set a lifetime other issues appear... + // Ideally these need to be fixed, but I lack the knowledge to do it, and this is good enough anyway. + hitObject.FadeTo(0.0005f, duration); + } + } + + public void ReadFromDifficulty(BeatmapDifficulty difficulty) + { + } + + public void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + this.difficulty = difficulty; + difficulty.SliderMultiplier /= hd_sv_scale; + } + + public override void ApplyToBeatmap(IBeatmap beatmap) + { + controlPointInfo = beatmap.ControlPointInfo; } } } From c00f9ae4b750d905a7165d3d7bd8b69782f95433 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 16:11:37 +0900 Subject: [PATCH 255/670] Reword settings text --- .../Settings/Sections/Online/AlertsAndPrivacySettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs index f9f5b927b7..9f70d23c27 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs @@ -18,12 +18,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online { new SettingsCheckbox { - LabelText = "Show a notification popup when someone says your name", + LabelText = "Show a notification when someone mentions your name", Current = config.GetBindable(OsuSetting.ChatHighlightName) }, new SettingsCheckbox { - LabelText = "Show private message notifications", + LabelText = "Show a notification when you receive a private message", Current = config.GetBindable(OsuSetting.ChatMessageNotification) }, }; From f00967388a5296f5d6a1dc193bf324ba279466a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 16:17:42 +0900 Subject: [PATCH 256/670] Refactor tests a bit --- .../Visual/Online/TestSceneMessageNotifier.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 28ec3e91a1..80c0c86fa3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -197,26 +197,34 @@ namespace osu.Game.Tests.Visual.Online private class TestContainer : Container { - private readonly Channel[] channels; - - public TestContainer(Channel[] channels) => this.channels = channels; - [Cached] public ChannelManager ChannelManager { get; } = new ChannelManager(); [Cached] public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay(); - [Cached] - public MessageNotifier MessageNotifier { get; } = new MessageNotifier(); - [Cached] public ChatOverlay ChatOverlay { get; } = new ChatOverlay(); + private readonly MessageNotifier messageNotifier = new MessageNotifier(); + + private readonly Channel[] channels; + + public TestContainer(Channel[] channels) + { + this.channels = channels; + } + [BackgroundDependencyLoader] private void load() { - AddRange(new Drawable[] { ChannelManager, ChatOverlay, NotificationOverlay, MessageNotifier }); + Children = new Drawable[] + { + ChannelManager, + ChatOverlay, + NotificationOverlay, + messageNotifier, + }; ((BindableList)ChannelManager.AvailableChannels).AddRange(channels); From 16e3a197380524a86031383e7eded8069681f40a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 16:18:51 +0900 Subject: [PATCH 257/670] Fix notification overlay not being in correct place in test scene --- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 80c0c86fa3..d193856217 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -201,7 +201,11 @@ namespace osu.Game.Tests.Visual.Online public ChannelManager ChannelManager { get; } = new ChannelManager(); [Cached] - public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay(); + public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }; [Cached] public ChatOverlay ChatOverlay { get; } = new ChatOverlay(); From 296761ade5ea6912fb39875ff87aea45fa327b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Jun 2021 09:18:24 +0200 Subject: [PATCH 258/670] Add missing `CurrentSkin` null check in DHO disposal --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5fd2b2493e..7fc35fc778 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -716,7 +716,8 @@ namespace osu.Game.Rulesets.Objects.Drawables if (HitObject != null) HitObject.DefaultsApplied -= onDefaultsApplied; - CurrentSkin.SourceChanged -= skinSourceChanged; + if (CurrentSkin != null) + CurrentSkin.SourceChanged -= skinSourceChanged; } } From 139401a04a4813072362d6807271cc07e01b0e22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 16:27:31 +0900 Subject: [PATCH 259/670] Inline and refactor overly verbose `MessageNotifier` code --- osu.Game/Online/Chat/MessageNotifier.cs | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 53bd3c61c3..685545f08c 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -45,8 +45,7 @@ namespace osu.Game.Online.Chat notifyOnPM = config.GetBindable(OsuSetting.ChatMessageNotification); localUser.BindTo(api.LocalUser); - // Listen for new messages - joinedChannels.CollectionChanged += channelsChanged; + joinedChannels.BindCollectionChanged(channelsChanged); joinedChannels.BindTo(channelManager.JoinedChannels); } @@ -70,28 +69,14 @@ namespace osu.Game.Online.Chat private void newMessagesArrived(IEnumerable messages) { - if (messages == null || !messages.Any()) + if (!messages.Any()) return; - HandleMessages(messages.First().ChannelId, messages); - } - - /// - /// Searches for a channel with the matching , returns when none found. - /// - private Channel fetchJoinedChannel(long channelId) - { - return channelManager.JoinedChannels.SingleOrDefault(c => c.Id == channelId); - } - - public void HandleMessages(long channelId, IEnumerable messages) - { - // Fetch channel object - var channel = fetchJoinedChannel(channelId); + var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == messages.First().ChannelId); if (channel == null) { - Logger.Log($"Couldn't resolve channel id {channelId}", LoggingTarget.Information); + Logger.Log($"Couldn't resolve channel id {messages.First().ChannelId}", LoggingTarget.Information); return; } From 3d645608eb9b46a94c5f3f453c929686b160dfc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 16:28:53 +0900 Subject: [PATCH 260/670] Remove nullability of DI dependencies and fix incorrect load order --- osu.Game/Online/Chat/MessageNotifier.cs | 18 +++++++----------- osu.Game/OsuGame.cs | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 685545f08c..2a676738d0 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -24,13 +24,13 @@ namespace osu.Game.Online.Chat /// public class MessageNotifier : Component { - [Resolved(CanBeNull = true)] - private NotificationOverlay notificationOverlay { get; set; } + [Resolved] + private NotificationOverlay notifications { get; set; } - [Resolved(CanBeNull = true)] + [Resolved] private ChatOverlay chatOverlay { get; set; } - [Resolved(CanBeNull = true)] + [Resolved] private ChannelManager channelManager { get; set; } private Bindable notifyOnMention; @@ -81,7 +81,7 @@ namespace osu.Game.Online.Chat } // Only send notifications, if ChatOverlay and the target channel aren't visible. - if (chatOverlay?.IsPresent == true && channelManager.CurrentChannel.Value == channel) + if (chatOverlay.IsPresent && channelManager.CurrentChannel.Value == channel) return; foreach (var message in messages.OrderByDescending(m => m.Id)) @@ -115,9 +115,7 @@ namespace osu.Game.Online.Chat if (channel.Id != message.ChannelId) throw new ArgumentException("The provided channel doesn't match with the channel id provided by the message parameter.", nameof(channel)); - var notification = new PrivateMessageNotification(message.Sender.Username, channel); - notificationOverlay?.Post(notification); - + notifications.Post(new PrivateMessageNotification(message.Sender.Username, channel)); return true; } @@ -135,9 +133,7 @@ namespace osu.Game.Online.Chat if (channel.Id != message.ChannelId) throw new ArgumentException("The provided channel doesn't match with the channel id provided by the message parameter.", nameof(channel)); - var notification = new MentionNotification(message.Sender.Username, channel); - notificationOverlay?.Post(notification); - + notifications.Post(new MentionNotification(message.Sender.Username, channel)); return true; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e753dd1424..0cd31def2e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -726,8 +726,8 @@ namespace osu.Game loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); - loadComponentSingleFile(new MessageNotifier(), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); + loadComponentSingleFile(new MessageNotifier(), AddInternal, true); loadComponentSingleFile(Settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true); var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true); loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true); From 20759657def9491b25d49f12d65a6b81d9c8e6c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 16:37:31 +0900 Subject: [PATCH 261/670] Rename configuration variables and refactor lots more --- osu.Game/Configuration/OsuConfigManager.cs | 8 +- osu.Game/Online/Chat/MessageNotifier.cs | 96 ++++++++----------- .../Online/AlertsAndPrivacySettings.cs | 4 +- 3 files changed, 46 insertions(+), 62 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 1c92c16333..60a0d5a0ac 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -61,8 +61,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ShowOnlineExplicitContent, false); - SetDefault(OsuSetting.ChatHighlightName, true); - SetDefault(OsuSetting.ChatMessageNotification, true); + SetDefault(OsuSetting.NotifyOnUsernameMentioned, true); + SetDefault(OsuSetting.NotifyOnPrivateMessage, true); // Audio SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); @@ -262,8 +262,8 @@ namespace osu.Game.Configuration ScalingSizeY, UIScale, IntroSequence, - ChatHighlightName, - ChatMessageNotification, + NotifyOnUsernameMentioned, + NotifyOnPrivateMessage, UIHoldActivationDelay, HitLighting, MenuBackgroundSource, diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 2a676738d0..b8947d6e47 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; -using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Online.API; @@ -33,16 +32,18 @@ namespace osu.Game.Online.Chat [Resolved] private ChannelManager channelManager { get; set; } - private Bindable notifyOnMention; - private Bindable notifyOnPM; + private Bindable notifyOnUsername; + private Bindable notifyOnPrivateMessage; + private readonly IBindable localUser = new Bindable(); private readonly IBindableList joinedChannels = new BindableList(); [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { - notifyOnMention = config.GetBindable(OsuSetting.ChatHighlightName); - notifyOnPM = config.GetBindable(OsuSetting.ChatMessageNotification); + notifyOnUsername = config.GetBindable(OsuSetting.NotifyOnUsernameMentioned); + notifyOnPrivateMessage = config.GetBindable(OsuSetting.NotifyOnPrivateMessage); + localUser.BindTo(api.LocalUser); joinedChannels.BindCollectionChanged(channelsChanged); @@ -55,19 +56,19 @@ namespace osu.Game.Online.Chat { case NotifyCollectionChangedAction.Add: foreach (var channel in e.NewItems.Cast()) - channel.NewMessagesArrived += newMessagesArrived; + channel.NewMessagesArrived += checkNewMessages; break; case NotifyCollectionChangedAction.Remove: foreach (var channel in e.OldItems.Cast()) - channel.NewMessagesArrived -= newMessagesArrived; + channel.NewMessagesArrived -= checkNewMessages; break; } } - private void newMessagesArrived(IEnumerable messages) + private void checkNewMessages(IEnumerable messages) { if (!messages.Any()) return; @@ -75,10 +76,7 @@ namespace osu.Game.Online.Chat var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == messages.First().ChannelId); if (channel == null) - { - Logger.Log($"Couldn't resolve channel id {messages.First().ChannelId}", LoggingTarget.Information); return; - } // Only send notifications, if ChatOverlay and the target channel aren't visible. if (chatOverlay.IsPresent && channelManager.CurrentChannel.Value == channel) @@ -93,12 +91,11 @@ namespace osu.Game.Online.Chat if (message.Sender.Id == localUser.Value.Id) continue; - // check for private messages first, - // to avoid both posting two notifications about the same message + // check for private messages first to avoid both posting two notifications about the same message if (checkForPMs(channel, message)) continue; - _ = checkForMentions(channel, message, localUser.Value.Username); + checkForMentions(channel, message); } } @@ -107,45 +104,52 @@ namespace osu.Game.Online.Chat /// /// The channel associated to the /// The message to be checked + /// Whether a notification was fired. private bool checkForPMs(Channel channel, Message message) { - if (!notifyOnPM.Value || channel.Type != ChannelType.PM) + if (!notifyOnPrivateMessage.Value || channel.Type != ChannelType.PM) return false; - if (channel.Id != message.ChannelId) - throw new ArgumentException("The provided channel doesn't match with the channel id provided by the message parameter.", nameof(channel)); - notifications.Post(new PrivateMessageNotification(message.Sender.Username, channel)); return true; } - /// - /// Checks whether the user enabled mention notifications and whether specified mentions the provided . - /// - /// The channel associated to the - /// The message to be checked - /// The username that will be checked for - private bool checkForMentions(Channel channel, Message message, string username) + private void checkForMentions(Channel channel, Message message) { - if (!notifyOnMention.Value || !isMentioning(message.Content, username)) - return false; - - if (channel.Id != message.ChannelId) - throw new ArgumentException("The provided channel doesn't match with the channel id provided by the message parameter.", nameof(channel)); + if (!notifyOnUsername.Value || !checkContainsUsername(message.Content, localUser.Value.Username)) return; notifications.Post(new MentionNotification(message.Sender.Username, channel)); - return true; } /// - /// Checks if contains , if not, retries making spaces into underscores. + /// Checks if contains . + /// This will match against the case where underscores are used instead of spaces (which is how osu-stable handles usernames with spaces). /// - /// If the mentions the - private static bool isMentioning(string message, string username) => message.Contains(username, StringComparison.OrdinalIgnoreCase) || message.Contains(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase); + private static bool checkContainsUsername(string message, string username) => message.Contains(username, StringComparison.OrdinalIgnoreCase) || message.Contains(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase); - public class OpenChannelNotification : SimpleNotification + public class PrivateMessageNotification : OpenChannelNotification { - public OpenChannelNotification(Channel channel) + public PrivateMessageNotification(string username, Channel channel) + : base(channel) + { + Icon = FontAwesome.Solid.Envelope; + Text = $"You received a private message from '{username}'. Click to read it!"; + } + } + + public class MentionNotification : OpenChannelNotification + { + public MentionNotification(string username, Channel channel) + : base(channel) + { + Icon = FontAwesome.Solid.At; + Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; + } + } + + public abstract class OpenChannelNotification : SimpleNotification + { + protected OpenChannelNotification(Channel channel) { this.channel = channel; } @@ -169,25 +173,5 @@ namespace osu.Game.Online.Chat }; } } - - public class PrivateMessageNotification : OpenChannelNotification - { - public PrivateMessageNotification(string username, Channel channel) - : base(channel) - { - Icon = FontAwesome.Solid.Envelope; - Text = $"You received a private message from '{username}'. Click to read it!"; - } - } - - public class MentionNotification : OpenChannelNotification - { - public MentionNotification(string username, Channel channel) - : base(channel) - { - Icon = FontAwesome.Solid.At; - Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; - } - } } } diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs index 9f70d23c27..b0f6400d4f 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs @@ -19,12 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online new SettingsCheckbox { LabelText = "Show a notification when someone mentions your name", - Current = config.GetBindable(OsuSetting.ChatHighlightName) + Current = config.GetBindable(OsuSetting.NotifyOnUsernameMentioned) }, new SettingsCheckbox { LabelText = "Show a notification when you receive a private message", - Current = config.GetBindable(OsuSetting.ChatMessageNotification) + Current = config.GetBindable(OsuSetting.NotifyOnPrivateMessage) }, }; } From 09df23e2a6f275893f8996f01b89682c7bbbdf9c Mon Sep 17 00:00:00 2001 From: Ivan Pavluk Date: Fri, 11 Jun 2021 15:07:41 +0700 Subject: [PATCH 262/670] improve reasoning for hd_sv_scale --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index eeebe66b77..58125d4a65 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -20,10 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Mods // In stable Taiko, hit position is 160, so playfield is essentially 160 pixels shorter // than actual screen width. Normalized screen height is 480, so on a 4:3 screen the // playfield ratio will actually be (640 - 160) / 480 = 1 - // For 16:9 resolutions, screen width with normalized height becomes 853.33333 instead, - // meaning 16:9 playfield ratio is (853.333 - 160) / 480 = 1.444444. - // Thus, 4:3 ratio / 16:9 ratio = 1 / 1.4444 = 9 / 13 - private const double hd_sv_scale = 9.0 / 13.0; + // For custom resolutions (x:y), screen width with normalized height becomes 480 * x / y instead, + // and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3 + // The following is 4:3 playfield ratio divided by 16:9 playfield ratio + private const double hd_sv_scale = (4 / 3 - 1/3) / (16 / 9 - 1/3); private BeatmapDifficulty difficulty; private ControlPointInfo controlPointInfo; From e34e26ae521de2bff25a27d9362476dccdd6e3f7 Mon Sep 17 00:00:00 2001 From: Ivan Pavluk Date: Fri, 11 Jun 2021 15:12:05 +0700 Subject: [PATCH 263/670] remove outdated comment --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 58125d4a65..e04d617e3c 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Taiko.Mods public BindableNumber VisibilityMod { get; } = new BindableDouble { MinValue = 0.5, - // Max visibility is only to be used with max playfield size MaxValue = 1.5, Default = 1.0, Value = 1.0, From a985e3b8d3562e357d3f431482794999e40948b9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 11:25:07 +0300 Subject: [PATCH 264/670] Apply documentation settings for better readability Co-authored-by: Dean Herbert Co-authored-by: Dan Balasescu --- osu.Game/Skinning/SkinProvidingContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 078c666472..0f2d8e2c22 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -24,7 +24,7 @@ namespace osu.Game.Skinning public event Action SourceChanged; /// - /// The list of skins provided by this . + /// Skins which should be exposed by this container, in order of lookup precedence. /// protected readonly BindableList SkinSources = new BindableList(); @@ -44,7 +44,7 @@ namespace osu.Game.Skinning protected virtual bool AllowColourLookup => true; /// - /// Constructs a new with a single skin added to the protected list. + /// Constructs a new initialised with a single skin source. /// public SkinProvidingContainer(ISkin skin) : this() @@ -54,7 +54,7 @@ namespace osu.Game.Skinning /// /// Constructs a new with no sources. - /// Up to the implementation for adding to the list. + /// Implementations can add or change sources through the list. /// protected SkinProvidingContainer() { From 813285275307871b77479effcf17ce9d797c845e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 11:34:22 +0300 Subject: [PATCH 265/670] Add other affectable change action cases --- osu.Game/Skinning/SkinProvidingContainer.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 0f2d8e2c22..ab33a66265 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -72,11 +72,21 @@ namespace osu.Game.Skinning break; + case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Remove: foreach (var source in args.OldItems.Cast().OfType()) source.SourceChanged -= OnSourceChanged; break; + + case NotifyCollectionChangedAction.Replace: + foreach (var source in args.OldItems.Cast().OfType()) + source.SourceChanged -= OnSourceChanged; + + foreach (var source in args.NewItems.Cast().OfType()) + source.SourceChanged += OnSourceChanged; + + break; } }), true); } From 2e01e611775c6a143d7363a51a9f03a4ef9a59d6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 11:46:29 +0300 Subject: [PATCH 266/670] Move TODO comment to correct location --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 1 + osu.Game/Skinning/SkinManager.cs | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 8087043230..c57522726d 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -56,6 +56,7 @@ namespace osu.Game.Skinning SkinSources.Add(Ruleset.CreateLegacySkinProvider(skinManager.CurrentSkin.Value, Beatmap)); + // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. if (skinManager.CurrentSkin.Value is LegacySkin) SkinSources.Add(Ruleset.CreateLegacySkinProvider(skinManager.DefaultLegacySkin, Beatmap)); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 660f44772c..7acc52809f 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -247,9 +247,6 @@ namespace osu.Game.Skinning if (lookupFunction(CurrentSkin.Value) is T skinSourced) return skinSourced; - // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. - // When attempting to address this, we may want to move the full DefaultLegacySkin fallback logic to within Player itself (to better allow - // for beatmap skin visibility). if (CurrentSkin.Value is LegacySkin && lookupFunction(DefaultLegacySkin) is T legacySourced) return legacySourced; From 8eab7df9551f150c394a3df174aa228868983082 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 17:51:58 +0900 Subject: [PATCH 267/670] Move `BindCollectionChanged` out of async load --- osu.Game/Online/Chat/MessageNotifier.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index b8947d6e47..6840c036ff 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -45,11 +45,15 @@ namespace osu.Game.Online.Chat notifyOnPrivateMessage = config.GetBindable(OsuSetting.NotifyOnPrivateMessage); localUser.BindTo(api.LocalUser); - - joinedChannels.BindCollectionChanged(channelsChanged); joinedChannels.BindTo(channelManager.JoinedChannels); } + protected override void LoadComplete() + { + base.LoadComplete(); + joinedChannels.BindCollectionChanged(channelsChanged, true); + } + private void channelsChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) From 6d06066ddee137a2f5f3c930bbc7cdda6d397497 Mon Sep 17 00:00:00 2001 From: Ivan Pavluk Date: Fri, 11 Jun 2021 15:54:30 +0700 Subject: [PATCH 268/670] forgot to run code inspection --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index e04d617e3c..b837866c4d 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Mods // For custom resolutions (x:y), screen width with normalized height becomes 480 * x / y instead, // and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3 // The following is 4:3 playfield ratio divided by 16:9 playfield ratio - private const double hd_sv_scale = (4 / 3 - 1/3) / (16 / 9 - 1/3); + private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0); private BeatmapDifficulty difficulty; private ControlPointInfo controlPointInfo; From af3f253b2182784c99072e3df362cbb07714c97d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 11 Jun 2021 18:28:48 +0900 Subject: [PATCH 269/670] Refactor `ScrollingHitObjectContainer` and expose more useful methods --- .../Scrolling/ScrollingHitObjectContainer.cs | 148 +++++++----------- 1 file changed, 57 insertions(+), 91 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index f478e37e3e..d21f30eb30 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -19,6 +19,11 @@ namespace osu.Game.Rulesets.UI.Scrolling private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); + /// + /// 0 for horizontal scroll, 1 for vertical scroll. + /// + private int scrollingAxis => direction.Value == ScrollingDirection.Left || direction.Value == ScrollingDirection.Right ? 0 : 1; + /// /// A set of top-level s which have an up-to-date layout. /// @@ -48,85 +53,65 @@ namespace osu.Game.Rulesets.UI.Scrolling } /// - /// Given a position in screen space, return the time within this column. + /// Given a position along the scrolling axis, return the time within this . /// - public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) + /// The position along the scrolling axis. + /// The time the scrolling speed is used. + public double TimeAtPosition(float position, double referenceTime) { - // convert to local space of column so we can snap and fetch correct location. - Vector2 localPosition = ToLocalSpace(screenSpacePosition); - - float position = 0; - - switch (scrollingInfo.Direction.Value) - { - case ScrollingDirection.Up: - case ScrollingDirection.Down: - position = localPosition.Y; - break; - - case ScrollingDirection.Right: - case ScrollingDirection.Left: - position = localPosition.X; - break; - } - flipPositionIfRequired(ref position); - - return scrollingInfo.Algorithm.TimeAt(position, Time.Current, scrollingInfo.TimeRange.Value, scrollLength); + return scrollingInfo.Algorithm.TimeAt(position, referenceTime, timeRange.Value, scrollLength); } /// - /// Given a time, return the screen space position within this column. + /// Given a position in screen space, return the time within this . + /// + public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) + { + Vector2 localPosition = ToLocalSpace(screenSpacePosition); + return TimeAtPosition(localPosition[scrollingAxis], Time.Current); + } + + /// + /// Given a time, return the position along the scrolling axis within this at time . + /// + public float PositionAtTime(double time, double currentTime) + { + float pos = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength); + flipPositionIfRequired(ref pos); + return pos; + } + + /// + /// Given a time, return the position along the scrolling axis within this at the current time. + /// + public float PositionAtTime(double time) => PositionAtTime(time, Time.Current); + + /// + /// Given a time, return the screen space position within this . + /// In the non-scrolling axis, the center of this is returned. /// public Vector2 ScreenSpacePositionAtTime(double time) { - var pos = scrollingInfo.Algorithm.PositionAt(time, Time.Current, scrollingInfo.TimeRange.Value, scrollLength); - - flipPositionIfRequired(ref pos); - - switch (scrollingInfo.Direction.Value) - { - case ScrollingDirection.Up: - case ScrollingDirection.Down: - return ToScreenSpace(new Vector2(getBreadth() / 2, pos)); - - default: - return ToScreenSpace(new Vector2(pos, getBreadth() / 2)); - } + float position = PositionAtTime(time, Time.Current); + return scrollingAxis == 0 + ? ToScreenSpace(new Vector2(position, DrawHeight / 2)) + : ToScreenSpace(new Vector2(DrawWidth / 2, position)); } - private float scrollLength + /// + /// Given a start time and end time of a scrolling object, return the length of the object along the scrolling axis. + /// + public float LengthAtTime(double startTime, double endTime) { - get - { - switch (scrollingInfo.Direction.Value) - { - case ScrollingDirection.Left: - case ScrollingDirection.Right: - return DrawWidth; - - default: - return DrawHeight; - } - } + return scrollingInfo.Algorithm.GetLength(startTime, endTime, timeRange.Value, scrollLength); } - private float getBreadth() - { - switch (scrollingInfo.Direction.Value) - { - case ScrollingDirection.Up: - case ScrollingDirection.Down: - return DrawWidth; - - default: - return DrawHeight; - } - } + private float scrollLength => DrawSize[scrollingAxis]; private void flipPositionIfRequired(ref float position) { - // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. + // We're dealing with coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, // so when scrolling downwards the coordinates need to be flipped. @@ -237,18 +222,11 @@ namespace osu.Game.Rulesets.UI.Scrolling { if (hitObject.HitObject is IHasDuration e) { - switch (direction.Value) - { - case ScrollingDirection.Up: - case ScrollingDirection.Down: - hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength); - break; - - case ScrollingDirection.Left: - case ScrollingDirection.Right: - hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength); - break; - } + float length = LengthAtTime(hitObject.HitObject.StartTime, e.EndTime); + if (scrollingAxis == 0) + hitObject.Width = length; + else + hitObject.Height = length; } foreach (var obj in hitObject.NestedHitObjects) @@ -262,24 +240,12 @@ namespace osu.Game.Rulesets.UI.Scrolling private void updatePosition(DrawableHitObject hitObject, double currentTime) { - switch (direction.Value) - { - case ScrollingDirection.Up: - hitObject.Y = scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength); - break; + float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime); - case ScrollingDirection.Down: - hitObject.Y = -scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength); - break; - - case ScrollingDirection.Left: - hitObject.X = scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength); - break; - - case ScrollingDirection.Right: - hitObject.X = -scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength); - break; - } + if (scrollingAxis == 0) + hitObject.X = position; + else + hitObject.Y = position; } } } From 9e16359f18a7e90049a1dacb80bad9ff2fe8d130 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 12:29:28 +0300 Subject: [PATCH 270/670] Refactor disallowing in `SkinProvidingContainer` to become per source Fixes `FindProvider` becoming completely broken, because of no way to perform the checks on one skin source. --- osu.Game/Skinning/SkinProvidingContainer.cs | 188 ++++++++++---------- 1 file changed, 96 insertions(+), 92 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index ab33a66265..c9bb3a6ec4 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using JetBrains.Annotations; @@ -28,11 +29,15 @@ namespace osu.Game.Skinning /// protected readonly BindableList SkinSources = new BindableList(); + /// + /// A dictionary mapping each from the + /// to one that performs the "allow lookup" checks before proceeding with a lookup. + /// + private readonly Dictionary disableableSkinSources = new Dictionary(); + [CanBeNull] private ISkinSource fallbackSource; - private readonly NoFallbackProxy noFallbackLookupProxy; - protected virtual bool AllowDrawableLookup(ISkinComponent component) => true; protected virtual bool AllowTextureLookup(string componentName) => true; @@ -60,31 +65,49 @@ namespace osu.Game.Skinning { RelativeSizeAxes = Axes.Both; - noFallbackLookupProxy = new NoFallbackProxy(this); - SkinSources.BindCollectionChanged(((_, args) => { switch (args.Action) { case NotifyCollectionChangedAction.Add: - foreach (var source in args.NewItems.Cast().OfType()) - source.SourceChanged += OnSourceChanged; + foreach (var skin in args.NewItems.Cast()) + { + disableableSkinSources.Add(skin, new DisableableSkinSource(skin, this)); + + if (skin is ISkinSource source) + source.SourceChanged += OnSourceChanged; + } break; case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Remove: - foreach (var source in args.OldItems.Cast().OfType()) - source.SourceChanged -= OnSourceChanged; + foreach (var skin in args.OldItems.Cast()) + { + disableableSkinSources.Remove(skin); + + if (skin is ISkinSource source) + source.SourceChanged -= OnSourceChanged; + } break; case NotifyCollectionChangedAction.Replace: - foreach (var source in args.OldItems.Cast().OfType()) - source.SourceChanged -= OnSourceChanged; + foreach (var skin in args.OldItems.Cast()) + { + disableableSkinSources.Remove(skin); - foreach (var source in args.NewItems.Cast().OfType()) - source.SourceChanged += OnSourceChanged; + if (skin is ISkinSource source) + source.SourceChanged -= OnSourceChanged; + } + + foreach (var skin in args.NewItems.Cast()) + { + disableableSkinSources.Add(skin, new DisableableSkinSource(skin, this)); + + if (skin is ISkinSource source) + source.SourceChanged += OnSourceChanged; + } break; } @@ -95,8 +118,7 @@ namespace osu.Game.Skinning { foreach (var skin in SkinSources) { - // a proxy must be used here to correctly pass through the "Allow" checks without implicitly falling back to the fallbackSource. - if (lookupFunction(noFallbackLookupProxy)) + if (lookupFunction(disableableSkinSources[skin])) return skin; } @@ -104,94 +126,50 @@ namespace osu.Game.Skinning } public Drawable GetDrawableComponent(ISkinComponent component) - => GetDrawableComponent(component, true); - - public Drawable GetDrawableComponent(ISkinComponent component, bool fallback) { - if (AllowDrawableLookup(component)) + foreach (var skin in SkinSources) { - foreach (var skin in SkinSources) - { - Drawable sourceDrawable; - if ((sourceDrawable = skin?.GetDrawableComponent(component)) != null) - return sourceDrawable; - } + Drawable sourceDrawable; + if ((sourceDrawable = disableableSkinSources[skin]?.GetDrawableComponent(component)) != null) + return sourceDrawable; } - if (!fallback) - return null; - return fallbackSource?.GetDrawableComponent(component); } public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) - => GetTexture(componentName, wrapModeS, wrapModeT, true); - - public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool fallback) { - if (AllowTextureLookup(componentName)) + foreach (var skin in SkinSources) { - foreach (var skin in SkinSources) - { - Texture sourceTexture; - if ((sourceTexture = skin?.GetTexture(componentName, wrapModeS, wrapModeT)) != null) - return sourceTexture; - } + Texture sourceTexture; + if ((sourceTexture = disableableSkinSources[skin]?.GetTexture(componentName, wrapModeS, wrapModeT)) != null) + return sourceTexture; } - if (!fallback) - return null; - return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT); } public ISample GetSample(ISampleInfo sampleInfo) - => GetSample(sampleInfo, true); - - public ISample GetSample(ISampleInfo sampleInfo, bool fallback) { - if (AllowSampleLookup(sampleInfo)) + foreach (var skin in SkinSources) { - foreach (var skin in SkinSources) - { - ISample sourceSample; - if ((sourceSample = skin?.GetSample(sampleInfo)) != null) - return sourceSample; - } + ISample sourceSample; + if ((sourceSample = disableableSkinSources[skin]?.GetSample(sampleInfo)) != null) + return sourceSample; } - if (!fallback) - return null; - return fallbackSource?.GetSample(sampleInfo); } public IBindable GetConfig(TLookup lookup) - => GetConfig(lookup, true); - - public IBindable GetConfig(TLookup lookup, bool fallback) { - if (lookup is GlobalSkinColours || lookup is SkinCustomColourLookup) - return lookupWithFallback(lookup, AllowColourLookup, fallback); - - return lookupWithFallback(lookup, AllowConfigurationLookup, fallback); - } - - private IBindable lookupWithFallback(TLookup lookup, bool canUseSkinLookup, bool canUseFallback) - { - if (canUseSkinLookup) + foreach (var skin in SkinSources) { - foreach (var skin in SkinSources) - { - IBindable bindable; - if ((bindable = skin?.GetConfig(lookup)) != null) - return bindable; - } + IBindable bindable; + if ((bindable = disableableSkinSources[skin]?.GetConfig(lookup)) != null) + return bindable; } - if (!canUseFallback) - return null; - return fallbackSource?.GetConfig(lookup); } @@ -224,35 +202,61 @@ namespace osu.Game.Skinning source.SourceChanged -= OnSourceChanged; } - private class NoFallbackProxy : ISkinSource + private class DisableableSkinSource : ISkin { + private readonly ISkin skin; private readonly SkinProvidingContainer provider; - public NoFallbackProxy(SkinProvidingContainer provider) + public DisableableSkinSource(ISkin skin, SkinProvidingContainer provider) { + this.skin = skin; this.provider = provider; } public Drawable GetDrawableComponent(ISkinComponent component) - => provider.GetDrawableComponent(component, false); - - public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) - => provider.GetTexture(componentName, wrapModeS, wrapModeT, false); - - public ISample GetSample(ISampleInfo sampleInfo) - => provider.GetSample(sampleInfo, false); - - public IBindable GetConfig(TLookup lookup) - => provider.GetConfig(lookup, false); - - public event Action SourceChanged { - add => provider.SourceChanged += value; - remove => provider.SourceChanged -= value; + if (provider.AllowDrawableLookup(component)) + return skin.GetDrawableComponent(component); + + return null; } - public ISkin FindProvider(Func lookupFunction) => - provider.FindProvider(lookupFunction); + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + { + if (provider.AllowTextureLookup(componentName)) + return skin.GetTexture(componentName, wrapModeS, wrapModeT); + + return null; + } + + public ISample GetSample(ISampleInfo sampleInfo) + { + if (provider.AllowSampleLookup(sampleInfo)) + return skin.GetSample(sampleInfo); + + return null; + } + + public IBindable GetConfig(TLookup lookup) + { + switch (lookup) + { + case GlobalSkinColours _: + case SkinCustomColourLookup _: + if (provider.AllowColourLookup) + return skin.GetConfig(lookup); + + break; + + default: + if (provider.AllowConfigurationLookup) + return skin.GetConfig(lookup); + + break; + } + + return null; + } } } } From e59beffc4e7057254c4790a3b9112c5e6c09f3ff Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 12:44:25 +0300 Subject: [PATCH 271/670] Forward all base transformer lookup methods to `Skin` --- osu.Game/Skinning/LegacySkinTransformer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index fedd63c7de..92b7a04dee 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -30,7 +30,7 @@ namespace osu.Game.Skinning Skin = skin ?? throw new ArgumentNullException(nameof(skin)); } - public abstract Drawable GetDrawableComponent(ISkinComponent component); + public virtual Drawable GetDrawableComponent(ISkinComponent component) => Skin.GetDrawableComponent(component); public Texture GetTexture(string componentName) => GetTexture(componentName, default, default); @@ -49,6 +49,6 @@ namespace osu.Game.Skinning return Skin.GetSample(sampleInfo); } - public abstract IBindable GetConfig(TLookup lookup); + public virtual IBindable GetConfig(TLookup lookup) => Skin.GetConfig(lookup); } } From fbb856d84bfdfb2e5c4c5ee3fbfde90302864000 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 12:44:44 +0300 Subject: [PATCH 272/670] Call `base` when overriding lookup methods Rather than arbitrarily accessing `Skin` here and there. --- .../Skinning/Legacy/CatchLegacySkinTransformer.cs | 8 ++++---- .../Skinning/Legacy/ManiaLegacySkinTransformer.cs | 12 ++++++------ .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 12 ++++++------ .../Skinning/Legacy/TaikoLegacySkinTransformer.cs | 12 +++--------- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index a5a1d1504f..287ed1b4c7 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy switch (targetComponent.Target) { case SkinnableTarget.MainHUDComponents: - var components = Skin.GetDrawableComponent(component) as SkinnableTargetComponentsContainer; + var components = base.GetDrawableComponent(component) as SkinnableTargetComponentsContainer; if (providesComboCounter && components != null) { @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy } } - return Skin.GetDrawableComponent(component); + return base.GetDrawableComponent(component); } public override IBindable GetConfig(TLookup lookup) @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy switch (lookup) { case CatchSkinColour colour: - var result = (Bindable)Skin.GetConfig(new SkinCustomColourLookup(colour)); + var result = (Bindable)base.GetConfig(new SkinCustomColourLookup(colour)); if (result == null) return null; @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return (IBindable)result; } - return Skin.GetConfig(lookup); + return base.GetConfig(lookup); } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 7d4d303bc9..814a737034 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -63,11 +63,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { this.beatmap = (ManiaBeatmap)beatmap; - isLegacySkin = new Lazy(() => skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); + isLegacySkin = new Lazy(() => GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); hasKeyTexture = new Lazy(() => { var keyImage = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1"; - return skin.GetAnimation(keyImage, true, true) != null; + return this.GetAnimation(keyImage, true, true) != null; }); } @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy break; } - return Skin.GetDrawableComponent(component); + return base.GetDrawableComponent(component); } private Drawable getResult(HitResult result) @@ -142,15 +142,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered) return new SampleVirtual(); - return Skin.GetSample(sampleInfo); + return base.GetSample(sampleInfo); } public override IBindable GetConfig(TLookup lookup) { if (lookup is ManiaSkinConfigurationLookup maniaLookup) - return Skin.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn)); + return base.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn)); - return Skin.GetConfig(lookup); + return base.GetConfig(lookup); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 3ad3b7d30b..41b0a88f11 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy public OsuLegacySkinTransformer(ISkin skin) : base(skin) { - hasHitCircle = new Lazy(() => Skin.GetTexture("hitcircle") != null); + hasHitCircle = new Lazy(() => GetTexture("hitcircle") != null); } public override Drawable GetDrawableComponent(ISkinComponent component) @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } } - return Skin.GetDrawableComponent(component); + return base.GetDrawableComponent(component); } public override IBindable GetConfig(TLookup lookup) @@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy switch (lookup) { case OsuSkinColour colour: - return Skin.GetConfig(new SkinCustomColourLookup(colour)); + return base.GetConfig(new SkinCustomColourLookup(colour)); case OsuSkinConfiguration osuLookup: switch (osuLookup) @@ -133,14 +133,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy case OsuSkinConfiguration.HitCircleOverlayAboveNumber: // See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D // HitCircleOverlayAboveNumer (with typo) should still be supported for now. - return Skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? - Skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); + return base.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? + base.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); } break; } - return Skin.GetConfig(lookup); + return base.GetConfig(lookup); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 0122f9a1cd..a3ecbbc436 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Audio.Sample; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Audio; using osu.Game.Rulesets.Scoring; @@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public TaikoLegacySkinTransformer(ISkin skin) : base(skin) { - hasExplosion = new Lazy(() => Skin.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null); + hasExplosion = new Lazy(() => GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null); } public override Drawable GetDrawableComponent(ISkinComponent component) @@ -50,7 +49,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.CentreHit: case TaikoSkinComponents.RimHit: - if (GetTexture("taikohitcircle") != null) return new LegacyHit(taikoComponent.Component); @@ -85,7 +83,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return null; case TaikoSkinComponents.TaikoExplosionMiss: - var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false); if (missSprite != null) return new LegacyHitExplosion(missSprite); @@ -94,7 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.TaikoExplosionOk: case TaikoSkinComponents.TaikoExplosionGreat: - var hitName = getHitName(taikoComponent.Component); var hitSprite = this.GetAnimation(hitName, true, false); @@ -126,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } } - return Skin.GetDrawableComponent(component); + return base.GetDrawableComponent(component); } private string getHitName(TaikoSkinComponents component) @@ -149,13 +145,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public override ISample GetSample(ISampleInfo sampleInfo) { if (sampleInfo is HitSampleInfo hitSampleInfo) - return Skin.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo)); + return base.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo)); return base.GetSample(sampleInfo); } - public override IBindable GetConfig(TLookup lookup) => Skin.GetConfig(lookup); - private class LegacyTaikoSampleInfo : HitSampleInfo { public LegacyTaikoSampleInfo(HitSampleInfo sampleInfo) From f20146d446d492ef9a1fa2036db0bc2dc27108b4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 12:58:38 +0300 Subject: [PATCH 273/670] Fix potentially adding null skin sources --- osu.Game/Skinning/SkinProvidingContainer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index c9bb3a6ec4..315571e79b 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -51,10 +51,11 @@ namespace osu.Game.Skinning /// /// Constructs a new initialised with a single skin source. /// - public SkinProvidingContainer(ISkin skin) + public SkinProvidingContainer([CanBeNull] ISkin skin) : this() { - SkinSources.Add(skin); + if (skin != null) + SkinSources.Add(skin); } /// From 7bb27bfd0e34a20f0beeb19a86f97fc0faa92a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Jun 2021 12:14:28 +0200 Subject: [PATCH 274/670] Add test scene for hidden mod --- .../Mods/TaikoModTestScene.cs | 12 ++++++++++ .../Mods/TestSceneTaikoModHidden.cs | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Mods/TaikoModTestScene.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TaikoModTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TaikoModTestScene.cs new file mode 100644 index 0000000000..3090facf8c --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TaikoModTestScene.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests.Mods +{ + public abstract class TaikoModTestScene : ModTestScene + { + protected sealed override Ruleset CreatePlayerRuleset() => new TaikoRuleset(); + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs new file mode 100644 index 0000000000..7abbb9d186 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs @@ -0,0 +1,24 @@ +// 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 NUnit.Framework; +using osu.Game.Rulesets.Taiko.Mods; + +namespace osu.Game.Rulesets.Taiko.Tests.Mods +{ + public class TestSceneTaikoModHidden : TaikoModTestScene + { + [Test] + public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData + { + Mod = new TaikoModHidden(), + Autoplay = true, + PassCondition = checkSomeAutoplayHits + }); + + private bool checkSomeAutoplayHits() + => Player.ScoreProcessor.JudgedHits >= 4 + && Player.Results.All(result => result.Type == result.Judgement.MaxResult); + } +} From a506f2a77699f955cd5b985f2ea4009810f76493 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Fri, 11 Jun 2021 06:22:24 -0400 Subject: [PATCH 275/670] Revert rename of lambda variables --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index ccfb0cb6e0..de9a7273c5 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -186,14 +186,14 @@ namespace osu.Game.Skinning.Editor yield return new OsuMenuItem("Anchor") { - Items = createAnchorItems((i, a) => i.UsesFixedAnchor && ((Drawable)i).Anchor == a, applyFixedAnchors) + Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors) .Prepend(closestItem) .ToArray() }; yield return new OsuMenuItem("Origin") { - Items = createAnchorItems((i, o) => ((Drawable)i).Origin == o, applyOrigins).ToArray() + Items = createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray() }; foreach (var item in base.GetContextMenuItemsForSelection(selection)) From e194f8b34a4dcd8402d717afa0435cedf35d5235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Jun 2021 12:09:40 +0200 Subject: [PATCH 276/670] Replace lifetime workaround with explicit set --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index b837866c4d..bf028fa007 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -7,7 +7,9 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Mods @@ -70,9 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Mods using (hitObject.BeginAbsoluteSequence(start)) { - // With 0 opacity the object is dying, and if I set a lifetime other issues appear... - // Ideally these need to be fixed, but I lack the knowledge to do it, and this is good enough anyway. - hitObject.FadeTo(0.0005f, duration); + hitObject.FadeOut(duration); + + // DrawableHitObject sets LifetimeEnd to LatestTransformEndTime if it isn't manually changed. + // in order for the object to not be killed before its actual end time (as the latest transform ends earlier), set lifetime end explicitly. + hitObject.LifetimeEnd = state == ArmedState.Idle + ? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + : hitObject.HitStateUpdateTime; } } From 635300b3115f721188ca3ab6593b5af0cc99c921 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Fri, 11 Jun 2021 06:28:30 -0400 Subject: [PATCH 277/670] Recalculate closest anchor when origin is changed --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index de9a7273c5..dedab1f5f5 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -240,6 +240,10 @@ namespace osu.Game.Skinning.Editor var previousOrigin = drawable.OriginPosition; drawable.Origin = origin; drawable.Position += drawable.OriginPosition - previousOrigin; + + if (item.UsesFixedAnchor) continue; + + applyClosestAnchor(drawable); } } From 15d3b4444d5dc5d1f3523bf5666d4a60366f6641 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 11 Jun 2021 19:34:54 +0900 Subject: [PATCH 278/670] Rename `HoverSounds` and `HoverClickSounds` samples --- osu.Game/Graphics/UserInterface/HoverClickSounds.cs | 2 +- osu.Game/Graphics/UserInterface/HoverSounds.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs index c1963ce62d..f6f2b270e6 100644 --- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs @@ -45,7 +45,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleClick = audio.Samples.Get($@"UI/generic-select{SampleSet.GetDescription()}"); + sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select"); } } } diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index f2e4c6d013..60c9c36be3 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio, SessionStatics statics) { - sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}"); + sampleHover = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-hover"); } public override void PlayHoverSample() @@ -43,19 +43,19 @@ namespace osu.Game.Graphics.UserInterface public enum HoverSampleSet { - [Description("")] + [Description("default")] Loud, - [Description("-soft")] + [Description("soft")] Normal, - [Description("-softer")] + [Description("softer")] Soft, - [Description("-toolbar")] + [Description("toolbar")] Toolbar, - [Description("-songselect")] + [Description("songselect")] SongSelect } } From a76eaeb52d7fccc6ff36fbd7727461392df8e9ff Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Fri, 11 Jun 2021 06:51:12 -0400 Subject: [PATCH 279/670] Make `getTieredComponent` local --- .../Skinning/Editor/SkinSelectionHandler.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index dedab1f5f5..c2db79b9ef 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -287,6 +287,17 @@ namespace osu.Game.Skinning.Editor var result = default(Anchor); + static Anchor getTieredComponent(float component, Anchor tier0, Anchor tier1, Anchor tier2) + { + if (component >= 2 / 3f) + return tier2; + + if (component >= 1 / 3f) + return tier1; + + return tier0; + } + result |= getTieredComponent(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2); result |= getTieredComponent(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2); @@ -310,17 +321,6 @@ namespace osu.Game.Skinning.Editor return result; } - private static Anchor getTieredComponent(float component, Anchor tier0, Anchor tier1, Anchor tier2) - { - if (component >= 2 / 3f) - return tier2; - - if (component >= 1 / 3f) - return tier1; - - return tier0; - } - private static void applyAnchor(Drawable drawable, Anchor anchor) { if (anchor == drawable.Anchor) return; From 6e181a6b6327658e4139cefc2d723eaf42e930c2 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Fri, 11 Jun 2021 06:53:04 -0400 Subject: [PATCH 280/670] Rename parameters of `getTieredComponent` --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index c2db79b9ef..048ba6556b 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -287,15 +287,15 @@ namespace osu.Game.Skinning.Editor var result = default(Anchor); - static Anchor getTieredComponent(float component, Anchor tier0, Anchor tier1, Anchor tier2) + static Anchor getTieredComponent(float component, Anchor anchor0, Anchor anchor1, Anchor anchor2) { if (component >= 2 / 3f) - return tier2; + return anchor2; if (component >= 1 / 3f) - return tier1; + return anchor1; - return tier0; + return anchor0; } result |= getTieredComponent(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2); From 1bc8460902a1e230ef2015717d9ac92fb8a1878f Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Fri, 11 Jun 2021 06:53:40 -0400 Subject: [PATCH 281/670] Rename `getTieredComponent` to `getAnchorFromPosition` Also rename parameter `component` to `xOrY`. --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 048ba6556b..295b1377ed 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -287,19 +287,19 @@ namespace osu.Game.Skinning.Editor var result = default(Anchor); - static Anchor getTieredComponent(float component, Anchor anchor0, Anchor anchor1, Anchor anchor2) + static Anchor getAnchorFromPosition(float xOrY, Anchor anchor0, Anchor anchor1, Anchor anchor2) { - if (component >= 2 / 3f) + if (xOrY >= 2 / 3f) return anchor2; - if (component >= 1 / 3f) + if (xOrY >= 1 / 3f) return anchor1; return anchor0; } - result |= getTieredComponent(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2); - result |= getTieredComponent(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2); + result |= getAnchorFromPosition(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2); + result |= getAnchorFromPosition(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2); return result; } From c9b4f9eb717e9d7d36a9a3d8e502216bac529296 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Fri, 11 Jun 2021 06:55:47 -0400 Subject: [PATCH 282/670] Make `getOriginPositionFromQuad` local --- .../Skinning/Editor/SkinSelectionHandler.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 295b1377ed..cd9e82997c 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -281,6 +281,23 @@ namespace osu.Game.Skinning.Editor if (parent == null) return drawable.Anchor; + static Vector2 getOriginPositionFromQuad(in Quad quad, Anchor origin) + { + var result = quad.TopLeft; + + if (origin.HasFlagFast(Anchor.x2)) + result.X += quad.Width; + else if (origin.HasFlagFast(Anchor.x1)) + result.X += quad.Width / 2f; + + if (origin.HasFlagFast(Anchor.y2)) + result.Y += quad.Height; + else if (origin.HasFlagFast(Anchor.y1)) + result.Y += quad.Height / 2f; + + return result; + } + var screenPosition = getOriginPositionFromQuad(drawable.ScreenSpaceDrawQuad, drawable.Origin); var absolutePosition = parent.ToLocalSpace(screenPosition); var factor = parent.RelativeToAbsoluteFactor; @@ -304,23 +321,6 @@ namespace osu.Game.Skinning.Editor return result; } - private static Vector2 getOriginPositionFromQuad(in Quad quad, Anchor origin) - { - var result = quad.TopLeft; - - if (origin.HasFlagFast(Anchor.x2)) - result.X += quad.Width; - else if (origin.HasFlagFast(Anchor.x1)) - result.X += quad.Width / 2f; - - if (origin.HasFlagFast(Anchor.y2)) - result.Y += quad.Height; - else if (origin.HasFlagFast(Anchor.y1)) - result.Y += quad.Height / 2f; - - return result; - } - private static void applyAnchor(Drawable drawable, Anchor anchor) { if (anchor == drawable.Anchor) return; From a6774eb5b52a9f0272da5b9de51644fee4e44710 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Fri, 11 Jun 2021 06:59:00 -0400 Subject: [PATCH 283/670] Inline `getOriginPositionFromQuad` --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index cd9e82997c..8e1f0ce7a3 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -281,24 +281,21 @@ namespace osu.Game.Skinning.Editor if (parent == null) return drawable.Anchor; - static Vector2 getOriginPositionFromQuad(in Quad quad, Anchor origin) + var screenPosition = drawable.ScreenSpaceDrawQuad.TopLeft; { - var result = quad.TopLeft; + var origin = drawable.Origin; if (origin.HasFlagFast(Anchor.x2)) - result.X += quad.Width; + screenPosition.X += drawable.ScreenSpaceDrawQuad.Width; else if (origin.HasFlagFast(Anchor.x1)) - result.X += quad.Width / 2f; + screenPosition.X += drawable.ScreenSpaceDrawQuad.Width / 2f; if (origin.HasFlagFast(Anchor.y2)) - result.Y += quad.Height; + screenPosition.Y += drawable.ScreenSpaceDrawQuad.Height; else if (origin.HasFlagFast(Anchor.y1)) - result.Y += quad.Height / 2f; - - return result; + screenPosition.Y += drawable.ScreenSpaceDrawQuad.Height / 2f; } - var screenPosition = getOriginPositionFromQuad(drawable.ScreenSpaceDrawQuad, drawable.Origin); var absolutePosition = parent.ToLocalSpace(screenPosition); var factor = parent.RelativeToAbsoluteFactor; From 0c8851f4b7a39ff14136cea8fdcb30f4b60a88a9 Mon Sep 17 00:00:00 2001 From: Robin Avery Date: Fri, 11 Jun 2021 07:06:22 -0400 Subject: [PATCH 284/670] Extract `drawable.ScreenSpaceDrawQuad` to a variable --- osu.Game/Skinning/Editor/SkinSelectionHandler.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 8e1f0ce7a3..c2ad08f0dc 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -281,19 +281,22 @@ namespace osu.Game.Skinning.Editor if (parent == null) return drawable.Anchor; - var screenPosition = drawable.ScreenSpaceDrawQuad.TopLeft; + Vector2 screenPosition; { + var quad = drawable.ScreenSpaceDrawQuad; var origin = drawable.Origin; + screenPosition = quad.TopLeft; + if (origin.HasFlagFast(Anchor.x2)) - screenPosition.X += drawable.ScreenSpaceDrawQuad.Width; + screenPosition.X += quad.Width; else if (origin.HasFlagFast(Anchor.x1)) - screenPosition.X += drawable.ScreenSpaceDrawQuad.Width / 2f; + screenPosition.X += quad.Width / 2f; if (origin.HasFlagFast(Anchor.y2)) - screenPosition.Y += drawable.ScreenSpaceDrawQuad.Height; + screenPosition.Y += quad.Height; else if (origin.HasFlagFast(Anchor.y1)) - screenPosition.Y += drawable.ScreenSpaceDrawQuad.Height / 2f; + screenPosition.Y += quad.Height / 2f; } var absolutePosition = parent.ToLocalSpace(screenPosition); From 6d2b5252c63540260104891ad31e829fd05e56c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Jun 2021 13:07:09 +0200 Subject: [PATCH 285/670] Attempt to reword setting to be more understandable --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index bf028fa007..00fcf5fa59 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Mods private BeatmapDifficulty difficulty; private ControlPointInfo controlPointInfo; - [SettingSource("Hide Time", "Multiplies the time the note stays hidden")] + [SettingSource("Fade-out Time", "The bigger this multiplier is, the sooner the notes will start fading out")] public BindableNumber VisibilityMod { get; } = new BindableDouble { MinValue = 0.5, From 4f80a3b66d0e61a9f5049ea52d7150e5eda23248 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 11 Jun 2021 20:14:35 +0900 Subject: [PATCH 286/670] Add fallback-to-default logic for HoverSounds and HoverClickSounds --- osu.Game/Graphics/UserInterface/HoverClickSounds.cs | 2 +- osu.Game/Graphics/UserInterface/HoverSounds.cs | 4 ++-- osu.Game/Graphics/UserInterface/OsuButton.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs index f6f2b270e6..61e0266372 100644 --- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs @@ -45,7 +45,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select"); + sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select") ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); } } } diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index 60c9c36be3..e17128ff83 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio, SessionStatics statics) { - sampleHover = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-hover"); + sampleHover = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-hover") ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-hover"); } public override void PlayHoverSample() @@ -44,7 +44,7 @@ namespace osu.Game.Graphics.UserInterface public enum HoverSampleSet { [Description("default")] - Loud, + Default, [Description("soft")] Normal, diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index a22c837080..1bd193e247 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -49,7 +49,7 @@ namespace osu.Game.Graphics.UserInterface protected Box Background; protected SpriteText SpriteText; - public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Loud) + public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Default) { Height = 40; From 0b95d07390c95fc6242b53592b854d91373cbd07 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 11 Jun 2021 20:42:10 +0900 Subject: [PATCH 287/670] Change 'default' hover/click samples into 'button' samples and make 'soft' the new 'default' --- osu.Game/Graphics/Containers/OsuClickableContainer.cs | 2 +- osu.Game/Graphics/UserInterface/HoverClickSounds.cs | 2 +- osu.Game/Graphics/UserInterface/HoverSounds.cs | 6 +++--- osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs | 1 + osu.Game/Graphics/UserInterface/OsuButton.cs | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs index 1f31e4cdda..60ded8952d 100644 --- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs +++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs @@ -19,7 +19,7 @@ namespace osu.Game.Graphics.Containers protected virtual HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet); - public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Normal) + public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Default) { this.sampleSet = sampleSet; } diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs index 61e0266372..3273482162 100644 --- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs @@ -28,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface /// Array of button codes which should trigger the click sound. /// If this optional parameter is omitted or set to null, the click sound will only be played on left click. /// - public HoverClickSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal, MouseButton[] buttons = null) + public HoverClickSounds(HoverSampleSet sampleSet = HoverSampleSet.Default, MouseButton[] buttons = null) : base(sampleSet) { this.buttons = buttons ?? new[] { MouseButton.Left }; diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index e17128ff83..ea81ef7d14 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -22,7 +22,7 @@ namespace osu.Game.Graphics.UserInterface protected readonly HoverSampleSet SampleSet; - public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal) + public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Default) { SampleSet = sampleSet; RelativeSizeAxes = Axes.Both; @@ -46,8 +46,8 @@ namespace osu.Game.Graphics.UserInterface [Description("default")] Default, - [Description("soft")] - Normal, + [Description("button")] + Button, [Description("softer")] Soft, diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index cfcf034d1c..70a107ca04 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -44,6 +44,7 @@ namespace osu.Game.Graphics.UserInterface private readonly Box hover; public OsuAnimatedButton() + : base(HoverSampleSet.Button) { base.Content.Add(content = new Container { diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index 1bd193e247..cd9ca9f87f 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -49,7 +49,7 @@ namespace osu.Game.Graphics.UserInterface protected Box Background; protected SpriteText SpriteText; - public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Default) + public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button) { Height = 40; From 876a357bf29eedd693804a3b6c75f3861f7aad6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Jun 2021 13:55:38 +0200 Subject: [PATCH 288/670] Add support for animated colour fill in new style legacy health bar --- osu.Game/Skinning/LegacyHealthDisplay.cs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 9d3bafd0b1..d463df5f80 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -148,9 +148,9 @@ namespace osu.Game.Skinning } } - internal class LegacyOldStyleFill : LegacyHealthPiece + internal abstract class LegacyFill : LegacyHealthPiece { - public LegacyOldStyleFill(ISkin skin) + protected LegacyFill(ISkin skin) { // required for sizing correctly.. var firstFrame = getTexture(skin, "colour-0"); @@ -166,23 +166,25 @@ namespace osu.Game.Skinning Size = new Vector2(firstFrame.DisplayWidth, firstFrame.DisplayHeight); } - Position = new Vector2(3, 10) * 1.6f; Masking = true; } } - internal class LegacyNewStyleFill : LegacyHealthPiece + internal class LegacyOldStyleFill : LegacyFill + { + public LegacyOldStyleFill(ISkin skin) + : base(skin) + { + Position = new Vector2(3, 10) * 1.6f; + } + } + + internal class LegacyNewStyleFill : LegacyFill { public LegacyNewStyleFill(ISkin skin) + : base(skin) { - InternalChild = new Sprite - { - Texture = getTexture(skin, "colour"), - }; - - Size = InternalChild.Size; Position = new Vector2(7.5f, 7.8f) * 1.6f; - Masking = true; } protected override void Update() From 550d566bf979521e86a16d62db1eb630ee60aca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Jun 2021 14:03:21 +0200 Subject: [PATCH 289/670] Simplify member access --- osu.Game/Skinning/LegacyHealthDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index d463df5f80..1da80f6613 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -162,7 +162,7 @@ namespace osu.Game.Skinning } else { - InternalChild = skin.GetAnimation("scorebar-colour", true, true, startAtCurrentTime: false, applyConfigFrameRate: true) ?? Drawable.Empty(); + InternalChild = skin.GetAnimation("scorebar-colour", true, true, startAtCurrentTime: false, applyConfigFrameRate: true) ?? Empty(); Size = new Vector2(firstFrame.DisplayWidth, firstFrame.DisplayHeight); } From d3a255fd8113eafeaa6de763ac8bcb99a85d6e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Jun 2021 14:21:54 +0200 Subject: [PATCH 290/670] Add animated assets for legacy health display test --- .../Resources/special-skin/scorebar-bg.png | Bin 0 -> 250 bytes .../Resources/special-skin/scorebar-colour-0.png | Bin 0 -> 1285 bytes .../Resources/special-skin/scorebar-colour-1.png | Bin 0 -> 1288 bytes .../Resources/special-skin/scorebar-colour-2.png | Bin 0 -> 1287 bytes .../Resources/special-skin/scorebar-colour-3.png | Bin 0 -> 1286 bytes .../Resources/special-skin/scorebar-marker.png | Bin 0 -> 126 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Tests/Resources/special-skin/scorebar-bg.png create mode 100644 osu.Game.Tests/Resources/special-skin/scorebar-colour-0.png create mode 100644 osu.Game.Tests/Resources/special-skin/scorebar-colour-1.png create mode 100644 osu.Game.Tests/Resources/special-skin/scorebar-colour-2.png create mode 100644 osu.Game.Tests/Resources/special-skin/scorebar-colour-3.png create mode 100644 osu.Game.Tests/Resources/special-skin/scorebar-marker.png diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-bg.png b/osu.Game.Tests/Resources/special-skin/scorebar-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..1a25274ed8bb9a3a5ea2cfd01445141b5f774292 GIT binary patch literal 250 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Wd)j8OJ21sKV{?Q5ip7re$fgPYWH+;45_&F_S!~X1_c4;jR%s> zoJ$F)cW<6B!PaS33=>cZe(+&qTGQ{eO=}nqWIz7!efLTRhC&904rT@i83u+&3=9J7 m3=I|x3`ZCl6!>r(w2Ig9Ig`kdJJ$<9YCK*2T-G@yGywqnV>P1y literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-colour-0.png b/osu.Game.Tests/Resources/special-skin/scorebar-colour-0.png new file mode 100644 index 0000000000000000000000000000000000000000..3c15449b039fe44b372fdb9b538be1601f6655d6 GIT binary patch literal 1285 zcmV+g1^W7lP)000vR1^@s6X5jw>00006VoOIv00000 z008+zyMF)x010qNS#tmYE+YT{E+YYWr9XB6000McNliruTa zK~#9!?b*MN9n}@U@$WfzX5M@I|jp zl!++lkdTmLBrI@ZyvW9Dd;NaS%)RGO%mX7jL=i1IpX$zNW_0Jw`B4mn5JCtcgb+dq zA%qY@2qAoo@5#C)fG-!+(J&*^JZ0lD1vpW)u-Xa1m4-Trm}PwkB*% zD(a>tC8bJ^Z zQ3+Hk{Nd6rvlg!3YB@X@adc&myEEtdac12sDv-@-t)~kgCMixZ?2q zgeEDe8cJu`6&BeLb?!fGSPsYbMYOR=6hB}&PdD#jdo-u-GbpGibzPHGNlEC7(RT$9%%R|j>*gDP;HU^qqcpXG2<}eF z&N>^V4EI(FRnj5vajY1nfMJ{38|r@ZR+z!49{QkT!+T_>;J8~6_q+f2=z|gi{WtK3 z|MrMN2qA?3r?>z5-o<}@^uc#O9&3aogb+eFd(22bKaPPOLI@$8K_wSrpob7b2xr_c z|NX{54+|EL?MI_ vLI@#*5JCtcgb+dqA%qY@2qA+nqUV>8`c00000NkvXXu0mjfBN9_U literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-colour-1.png b/osu.Game.Tests/Resources/special-skin/scorebar-colour-1.png new file mode 100644 index 0000000000000000000000000000000000000000..a444723ef4023391257479a66c443a3e593dd019 GIT binary patch literal 1288 zcmV+j1^4=iP)000vR1^@s6X5jw>00006VoOIv00000 z008+zyMF)x010qNS#tmYE+YT{E+YYWr9XB6000McNliruXb2&M5JCtc zgb+dqA%qY@2qAUE0-!>c&f&-AxY3p+=>}ZQktZggQ~EzT{CWKN-4O*sICCvWZvQisj5&_9?VAIf9s;4hTqY1&uW`NUK=7-gqB#A4R_qlLx!oj%_hI4Y-Qmc@v#MZdt z@ce`(DXAJtXW11N*${Oe{Lr$Px6GC;i*-hUzBk^z_6@gBmQ)Q%-P33(V8{g%G&%FxoTjN5jkY;DdV!lauhaF03MfLC zdn{*C5{hL`A1$zAOs5mF^@s~e1WiirdobJNVvkTrRYOyaX#198J#|Wyk}*}1SGYMM zLNVv+)jxCe$IJZlU+*Dm2ud+SR7faVCq|<&nuOWxlv14ee8K*?9mE||#oc)2#UqZ7 zzvu4#6GqL1R42OilDzreBFe#ZhbzxKjR0nbs&nz+fKe?xm@W9~yE_=pg}vRMb4^un zE0mJ??%qSdxpMhQ9An*ee17XLx9&dR`R6Y4#n<0rRyIvcQJ|QyTDOcx4XQfiu|rcO zUVZUd{_@guT)Hsj>BIBvPbX9gZPzoKuju+hw!)-om~PcnqX|_dtR9{+-5GQD`x&ar ze6{A^U)<;Z!zCxDOV)jlnGN+(h92RY#J6IM#}gLoCF}W;zrOl1D)8xN|6$!_%pC*z zuF!TpW<%dC#XyC3Nr9r7N>icty}wyC|L?l*6R8I0~ryM|KL1yA^T2_u>2Rlo;s0 zf`9wAM-)N`A^ev9_U4xtKYIV2J3k$3gd~IzLO6TONdJ2r13iQgLO6p;F2q0&A%qal zxc~q68v{Lr5JLDJh=CqL2qBz75(7Pi5JEWn`fM@KLkJ;+vu@Sq80aB{5W-n!g+~#E y5JCtcgb+dqA%qY@2qA21sKV{?Q;n6f@tTAC=V433S;uunK>+Mb3e%Ys@2R{C{ zJb&g+_4lXiu4|u3N9aoU*_lVy_qJANF|6aOalO})W#sl-%=rHAD8-x_5k(!u zoVz-s4HtWQO%^E*5G~u5eazQaleJvps;I)8GtqYxLPb}dYOXxJ%Xp1Q+4k%^MzPm7 zM|}NqG{Z#E;wDdl)#;k;QIk~)-=4A)Kj&*r`_C?y78RGh407O=ihsD?&PFZLORWn2Xj4m1aB)I-j1Gpy!B1L_AQwYiS_S{Y&@rIJ00Siv}^bAhr#|uuj`LHGYbT) zEb!l9y)TnHt#7yTDuv7)0YZyRm3G}M{hBOXm{YvIL1Gnel&MG17OBNa5B_d&Jg>m& zSdiG!o?gIQUvo}Xk@XI*&jGHmsxAp8XT2E04%*6L-#+I`eFMh81_p@}@y#Pkn z7e)O~eY~7Do_V>LHLT}liPJW@LdDJno+oSh(`LuK5#(B#m9a`N_kPft<$j$(M$5cD zahbcjXgu5V>lL@m--lf9_x=}L)V4sx*rl~)QE8Hi)MC#WXZ^)fz59S=49$(-5 zFy_YXzlPUCShqWA&RqZAmSs+M_`D;A*J6*0Z+~C(`yKPV>Tg`;`+wP|rCvTF!rCRd zy7Y9#l?AJu<*ZFYA8i$1|7rR3c~393YEI`rzuA6nfaaFi^Evw#z2?5jHF;U$uF&9K zwUDVBswz&Eq@TH)pT0ut^VM1P_xGIt+wSe>H+{`-Ib-Qpi+OAhnRn(NmFQR3x#oTC zvub_a+tiQ;o9q8hFP&~!&huio)9tS_cx$(dFpGZsw(N}D*_Xe0xU{@tud9Rx3H?g? z63fU^tYPtJ*H1PD3oWP18op~4nsx_o+I-GU^YGrOPE$)xRLf|)X(rrQ_N{H#{XL>- z2CLfbUflQ~cT>~iUD%Po_wL*MTX4kTzvv(P_0=xG40|a5rT+W4eLueU+y8jhp4>YD zl!w#%C0{N-Zg{~Nn5B(ZJaOXr0u+4M^ZV!i`w}len%+A=gm@-F_&2YocQ?ubOMp40 stIdu(@B`JHv(mQd(m*Q_VB8zopr0M~s-fB*mh literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-colour-3.png b/osu.Game.Tests/Resources/special-skin/scorebar-colour-3.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a5ca4716d3898929332a54b631ee604c017390 GIT binary patch literal 1286 zcmeAS@N?(olHy`uVBq!ia0y~yVEh7P3v;jm$+QRmS%4H21sKV{?Q;W1RpeUik$z%tp>#WAGf*4vx5{jyI*4}AP@ zdH&3u>hDk2UDrO7)SS}85V@wMXMuyFtjQDZB@36FJ^mngNvDP;vuCHvLq1MU$!MlV z;m59KakIbH&Yk((QvGO5!4$qgm+o`xtI|x2tDc>y)MRtwP;3!EB^VS0o8sTUwf~!X zYkJ+Ph7P4gE3_U&==m$X?)$x=j9t(%#L~jX-*%PdzUroq2Nto>JAVAx{O9N2@)>^H zcmBKSYxna-uz>39!d(%UwrSt#QDWlIYwBv&UUul*wU)J>u6M62SrVc-(M7^vDrC;C zpl{{dBXy$l?(8^SbT{y7^0BisPG75v6rD%qgU&6{kKNh51-AQ zcDPy4J^b4a`{F#g^lN(;G2NVVt;$Q#L9ozE?@3UB`L%Azd)qstcg7Ze4LYiQ{#t~Y z3g@oQXv4*xUXw+N14PTVWgqkP)nqM~xGJhJ=S=h+g;3E|rw&)1-etT-q-=Zk9i!Om znm!tvR6E2(VkwwTwiofRgv`$ug?Llu&OQzCTG1F3r@~F|IEbqXvUVa7cXkx*;U%S&OF3H zaMzt=%gK`hbc%mfG>Rr$&7H8;J;p;RBdF+p^G!AJ>K3O}QnRvLZf_TPy)t;wl%AE7 z_8dAZDA;A0zwf7(yMFAm_4U847qu-AF?MNfSyY;2BDL6a##ukjqchKoznOpA@s45W zonG^GvV}E&l)uZ(G00L|Vs&xt^e1P(iv{v6vyQ3#XUSq<95T0?oxLaP;lrGR+wcEx zco=iz_FvWOA*|b-G-tklZ_hMm_4N7246nr=zh3^X@cBLGxSEGX_iMiApE%TUfgV$Jk5 z&%OWsxb}L*f#>lxM|Yi`Va@g>cH!HqGj02#yBfKwtAo?}pMR-kV-;PxE_;%eCTFeT zuXPMfmLf8RcWamhWJDKcPFWTvaPY3?&B*k{Qy$*)TIgl<#I{d#v4}xVaJBQ@{Cix+ z5?7t&OL8ju--yVRYd@;{FE3vok+k4HTb*pb1~9+=sIU6o{9W(g=g0i@KYm6is{k|c zp_|EWzurHVcmZNdh7>8XRRQyM!Lh&Z_uq#IzIWjH0?g|Vc_u;lH?OC6H_8Ds|D4j* tX2%`)ff~@23MQ{KB literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-marker.png b/osu.Game.Tests/Resources/special-skin/scorebar-marker.png new file mode 100644 index 0000000000000000000000000000000000000000..b5af0b2148832b745caf04c4e2b5103efa43c913 GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blwj^(N7a$D;Kb?2i11Zh|kH}&M z20djEW~^9hUj`IpFY)wsWq-=X%_F6M;8)Hypb)pGi(?4K_2dKwCI$vp2F6Efth0f_ N44$rjF6*2UngDK&89D#} literal 0 HcmV?d00001 From dca001a72d6e079e22cf3c8f0e60b3ebd2909a48 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Jun 2021 22:22:47 +0900 Subject: [PATCH 291/670] Upgraed localisation analyser packages --- .config/dotnet-tools.json | 4 ++-- osu.Game/osu.Game.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b51ecb4f7e..84673fe2ba 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -33,10 +33,10 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2021.524.0", + "version": "2021.608.0", "commands": [ "localisation" ] } } -} \ No newline at end of file +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 2ee8ed527f..ed299456de 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 108a3deb27382b833f297abf79e63a53e8da1b4d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 16:26:53 +0300 Subject: [PATCH 292/670] Also handle null `Ruleset.CreateLegacySkinProvider` values Let's just go this way for now, maybe it's a better choice to always create transformers and disallow null, but it's too much work and out of scope at this point --- .../Skinning/RulesetSkinProvidingContainer.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index c57522726d..b6a3bd7cda 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Skinning Ruleset = ruleset; Beatmap = beatmap; - InternalChild = new BeatmapSkinProvidingContainer(beatmapSkin == null ? null : ruleset.CreateLegacySkinProvider(beatmapSkin, beatmap)) + InternalChild = new BeatmapSkinProvidingContainer(GetRulesetTransformedSkin(beatmapSkin)) { Child = Content = new Container { @@ -54,13 +54,25 @@ namespace osu.Game.Skinning { SkinSources.Clear(); - SkinSources.Add(Ruleset.CreateLegacySkinProvider(skinManager.CurrentSkin.Value, Beatmap)); + SkinSources.Add(GetRulesetTransformedSkin(skinManager.CurrentSkin.Value)); // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. if (skinManager.CurrentSkin.Value is LegacySkin) - SkinSources.Add(Ruleset.CreateLegacySkinProvider(skinManager.DefaultLegacySkin, Beatmap)); + SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultLegacySkin)); - SkinSources.Add(Ruleset.CreateLegacySkinProvider(skinManager.DefaultSkin, Beatmap)); + SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultSkin)); + } + + protected ISkin GetRulesetTransformedSkin(ISkin skin) + { + if (skin == null) + return null; + + var rulesetTransformed = Ruleset.CreateLegacySkinProvider(skin, Beatmap); + if (rulesetTransformed != null) + return rulesetTransformed; + + return skin; } } } From 97bb3de1c9024ef77798739981ee0615a5c4c7cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 23:03:27 +0900 Subject: [PATCH 293/670] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 0d3fafd19f..ba8b8b32ba 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 2ee8ed527f..6d3a1d5226 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 7832aaaf2d..e382a804c8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From d6d87e1975b3cd6145328e2849f358606acc17c9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 17:35:32 +0300 Subject: [PATCH 294/670] Move collection change bind to LoadComplete Best practice anyways --- osu.Game/Skinning/SkinProvidingContainer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 315571e79b..ac1b4d0395 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -65,6 +65,11 @@ namespace osu.Game.Skinning protected SkinProvidingContainer() { RelativeSizeAxes = Axes.Both; + } + + protected override void LoadComplete() + { + base.LoadComplete(); SkinSources.BindCollectionChanged(((_, args) => { From 5887b4a27cc585bb20cb3e9bb3954f19e40de3be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 23:46:42 +0900 Subject: [PATCH 295/670] Update stand-alone usage of hover/select sounds in `DrawableOsuMenuItem` --- .../UserInterface/DrawableOsuMenuItem.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 8df2c1c2fd..fea84998cf 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,9 +22,6 @@ namespace osu.Game.Graphics.UserInterface private const int text_size = 17; private const int transition_length = 80; - private Sample sampleClick; - private Sample sampleHover; - private TextContainer text; public DrawableOsuMenuItem(MenuItem item) @@ -36,12 +32,11 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleHover = audio.Samples.Get(@"UI/generic-hover"); - sampleClick = audio.Samples.Get(@"UI/generic-select"); - BackgroundColour = Color4.Transparent; BackgroundColourHover = Color4Extensions.FromHex(@"172023"); + AddInternal(new HoverClickSounds()); + updateTextColour(); Item.Action.BindDisabledChanged(_ => updateState(), true); @@ -84,7 +79,6 @@ namespace osu.Game.Graphics.UserInterface if (IsHovered && !Item.Action.Disabled) { - sampleHover.Play(); text.BoldText.FadeIn(transition_length, Easing.OutQuint); text.NormalText.FadeOut(transition_length, Easing.OutQuint); } @@ -95,12 +89,6 @@ namespace osu.Game.Graphics.UserInterface } } - protected override bool OnClick(ClickEvent e) - { - sampleClick.Play(); - return base.OnClick(e); - } - protected sealed override Drawable CreateContent() => text = CreateTextContainer(); protected virtual TextContainer CreateTextContainer() => new TextContainer(); From e098cac1cffb9388f5c2124eb5e0b6bac953ba20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Jun 2021 23:49:14 +0900 Subject: [PATCH 296/670] Minor code reformatting / moving --- .../UserInterface/HoverClickSounds.cs | 3 ++- .../Graphics/UserInterface/HoverSampleSet.cs | 25 +++++++++++++++++++ .../Graphics/UserInterface/HoverSounds.cs | 22 ++-------------- 3 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Graphics/UserInterface/HoverSampleSet.cs diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs index 3273482162..12819840e5 100644 --- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs @@ -45,7 +45,8 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select") ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); + sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select") + ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); } } } diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs new file mode 100644 index 0000000000..c74ac90a4c --- /dev/null +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -0,0 +1,25 @@ +// 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; + +namespace osu.Game.Graphics.UserInterface +{ + public enum HoverSampleSet + { + [Description("default")] + Default, + + [Description("button")] + Button, + + [Description("softer")] + Soft, + + [Description("toolbar")] + Toolbar, + + [Description("songselect")] + SongSelect + } +} diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index ea81ef7d14..c0ef5cb3fc 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.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.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -31,7 +30,8 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio, SessionStatics statics) { - sampleHover = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-hover") ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-hover"); + sampleHover = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-hover") + ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-hover"); } public override void PlayHoverSample() @@ -40,22 +40,4 @@ namespace osu.Game.Graphics.UserInterface sampleHover.Play(); } } - - public enum HoverSampleSet - { - [Description("default")] - Default, - - [Description("button")] - Button, - - [Description("softer")] - Soft, - - [Description("toolbar")] - Toolbar, - - [Description("songselect")] - SongSelect - } } From b6947c25ec5d5c3c40e50cf4ad7e3450009a8a68 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 17:55:07 +0300 Subject: [PATCH 297/670] Fix potentially adding the same skin multiple times --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index b6a3bd7cda..8a27899e89 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -57,10 +57,11 @@ namespace osu.Game.Skinning SkinSources.Add(GetRulesetTransformedSkin(skinManager.CurrentSkin.Value)); // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. - if (skinManager.CurrentSkin.Value is LegacySkin) + if (skinManager.CurrentSkin.Value is LegacySkin && skinManager.CurrentSkin.Value != skinManager.DefaultLegacySkin) SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultLegacySkin)); - SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultSkin)); + if (skinManager.CurrentSkin.Value != skinManager.DefaultSkin) + SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultSkin)); } protected ISkin GetRulesetTransformedSkin(ISkin skin) From 8de0d33c5a36b8b9e9af4c035eb9a21caa25d3cf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Jun 2021 17:59:28 +0300 Subject: [PATCH 298/670] Revert "Move collection change bind to LoadComplete" This reverts commit d6d87e1975b3cd6145328e2849f358606acc17c9. Actually that broke things due to the "disableable" instances not added early enough, revert for now. --- osu.Game/Skinning/SkinProvidingContainer.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index ac1b4d0395..315571e79b 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -65,11 +65,6 @@ namespace osu.Game.Skinning protected SkinProvidingContainer() { RelativeSizeAxes = Axes.Both; - } - - protected override void LoadComplete() - { - base.LoadComplete(); SkinSources.BindCollectionChanged(((_, args) => { From d9ea8d64d4855feda54c3512b054344c86352d2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Jun 2021 00:05:49 +0900 Subject: [PATCH 299/670] Remove weird local sample logic in `ChangelogOverlay` --- osu.Game/Overlays/ChangelogOverlay.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index e7d68853ad..67d83980ef 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -25,8 +25,6 @@ namespace osu.Game.Overlays public readonly Bindable Current = new Bindable(); - private Sample sampleBack; - private List builds; protected List Streams; @@ -41,8 +39,6 @@ namespace osu.Game.Overlays { Header.Build.BindTarget = Current; - sampleBack = audio.Samples.Get(@"UI/generic-select-soft"); - Current.BindValueChanged(e => { if (e.NewValue != null) @@ -108,7 +104,6 @@ namespace osu.Game.Overlays else { Current.Value = null; - sampleBack?.Play(); } return true; From 0dbe5dd2190f030e76a875bca0845cd67856d29d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Jun 2021 00:05:49 +0900 Subject: [PATCH 300/670] Remove unused using statement --- osu.Game/Overlays/ChangelogOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 67d83980ef..a8f2e654d7 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Input.Bindings; From 121df57dca7e5f66458ac952920b927861113f0e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Jun 2021 00:26:33 +0900 Subject: [PATCH 301/670] Fix focused overlays playing their "appear" sound when not necessarily changing state --- .../Containers/OsuFocusedOverlayContainer.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index c0518247a9..b9b098df80 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -107,10 +107,10 @@ namespace osu.Game.Graphics.Containers { } - private bool playedPopInSound; - protected override void UpdateState(ValueChangedEvent state) { + bool didChange = state.NewValue != state.OldValue; + switch (state.NewValue) { case Visibility.Visible: @@ -121,18 +121,15 @@ namespace osu.Game.Graphics.Containers return; } - samplePopIn?.Play(); - playedPopInSound = true; + if (didChange) + samplePopIn?.Play(); if (BlockScreenWideMouse && DimMainContent) game?.AddBlockingOverlay(this); break; case Visibility.Hidden: - if (playedPopInSound) - { + if (didChange) samplePopOut?.Play(); - playedPopInSound = false; - } if (BlockScreenWideMouse) game?.RemoveBlockingOverlay(this); break; From f773ea475d08fa0758e436db9ead42e97ad227d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Jun 2021 01:37:13 +0900 Subject: [PATCH 302/670] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index ba8b8b32ba..13d45835be 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 877ae94a5e..7eb3c84582 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -34,7 +34,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index e382a804c8..3e8facaf6e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From fe39a47797d9d754c59c86414d90bd237a02e9cb Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sat, 12 Jun 2021 00:34:53 +0200 Subject: [PATCH 303/670] Add `OsuModSettingsTextBox` and `OsuModSettingsNumberBox` --- .../UserInterface/OsuModSettingsNumberBox.cs | 10 +++++ .../UserInterface/OsuModSettingsTextBox.cs | 44 +++++++++++++++++++ osu.Game/Overlays/Settings/SettingsTextBox.cs | 2 +- osu.Game/Rulesets/Mods/ModRandom.cs | 4 +- 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Graphics/UserInterface/OsuModSettingsNumberBox.cs create mode 100644 osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs diff --git a/osu.Game/Graphics/UserInterface/OsuModSettingsNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuModSettingsNumberBox.cs new file mode 100644 index 0000000000..4ec4165c0e --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuModSettingsNumberBox.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Graphics.UserInterface +{ + public class OsuModSettingsNumberBox : OsuModSettingsTextBox + { + protected override bool CanAddCharacter(char character) => char.IsNumber(character); + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs b/osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs new file mode 100644 index 0000000000..6720727b7a --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs @@ -0,0 +1,44 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics.Colour; +using osu.Framework.Input.Events; + +namespace osu.Game.Graphics.UserInterface +{ + public class OsuModSettingsTextBox : OsuTextBox + { + private const float border_thickness = 3; + + private SRGBColour borderColourFocused; + private SRGBColour borderColourUnfocused; + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + borderColourUnfocused = colour.Gray4.Opacity(0.5f); + borderColourFocused = BorderColour; + + BorderThickness = border_thickness; + BorderColour = borderColourUnfocused; + } + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + BorderThickness = border_thickness; + BorderColour = borderColourFocused; + } + + protected override void OnFocusLost(FocusLostEvent e) + { + base.OnFocusLost(e); + + BorderThickness = border_thickness; + BorderColour = borderColourUnfocused; + } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index 5e700a1d6b..43bc8e87f8 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs @@ -8,7 +8,7 @@ namespace osu.Game.Overlays.Settings { public class SettingsTextBox : SettingsItem { - protected override Drawable CreateControl() => new OsuTextBox + protected override Drawable CreateControl() => new OsuModSettingsTextBox { Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 3f14263420..450b2a0680 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mods } } - private readonly OsuNumberBox seedNumberBox; + private readonly OsuModSettingsNumberBox seedNumberBox; public SeedControl() { @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Mods { new Drawable[] { - seedNumberBox = new OsuNumberBox + seedNumberBox = new OsuModSettingsNumberBox { RelativeSizeAxes = Axes.X, CommitOnFocusLost = true From bb661abfa65d44f1fb15f6d351f3fbcbb8e3984a Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sat, 12 Jun 2021 17:25:22 +0200 Subject: [PATCH 304/670] Clean up `OsuModSettingsTextBox` --- .../UserInterface/OsuModSettingsTextBox.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs b/osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs index 6720727b7a..11b7ed33d0 100644 --- a/osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs @@ -3,8 +3,8 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics.Colour; using osu.Framework.Input.Events; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { @@ -12,8 +12,8 @@ namespace osu.Game.Graphics.UserInterface { private const float border_thickness = 3; - private SRGBColour borderColourFocused; - private SRGBColour borderColourUnfocused; + private Color4 borderColourFocused; + private Color4 borderColourUnfocused; [BackgroundDependencyLoader] private void load(OsuColour colour) @@ -21,24 +21,27 @@ namespace osu.Game.Graphics.UserInterface borderColourUnfocused = colour.Gray4.Opacity(0.5f); borderColourFocused = BorderColour; - BorderThickness = border_thickness; - BorderColour = borderColourUnfocused; + updateBorder(); } protected override void OnFocus(FocusEvent e) { base.OnFocus(e); - BorderThickness = border_thickness; - BorderColour = borderColourFocused; + updateBorder(); } protected override void OnFocusLost(FocusLostEvent e) { base.OnFocusLost(e); + updateBorder(); + } + + private void updateBorder() + { BorderThickness = border_thickness; - BorderColour = borderColourUnfocused; + BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused; } } } From 29f38804156b58aee0b6f31c71a28f7dec856b17 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sat, 12 Jun 2021 17:34:02 +0200 Subject: [PATCH 305/670] Move classes into `SettingsTextBox` --- .../UserInterface/OsuModSettingsNumberBox.cs | 10 ---- .../UserInterface/OsuModSettingsTextBox.cs | 47 ------------------- osu.Game/Overlays/Settings/SettingsTextBox.cs | 47 +++++++++++++++++++ osu.Game/Rulesets/Mods/ModRandom.cs | 5 +- 4 files changed, 49 insertions(+), 60 deletions(-) delete mode 100644 osu.Game/Graphics/UserInterface/OsuModSettingsNumberBox.cs delete mode 100644 osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs diff --git a/osu.Game/Graphics/UserInterface/OsuModSettingsNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuModSettingsNumberBox.cs deleted file mode 100644 index 4ec4165c0e..0000000000 --- a/osu.Game/Graphics/UserInterface/OsuModSettingsNumberBox.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Graphics.UserInterface -{ - public class OsuModSettingsNumberBox : OsuModSettingsTextBox - { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); - } -} diff --git a/osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs b/osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs deleted file mode 100644 index 11b7ed33d0..0000000000 --- a/osu.Game/Graphics/UserInterface/OsuModSettingsTextBox.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Input.Events; -using osuTK.Graphics; - -namespace osu.Game.Graphics.UserInterface -{ - public class OsuModSettingsTextBox : OsuTextBox - { - private const float border_thickness = 3; - - private Color4 borderColourFocused; - private Color4 borderColourUnfocused; - - [BackgroundDependencyLoader] - private void load(OsuColour colour) - { - borderColourUnfocused = colour.Gray4.Opacity(0.5f); - borderColourFocused = BorderColour; - - updateBorder(); - } - - protected override void OnFocus(FocusEvent e) - { - base.OnFocus(e); - - updateBorder(); - } - - protected override void OnFocusLost(FocusLostEvent e) - { - base.OnFocusLost(e); - - updateBorder(); - } - - private void updateBorder() - { - BorderThickness = border_thickness; - BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused; - } - } -} diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index 43bc8e87f8..4e96573538 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs @@ -1,8 +1,13 @@ // 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.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osuTK.Graphics; namespace osu.Game.Overlays.Settings { @@ -14,5 +19,47 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.X, CommitOnFocusLost = true, }; + + public class OsuModSettingsTextBox : OsuTextBox + { + private const float border_thickness = 3; + + private Color4 borderColourFocused; + private Color4 borderColourUnfocused; + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + borderColourUnfocused = colour.Gray4.Opacity(0.5f); + borderColourFocused = BorderColour; + + updateBorder(); + } + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + updateBorder(); + } + + protected override void OnFocusLost(FocusLostEvent e) + { + base.OnFocusLost(e); + + updateBorder(); + } + + private void updateBorder() + { + BorderThickness = border_thickness; + BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused; + } + } + + public class OsuModSettingsNumberBox : OsuModSettingsTextBox + { + protected override bool CanAddCharacter(char character) => char.IsNumber(character); + } } } diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 450b2a0680..7220580b9f 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods @@ -50,7 +49,7 @@ namespace osu.Game.Rulesets.Mods } } - private readonly OsuModSettingsNumberBox seedNumberBox; + private readonly SettingsTextBox.OsuModSettingsNumberBox seedNumberBox; public SeedControl() { @@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Mods { new Drawable[] { - seedNumberBox = new OsuModSettingsNumberBox + seedNumberBox = new SettingsTextBox.OsuModSettingsNumberBox { RelativeSizeAxes = Axes.X, CommitOnFocusLost = true From c728f673d6dffc3e24d3d1b0163034922b45544e Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sat, 12 Jun 2021 17:37:01 +0200 Subject: [PATCH 306/670] Rename classes --- osu.Game/Overlays/Settings/SettingsTextBox.cs | 6 +++--- osu.Game/Rulesets/Mods/ModRandom.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index 4e96573538..f895a66128 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs @@ -13,14 +13,14 @@ namespace osu.Game.Overlays.Settings { public class SettingsTextBox : SettingsItem { - protected override Drawable CreateControl() => new OsuModSettingsTextBox + protected override Drawable CreateControl() => new OsuSettingsTextBox { Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, CommitOnFocusLost = true, }; - public class OsuModSettingsTextBox : OsuTextBox + public class OsuSettingsTextBox : OsuTextBox { private const float border_thickness = 3; @@ -57,7 +57,7 @@ namespace osu.Game.Overlays.Settings } } - public class OsuModSettingsNumberBox : OsuModSettingsTextBox + public class OsuSettingsNumberBox : OsuSettingsTextBox { protected override bool CanAddCharacter(char character) => char.IsNumber(character); } diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 7220580b9f..cef1814ee6 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mods } } - private readonly SettingsTextBox.OsuModSettingsNumberBox seedNumberBox; + private readonly SettingsTextBox.OsuSettingsNumberBox seedNumberBox; public SeedControl() { @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Mods { new Drawable[] { - seedNumberBox = new SettingsTextBox.OsuModSettingsNumberBox + seedNumberBox = new SettingsTextBox.OsuSettingsNumberBox { RelativeSizeAxes = Axes.X, CommitOnFocusLost = true From b79d57b68c4586ca174b702b6c50396a0262bb72 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sat, 12 Jun 2021 17:57:40 +0200 Subject: [PATCH 307/670] Move `OsuSettingsNumberBox` into `SettingsNumberBox` --- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 8 ++++++-- osu.Game/Overlays/Settings/SettingsTextBox.cs | 5 ----- osu.Game/Rulesets/Mods/ModRandom.cs | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index cb7e63ae6f..20de35ed87 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -2,16 +2,20 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { public class SettingsNumberBox : SettingsItem { - protected override Drawable CreateControl() => new OsuNumberBox + protected override Drawable CreateControl() => new OsuSettingsNumberBox { Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, }; + + public class OsuSettingsNumberBox : SettingsTextBox.OsuSettingsTextBox + { + protected override bool CanAddCharacter(char character) => char.IsNumber(character); + } } } diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index f895a66128..efcfb0ec5b 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs @@ -56,10 +56,5 @@ namespace osu.Game.Overlays.Settings BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused; } } - - public class OsuSettingsNumberBox : OsuSettingsTextBox - { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); - } } } diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index cef1814ee6..49e5ec0cbc 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mods } } - private readonly SettingsTextBox.OsuSettingsNumberBox seedNumberBox; + private readonly SettingsNumberBox.OsuSettingsNumberBox seedNumberBox; public SeedControl() { @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Mods { new Drawable[] { - seedNumberBox = new SettingsTextBox.OsuSettingsNumberBox + seedNumberBox = new SettingsNumberBox.OsuSettingsNumberBox { RelativeSizeAxes = Axes.X, CommitOnFocusLost = true From ef9cb2c95836a1ff7eeffe0d46f6cc5afe72b6e7 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Sat, 12 Jun 2021 18:37:31 +0200 Subject: [PATCH 308/670] Rename nested classes --- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 4 ++-- osu.Game/Overlays/Settings/SettingsTextBox.cs | 4 ++-- osu.Game/Rulesets/Mods/ModRandom.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index 20de35ed87..ca9a8e9c08 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -7,13 +7,13 @@ namespace osu.Game.Overlays.Settings { public class SettingsNumberBox : SettingsItem { - protected override Drawable CreateControl() => new OsuSettingsNumberBox + protected override Drawable CreateControl() => new NumberBox { Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, }; - public class OsuSettingsNumberBox : SettingsTextBox.OsuSettingsTextBox + public class NumberBox : SettingsTextBox.TextBox { protected override bool CanAddCharacter(char character) => char.IsNumber(character); } diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index efcfb0ec5b..25424e85a1 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs @@ -13,14 +13,14 @@ namespace osu.Game.Overlays.Settings { public class SettingsTextBox : SettingsItem { - protected override Drawable CreateControl() => new OsuSettingsTextBox + protected override Drawable CreateControl() => new TextBox { Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, CommitOnFocusLost = true, }; - public class OsuSettingsTextBox : OsuTextBox + public class TextBox : OsuTextBox { private const float border_thickness = 3; diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 49e5ec0cbc..e0c3008ae8 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mods } } - private readonly SettingsNumberBox.OsuSettingsNumberBox seedNumberBox; + private readonly SettingsNumberBox.NumberBox seedNumberBox; public SeedControl() { @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Mods { new Drawable[] { - seedNumberBox = new SettingsNumberBox.OsuSettingsNumberBox + seedNumberBox = new SettingsNumberBox.NumberBox { RelativeSizeAxes = Axes.X, CommitOnFocusLost = true From 17347401cf29d825686ab775a471698c5fb68304 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Jun 2021 11:27:45 +0900 Subject: [PATCH 309/670] Remove unused `RankingType` enum We have `BeatmapLeaderboardScope` instead. --- osu.Game/Configuration/RankingType.cs | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 osu.Game/Configuration/RankingType.cs diff --git a/osu.Game/Configuration/RankingType.cs b/osu.Game/Configuration/RankingType.cs deleted file mode 100644 index 7701e1dd1d..0000000000 --- a/osu.Game/Configuration/RankingType.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.ComponentModel; - -namespace osu.Game.Configuration -{ - public enum RankingType - { - Local, - - [Description("Global")] - Top, - - [Description("Selected Mods")] - SelectedMod, - Friends, - Country - } -} From 8cf44547802fff912ec0a9ce42032e51df1ac0f3 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 11 Jun 2021 23:50:41 +0900 Subject: [PATCH 310/670] Use `Direction` enum instead of `int` The property is named `scrollingAxis` to distinguish from `direction`, which is of `ScrollingDirection` type (unfortunate name crash). --- .../Scrolling/ScrollingHitObjectContainer.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index d21f30eb30..b2c549244d 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -20,9 +20,9 @@ namespace osu.Game.Rulesets.UI.Scrolling private readonly IBindable direction = new Bindable(); /// - /// 0 for horizontal scroll, 1 for vertical scroll. + /// Whether the scrolling direction is horizontal or vertical. /// - private int scrollingAxis => direction.Value == ScrollingDirection.Left || direction.Value == ScrollingDirection.Right ? 0 : 1; + private Direction scrollingAxis => direction.Value == ScrollingDirection.Left || direction.Value == ScrollingDirection.Right ? Direction.Horizontal : Direction.Vertical; /// /// A set of top-level s which have an up-to-date layout. @@ -68,8 +68,8 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) { - Vector2 localPosition = ToLocalSpace(screenSpacePosition); - return TimeAtPosition(localPosition[scrollingAxis], Time.Current); + Vector2 position = ToLocalSpace(screenSpacePosition); + return TimeAtPosition(scrollingAxis == Direction.Horizontal ? position.X : position.Y, Time.Current); } /// @@ -77,9 +77,9 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public float PositionAtTime(double time, double currentTime) { - float pos = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength); - flipPositionIfRequired(ref pos); - return pos; + float position = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength); + flipPositionIfRequired(ref position); + return position; } /// @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.UI.Scrolling public Vector2 ScreenSpacePositionAtTime(double time) { float position = PositionAtTime(time, Time.Current); - return scrollingAxis == 0 + return scrollingAxis == Direction.Horizontal ? ToScreenSpace(new Vector2(position, DrawHeight / 2)) : ToScreenSpace(new Vector2(DrawWidth / 2, position)); } @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.UI.Scrolling return scrollingInfo.Algorithm.GetLength(startTime, endTime, timeRange.Value, scrollLength); } - private float scrollLength => DrawSize[scrollingAxis]; + private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight; private void flipPositionIfRequired(ref float position) { @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.UI.Scrolling if (hitObject.HitObject is IHasDuration e) { float length = LengthAtTime(hitObject.HitObject.StartTime, e.EndTime); - if (scrollingAxis == 0) + if (scrollingAxis == Direction.Horizontal) hitObject.Width = length; else hitObject.Height = length; @@ -242,7 +242,7 @@ namespace osu.Game.Rulesets.UI.Scrolling { float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime); - if (scrollingAxis == 0) + if (scrollingAxis == Direction.Horizontal) hitObject.X = position; else hitObject.Y = position; From fdb09ef4d7a7a6791f887f84b4b91ed517fd2b59 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 11 Jun 2021 23:53:01 +0900 Subject: [PATCH 311/670] Simplify `flipPositionIfRequired` using `scrollLength` --- .../UI/Scrolling/ScrollingHitObjectContainer.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index b2c549244d..283d84e8df 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -114,17 +114,8 @@ namespace osu.Game.Rulesets.UI.Scrolling // We're dealing with coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, // so when scrolling downwards the coordinates need to be flipped. - - switch (scrollingInfo.Direction.Value) - { - case ScrollingDirection.Down: - position = DrawHeight - position; - break; - - case ScrollingDirection.Right: - position = DrawWidth - position; - break; - } + if (direction.Value == ScrollingDirection.Down || direction.Value == ScrollingDirection.Right) + position = scrollLength - position; } protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable) From 09f1cbde7eb13b8a72f9965ac0ee9ef933d622e3 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Jun 2021 12:41:44 +0900 Subject: [PATCH 312/670] Fix `TimeAtPosition` doc comment --- .../UI/Scrolling/ScrollingHitObjectContainer.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 283d84e8df..061c9aa948 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -53,19 +53,23 @@ namespace osu.Game.Rulesets.UI.Scrolling } /// - /// Given a position along the scrolling axis, return the time within this . + /// Given a position at , return the time of the object corresponding the position. /// - /// The position along the scrolling axis. - /// The time the scrolling speed is used. - public double TimeAtPosition(float position, double referenceTime) + /// + /// If there are multiple valid time values, one arbitrary time is returned. + /// + public double TimeAtPosition(float position, double currentTime) { flipPositionIfRequired(ref position); - return scrollingInfo.Algorithm.TimeAt(position, referenceTime, timeRange.Value, scrollLength); + return scrollingInfo.Algorithm.TimeAt(position, currentTime, timeRange.Value, scrollLength); } /// - /// Given a position in screen space, return the time within this . + /// Given a position at the current time in screen space, return the time of the object corresponding the position. /// + /// + /// If there are multiple valid time values, one arbitrary time is returned. + /// public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) { Vector2 position = ToLocalSpace(screenSpacePosition); From 660bf50dc7ce71115a64a4a8168826bd5cc6cf90 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Jun 2021 13:10:07 +0900 Subject: [PATCH 313/670] Clarify multiple coordinate systems - Fix wrong position is set for DHOs for down/right scrolling direction. --- .../Scrolling/ScrollingHitObjectContainer.cs | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 061c9aa948..d75954d77f 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.UI.Scrolling /// private Direction scrollingAxis => direction.Value == ScrollingDirection.Left || direction.Value == ScrollingDirection.Right ? Direction.Horizontal : Direction.Vertical; + /// + /// Whether the scrolling direction is the positive-to-negative direction in the local coordinate. + /// + private bool axisInverted => direction.Value == ScrollingDirection.Down || direction.Value == ScrollingDirection.Right; + /// /// A set of top-level s which have an up-to-date layout. /// @@ -58,10 +63,10 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// If there are multiple valid time values, one arbitrary time is returned. /// - public double TimeAtPosition(float position, double currentTime) + public double TimeAtPosition(float localPosition, double currentTime) { - flipPositionIfRequired(ref position); - return scrollingInfo.Algorithm.TimeAt(position, currentTime, timeRange.Value, scrollLength); + float scrollPosition = axisInverted ? scrollLength - localPosition : localPosition; + return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength); } /// @@ -72,8 +77,8 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) { - Vector2 position = ToLocalSpace(screenSpacePosition); - return TimeAtPosition(scrollingAxis == Direction.Horizontal ? position.X : position.Y, Time.Current); + Vector2 localPosition = ToLocalSpace(screenSpacePosition); + return TimeAtPosition(scrollingAxis == Direction.Horizontal ? localPosition.X : localPosition.Y, Time.Current); } /// @@ -81,9 +86,8 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public float PositionAtTime(double time, double currentTime) { - float position = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength); - flipPositionIfRequired(ref position); - return position; + float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength); + return axisInverted ? scrollLength - scrollPosition : scrollPosition; } /// @@ -97,10 +101,10 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public Vector2 ScreenSpacePositionAtTime(double time) { - float position = PositionAtTime(time, Time.Current); + float localPosition = PositionAtTime(time, Time.Current); return scrollingAxis == Direction.Horizontal - ? ToScreenSpace(new Vector2(position, DrawHeight / 2)) - : ToScreenSpace(new Vector2(DrawWidth / 2, position)); + ? ToScreenSpace(new Vector2(localPosition, DrawHeight / 2)) + : ToScreenSpace(new Vector2(DrawWidth / 2, localPosition)); } /// @@ -113,15 +117,6 @@ namespace osu.Game.Rulesets.UI.Scrolling private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight; - private void flipPositionIfRequired(ref float position) - { - // We're dealing with coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. - // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, - // so when scrolling downwards the coordinates need to be flipped. - if (direction.Value == ScrollingDirection.Down || direction.Value == ScrollingDirection.Right) - position = scrollLength - position; - } - protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable) { base.AddDrawable(entry, drawable); @@ -237,10 +232,14 @@ namespace osu.Game.Rulesets.UI.Scrolling { float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime); + // The position returned from `PositionAtTime` is assuming the `TopLeft` anchor. + // A correction is needed because the hit objects are using a different anchor for each direction (e.g. `BottomCentre` for `Bottom` direction). + float anchorCorrection = axisInverted ? scrollLength : 0; + if (scrollingAxis == Direction.Horizontal) - hitObject.X = position; + hitObject.X = position - anchorCorrection; else - hitObject.Y = position; + hitObject.Y = position - anchorCorrection; } } } From 564682270a9bb0d49faf095bbad1a919e0915f42 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Jun 2021 13:18:52 +0900 Subject: [PATCH 314/670] Revert "Add nested `PlatformActionContainer` to allow testing of platform actions in visual tests" This reverts commit be91203c92ba7004f0f03b32878b3a4182092584. --- osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index c7edc0174a..01dd7a25c8 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -4,7 +4,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; using osu.Framework.Testing.Input; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; @@ -49,7 +48,7 @@ namespace osu.Game.Tests.Visual InputManager = new ManualInputManager { UseParentInput = true, - Child = new PlatformActionContainer().WithChild(mainContent) + Child = mainContent }, new Container { From 8dd48d48f683823fe511f68fafe29b778a8393fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Jun 2021 14:20:23 +0900 Subject: [PATCH 315/670] Add support for song select leaderboard to handle newly imported scores --- osu.Game/Online/Leaderboards/Leaderboard.cs | 6 +++--- .../Select/Leaderboards/BeatmapLeaderboard.cs | 21 ++++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index d18f189a70..c7610e0ba6 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -44,9 +44,9 @@ namespace osu.Game.Online.Leaderboards protected override Container Content => content; - private IEnumerable scores; + private ICollection scores; - public IEnumerable Scores + public ICollection Scores { get => scores; set @@ -290,7 +290,7 @@ namespace osu.Game.Online.Leaderboards getScoresRequest = FetchScores(scores => Schedule(() => { - Scores = scores; + Scores = scores.ToArray(); PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; })); diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 8ddae67dba..2bbcb6678f 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -44,6 +44,8 @@ namespace osu.Game.Screens.Select.Leaderboards private IBindable> itemRemoved; + private IBindable> itemAdded; + /// /// Whether to apply the game's currently selected mods as a filter when retrieving scores. /// @@ -85,6 +87,9 @@ namespace osu.Game.Screens.Select.Leaderboards itemRemoved = scoreManager.ItemRemoved.GetBoundCopy(); itemRemoved.BindValueChanged(onScoreRemoved); + + itemAdded = scoreManager.ItemUpdated.GetBoundCopy(); + itemAdded.BindValueChanged(onScoreAdded); } protected override void Reset() @@ -93,7 +98,21 @@ namespace osu.Game.Screens.Select.Leaderboards TopScore = null; } - private void onScoreRemoved(ValueChangedEvent> score) => Schedule(RefreshScores); + private void onScoreRemoved(ValueChangedEvent> score) + { + if (Scope != BeatmapLeaderboardScope.Local) + return; + + Scheduler.AddOnce(RefreshScores); + } + + private void onScoreAdded(ValueChangedEvent> score) + { + if (Scope != BeatmapLeaderboardScope.Local) + return; + + Scheduler.AddOnce(RefreshScores); + } protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; From fc442713bbd46e072c8166db9fd0dc19fdea03c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Jun 2021 14:26:40 +0900 Subject: [PATCH 316/670] Debounce schedule at base class --- osu.Game/Online/Leaderboards/Leaderboard.cs | 10 +++++----- .../Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index c7610e0ba6..70e38e421d 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -126,7 +126,7 @@ namespace osu.Game.Online.Leaderboards return; scope = value; - UpdateScores(); + RefreshScores(); } } @@ -154,7 +154,7 @@ namespace osu.Game.Online.Leaderboards case PlaceholderState.NetworkFailure: replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) { - Action = UpdateScores, + Action = RefreshScores }); break; @@ -254,8 +254,6 @@ namespace osu.Game.Online.Leaderboards apiState.BindValueChanged(onlineStateChanged, true); } - public void RefreshScores() => UpdateScores(); - private APIRequest getScoresRequest; protected abstract bool IsOnlineScope { get; } @@ -267,12 +265,14 @@ namespace osu.Game.Online.Leaderboards case APIState.Online: case APIState.Offline: if (IsOnlineScope) - UpdateScores(); + RefreshScores(); break; } }); + public void RefreshScores() => Scheduler.AddOnce(UpdateScores); + protected void UpdateScores() { // don't display any scores or placeholder until the first Scores_Set has been called. diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 2bbcb6678f..d6967c17a8 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope != BeatmapLeaderboardScope.Local) return; - Scheduler.AddOnce(RefreshScores); + RefreshScores(); } private void onScoreAdded(ValueChangedEvent> score) @@ -111,7 +111,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope != BeatmapLeaderboardScope.Local) return; - Scheduler.AddOnce(RefreshScores); + RefreshScores(); } protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; From f8b09b7c81bfa370ed4765133c1231f520b21163 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Jun 2021 14:26:54 +0900 Subject: [PATCH 317/670] Avoid refresh if score is not related to current display --- .../Select/Leaderboards/BeatmapLeaderboard.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index d6967c17a8..587a35c480 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -98,18 +98,22 @@ namespace osu.Game.Screens.Select.Leaderboards TopScore = null; } - private void onScoreRemoved(ValueChangedEvent> score) + private void onScoreRemoved(ValueChangedEvent> score) => + scoreStoreChanged(score); + + private void onScoreAdded(ValueChangedEvent> score) => + scoreStoreChanged(score); + + private void scoreStoreChanged(ValueChangedEvent> score) { if (Scope != BeatmapLeaderboardScope.Local) return; - RefreshScores(); - } - - private void onScoreAdded(ValueChangedEvent> score) - { - if (Scope != BeatmapLeaderboardScope.Local) - return; + if (score.NewValue.TryGetTarget(out var scoreInfo)) + { + if (Beatmap.ID != scoreInfo.BeatmapInfoID) + return; + } RefreshScores(); } From b06477a1f59fbf00546dd84fce6af2b187cf4bb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Jun 2021 14:35:24 +0900 Subject: [PATCH 318/670] Split out tests into individual test methods --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 67cd720260..2a4ad48568 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; @@ -37,18 +38,37 @@ namespace osu.Game.Tests.Visual.SongSelect Size = new Vector2(550f, 450f), Scope = BeatmapLeaderboardScope.Global, }); + } + [Test] + public void TestScoresDisplay() + { AddStep(@"New Scores", newScores); + } + + [Test] + public void TestPersonalBest() + { AddStep(@"Show personal best", showPersonalBest); + AddStep("null personal best position", showPersonalBestWithNullPosition); + } + + [Test] + public void TestPlaceholderStates() + { AddStep(@"Empty Scores", () => leaderboard.SetRetrievalState(PlaceholderState.NoScores)); AddStep(@"Network failure", () => leaderboard.SetRetrievalState(PlaceholderState.NetworkFailure)); AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter)); AddStep(@"Not logged in", () => leaderboard.SetRetrievalState(PlaceholderState.NotLoggedIn)); AddStep(@"Unavailable", () => leaderboard.SetRetrievalState(PlaceholderState.Unavailable)); AddStep(@"None selected", () => leaderboard.SetRetrievalState(PlaceholderState.NoneSelected)); + } + + [Test] + public void TestBeatmapStates() + { foreach (BeatmapSetOnlineStatus status in Enum.GetValues(typeof(BeatmapSetOnlineStatus))) AddStep($"{status} beatmap", () => showBeatmapWithStatus(status)); - AddStep("null personal best position", showPersonalBestWithNullPosition); } private void showPersonalBestWithNullPosition() From 83402a70db9088e6ddf160b8ceb3de483aedcc28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Jun 2021 15:06:24 +0900 Subject: [PATCH 319/670] Fix potential null ref when no beatmap is selected --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 587a35c480..a86a614a05 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (score.NewValue.TryGetTarget(out var scoreInfo)) { - if (Beatmap.ID != scoreInfo.BeatmapInfoID) + if (Beatmap?.ID != scoreInfo.BeatmapInfoID) return; } From fcb0b8d825c887cb78937471e8877e2fec86c043 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Jun 2021 15:06:33 +0900 Subject: [PATCH 320/670] Add test coverage --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 110 +++++++++++++++--- 1 file changed, 94 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 2a4ad48568..184a2e59da 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -2,16 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -24,26 +30,73 @@ namespace osu.Game.Tests.Visual.SongSelect [Cached] private readonly DialogOverlay dialogOverlay; + private ScoreManager scoreManager; + + private RulesetStore rulesetStore; + private BeatmapManager beatmapManager; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); + + return dependencies; + } + public TestSceneBeatmapLeaderboard() { - Add(dialogOverlay = new DialogOverlay + AddRange(new Drawable[] { - Depth = -1 - }); - - Add(leaderboard = new FailableLeaderboard - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(550f, 450f), - Scope = BeatmapLeaderboardScope.Global, + dialogOverlay = new DialogOverlay + { + Depth = -1 + }, + leaderboard = new FailableLeaderboard + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(550f, 450f), + Scope = BeatmapLeaderboardScope.Global, + } }); } [Test] - public void TestScoresDisplay() + public void TestLocalScoresDisplay() { - AddStep(@"New Scores", newScores); + BeatmapInfo beatmapInfo = null; + + AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local); + + AddStep(@"Set beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + leaderboard.Beatmap = beatmapInfo; + }); + + clearScores(); + checkCount(0); + + loadMoreScores(() => beatmapInfo); + checkCount(10); + + loadMoreScores(() => beatmapInfo); + checkCount(20); + + clearScores(); + checkCount(0); + } + + [Test] + public void TestGlobalScoresDisplay() + { + AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); + AddStep(@"New Scores", () => leaderboard.Scores = generateSampleScores(null)); } [Test] @@ -116,9 +169,26 @@ namespace osu.Game.Tests.Visual.SongSelect }; } - private void newScores() + private void loadMoreScores(Func beatmapInfo) { - var scores = new[] + AddStep(@"Load new scores via manager", () => + { + foreach (var score in generateSampleScores(beatmapInfo())) + scoreManager.Import(score).Wait(); + }); + } + + private void clearScores() + { + AddStep("Clear all scores", () => scoreManager.Delete(scoreManager.GetAllUsableScores())); + } + + private void checkCount(int expected) => + AddUntilStep("Correct count displayed", () => leaderboard.ChildrenOfType().Count() == expected); + + private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmap) + { + return new[] { new ScoreInfo { @@ -127,6 +197,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Beatmap = beatmap, User = new User { Id = 6602580, @@ -145,6 +216,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Beatmap = beatmap, User = new User { Id = 4608074, @@ -163,6 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Beatmap = beatmap, User = new User { Id = 1014222, @@ -181,6 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Beatmap = beatmap, User = new User { Id = 1541390, @@ -199,6 +273,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Beatmap = beatmap, User = new User { Id = 2243452, @@ -217,6 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Beatmap = beatmap, User = new User { Id = 2705430, @@ -235,6 +311,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Beatmap = beatmap, User = new User { Id = 7151382, @@ -253,6 +330,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Beatmap = beatmap, User = new User { Id = 2051389, @@ -271,6 +349,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Beatmap = beatmap, User = new User { Id = 6169483, @@ -289,6 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Beatmap = beatmap, User = new User { Id = 6702666, @@ -301,8 +381,6 @@ namespace osu.Game.Tests.Visual.SongSelect }, }, }; - - leaderboard.Scores = scores; } private void showBeatmapWithStatus(BeatmapSetOnlineStatus status) From aa5dae84b2ad3c20580d0a5f7a44aa880c9c1603 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 14 Jun 2021 16:51:17 +0900 Subject: [PATCH 321/670] Make all localisation class strings verbatim --- osu.Game/Localisation/ChatStrings.cs | 6 +++--- osu.Game/Localisation/CommonStrings.cs | 4 ++-- osu.Game/Localisation/Language.cs | 4 ++-- osu.Game/Localisation/NotificationsStrings.cs | 6 +++--- osu.Game/Localisation/NowPlayingStrings.cs | 6 +++--- osu.Game/Localisation/SettingsStrings.cs | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs index daddb602ad..636351470b 100644 --- a/osu.Game/Localisation/ChatStrings.cs +++ b/osu.Game/Localisation/ChatStrings.cs @@ -7,17 +7,17 @@ namespace osu.Game.Localisation { public static class ChatStrings { - private const string prefix = "osu.Game.Localisation.Chat"; + private const string prefix = @"osu.Game.Localisation.Chat"; /// /// "chat" /// - public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "chat"); + public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"chat"); /// /// "join the real-time discussion" /// - public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "join the real-time discussion"); + public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"join the real-time discussion"); private static string getKey(string key) => $"{prefix}:{key}"; } diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index f448158191..ced0d80955 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -7,12 +7,12 @@ namespace osu.Game.Localisation { public static class CommonStrings { - private const string prefix = "osu.Game.Localisation.Common"; + private const string prefix = @"osu.Game.Localisation.Common"; /// /// "Cancel" /// - public static LocalisableString Cancel => new TranslatableString(getKey("cancel"), "Cancel"); + public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"Cancel"); private static string getKey(string key) => $"{prefix}:{key}"; } diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index edcf264c7f..a3e845f229 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.cs @@ -7,10 +7,10 @@ namespace osu.Game.Localisation { public enum Language { - [Description("English")] + [Description(@"English")] en, - [Description("日本語")] + [Description(@"日本語")] ja } } diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 092eec3a6b..ba28ef5560 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -7,17 +7,17 @@ namespace osu.Game.Localisation { public static class NotificationsStrings { - private const string prefix = "osu.Game.Localisation.Notifications"; + private const string prefix = @"osu.Game.Localisation.Notifications"; /// /// "notifications" /// - public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "notifications"); + public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"notifications"); /// /// "waiting for 'ya" /// - public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "waiting for 'ya"); + public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"waiting for 'ya"); private static string getKey(string key) => $"{prefix}:{key}"; } diff --git a/osu.Game/Localisation/NowPlayingStrings.cs b/osu.Game/Localisation/NowPlayingStrings.cs index d742a56895..47646b0f68 100644 --- a/osu.Game/Localisation/NowPlayingStrings.cs +++ b/osu.Game/Localisation/NowPlayingStrings.cs @@ -7,17 +7,17 @@ namespace osu.Game.Localisation { public static class NowPlayingStrings { - private const string prefix = "osu.Game.Localisation.NowPlaying"; + private const string prefix = @"osu.Game.Localisation.NowPlaying"; /// /// "now playing" /// - public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "now playing"); + public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"now playing"); /// /// "manage the currently playing track" /// - public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "manage the currently playing track"); + public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"manage the currently playing track"); private static string getKey(string key) => $"{prefix}:{key}"; } diff --git a/osu.Game/Localisation/SettingsStrings.cs b/osu.Game/Localisation/SettingsStrings.cs index cfbd392691..f4b417fa28 100644 --- a/osu.Game/Localisation/SettingsStrings.cs +++ b/osu.Game/Localisation/SettingsStrings.cs @@ -7,17 +7,17 @@ namespace osu.Game.Localisation { public static class SettingsStrings { - private const string prefix = "osu.Game.Localisation.Settings"; + private const string prefix = @"osu.Game.Localisation.Settings"; /// /// "settings" /// - public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "settings"); + public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"settings"); /// /// "change the way osu! behaves" /// - public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "change the way osu! behaves"); + public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"change the way osu! behaves"); private static string getKey(string key) => $"{prefix}:{key}"; } From b327baa4dea5812234c8ef9754c61a0bfbfcba61 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 14 Jun 2021 17:47:31 +0900 Subject: [PATCH 322/670] Match any arbitrary assembly for localisations --- .../ResourceManagerLocalisationStore.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs index 7b21e1af42..a35ce7a9c8 100644 --- a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs +++ b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Resources; using System.Threading.Tasks; using osu.Framework.Localisation; @@ -34,7 +35,29 @@ namespace osu.Game.Localisation lock (resourceManagers) { if (!resourceManagers.TryGetValue(ns, out var manager)) - resourceManagers[ns] = manager = new ResourceManager(ns, GetType().Assembly); + { + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + + // Traverse backwards through periods in the namespace to find a matching assembly. + string assemblyName = ns; + + while (!string.IsNullOrEmpty(assemblyName)) + { + var matchingAssembly = loadedAssemblies.FirstOrDefault(asm => asm.GetName().Name == assemblyName); + + if (matchingAssembly != null) + { + resourceManagers[ns] = manager = new ResourceManager(ns, matchingAssembly); + break; + } + + int lastIndex = Math.Max(0, assemblyName.LastIndexOf('.')); + assemblyName = assemblyName.Substring(0, lastIndex); + } + } + + if (manager == null) + return null; try { From 13d0eaa9fe9b7d418d544cf35ea7d0fef55ab04f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Jun 2021 19:03:31 +0900 Subject: [PATCH 323/670] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 13d45835be..c020b1d783 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7eb3c84582..a7bd5f2e9f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -34,7 +34,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 3e8facaf6e..5b3efb4ba4 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From ca061c4b939fdc8b8782ba0e80885f17da3965d5 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Jun 2021 19:41:51 +0900 Subject: [PATCH 324/670] Factor out `SkinnableDrawable` component of the catcher to `SkinnableCatcher` --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 37 ++++++------------- .../UI/SkinnableCatcher.cs | 26 +++++++++++++ 2 files changed, 37 insertions(+), 26 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index ee2986c73c..dce89a9dae 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -17,7 +17,6 @@ using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Skinning; -using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Rulesets.Judgements; using osu.Game.Skinning; using osuTK; @@ -83,17 +82,18 @@ namespace osu.Game.Rulesets.Catch.UI /// private readonly Container droppedObjectTarget; - [Cached] - protected readonly Bindable CurrentStateBindable = new Bindable(); - - public CatcherAnimationState CurrentState => CurrentStateBindable.Value; + public CatcherAnimationState CurrentState + { + get => body.AnimationState.Value; + private set => body.AnimationState.Value = value; + } /// /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable. /// public const float ALLOWED_CATCH_RANGE = 0.8f; - internal Texture CurrentTexture => ((ICatcherSprite)currentCatcher.Drawable).CurrentTexture; + internal Texture CurrentTexture => ((ICatcherSprite)body.Drawable).CurrentTexture; private bool dashing; @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.UI /// private readonly float catchWidth; - private readonly SkinnableDrawable currentCatcher; + private readonly SkinnableCatcher body; private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR; private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR; @@ -161,13 +161,7 @@ namespace osu.Game.Rulesets.Catch.UI Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, }, - currentCatcher = new SkinnableDrawable( - new CatchSkinComponent(CatchSkinComponents.Catcher), - _ => new DefaultCatcher()) - { - Anchor = Anchor.TopCentre, - OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE - }, + body = new SkinnableCatcher(), hitExplosionContainer = new HitExplosionContainer { Anchor = Anchor.TopCentre, @@ -268,17 +262,16 @@ namespace osu.Game.Rulesets.Catch.UI SetHyperDashState(); if (result.IsHit) - updateState(hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); + CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle; else if (!(hitObject is Banana)) - updateState(CatcherAnimationState.Fail); + CurrentState = CatcherAnimationState.Fail; } public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result) { var catchResult = (CatchJudgementResult)result; - if (CurrentState != catchResult.CatcherAnimationState) - updateState(catchResult.CatcherAnimationState); + CurrentState = catchResult.CatcherAnimationState; if (HyperDashing != catchResult.CatcherHyperDash) { @@ -373,14 +366,6 @@ namespace osu.Game.Rulesets.Catch.UI } } - private void updateState(CatcherAnimationState state) - { - if (CurrentState == state) - return; - - CurrentStateBindable.Value = state; - } - private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position) { var caughtObject = getCaughtObject(drawableObject.HitObject); diff --git a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs new file mode 100644 index 0000000000..5bd97858b2 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.Skinning.Default; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class SkinnableCatcher : SkinnableDrawable + { + [Cached] + public readonly Bindable AnimationState = new Bindable(); + + public SkinnableCatcher() + : base(new CatchSkinComponent(CatchSkinComponents.Catcher), _ => new DefaultCatcher()) + { + Anchor = Anchor.TopCentre; + // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling. + OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE; + } + } +} From 9b6ab4360e497a7b0e32fa1d21bf490d8d70e943 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Jun 2021 19:45:58 +0900 Subject: [PATCH 325/670] Use common skinnable catcher in catcher trails --- .../UI/CatcherTrailDisplay.cs | 4 ++-- .../UI/CatcherTrailSprite.cs | 18 +++++------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index 0aef215797..dbe41121c8 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -120,8 +120,8 @@ namespace osu.Game.Rulesets.Catch.UI { CatcherTrailSprite sprite = trailPool.Get(); - sprite.Texture = catcher.CurrentTexture; - sprite.Anchor = catcher.Anchor; + sprite.AnimationState = catcher.CurrentState; + sprite.Origin = catcher.Origin; sprite.Scale = catcher.Scale; sprite.Blending = BlendingParameters.Additive; sprite.RelativePositionAxes = catcher.RelativePositionAxes; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs index 0e3e409fac..c4bb0aa1f2 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs @@ -3,32 +3,24 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osuTK; namespace osu.Game.Rulesets.Catch.UI { public class CatcherTrailSprite : PoolableDrawable { - public Texture Texture + public CatcherAnimationState AnimationState { - set => sprite.Texture = value; + set => body.AnimationState.Value = value; } - private readonly Sprite sprite; + private readonly SkinnableCatcher body; public CatcherTrailSprite() { - InternalChild = sprite = new Sprite - { - RelativeSizeAxes = Axes.Both - }; - Size = new Vector2(CatcherArea.CATCHER_SIZE); - - // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling. - OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE; + Origin = Anchor.TopCentre; + InternalChild = body = new SkinnableCatcher(); } protected override void FreeAfterUse() From c094914023aab5e29e71cba493a5858233fbd8cc Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Jun 2021 19:46:48 +0900 Subject: [PATCH 326/670] Simplify catcher trail creation --- osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs | 3 --- osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index dbe41121c8..382e796480 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -121,10 +121,7 @@ namespace osu.Game.Rulesets.Catch.UI CatcherTrailSprite sprite = trailPool.Get(); sprite.AnimationState = catcher.CurrentState; - sprite.Origin = catcher.Origin; sprite.Scale = catcher.Scale; - sprite.Blending = BlendingParameters.Additive; - sprite.RelativePositionAxes = catcher.RelativePositionAxes; sprite.Position = catcher.Position; target.Add(sprite); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs index c4bb0aa1f2..8417e5a250 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Catch.UI { Size = new Vector2(CatcherArea.CATCHER_SIZE); Origin = Anchor.TopCentre; + Blending = BlendingParameters.Additive; InternalChild = body = new SkinnableCatcher(); } From 38a56d64d316404ac0ca2260ca1ad2efc46a5b94 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Jun 2021 19:47:18 +0900 Subject: [PATCH 327/670] Rename `CatcherTrailSprite` -> `CatcherTrail` --- ...{CatcherTrailSprite.cs => CatcherTrail.cs} | 4 ++-- .../UI/CatcherTrailDisplay.cs | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) rename osu.Game.Rulesets.Catch/UI/{CatcherTrailSprite.cs => CatcherTrail.cs} (90%) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs similarity index 90% rename from osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs rename to osu.Game.Rulesets.Catch/UI/CatcherTrail.cs index 8417e5a250..90fb59db9a 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs @@ -7,7 +7,7 @@ using osuTK; namespace osu.Game.Rulesets.Catch.UI { - public class CatcherTrailSprite : PoolableDrawable + public class CatcherTrail : PoolableDrawable { public CatcherAnimationState AnimationState { @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.UI private readonly SkinnableCatcher body; - public CatcherTrailSprite() + public CatcherTrail() { Size = new Vector2(CatcherArea.CATCHER_SIZE); Origin = Anchor.TopCentre; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index 382e796480..7e4a5b6a86 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -19,11 +19,11 @@ namespace osu.Game.Rulesets.Catch.UI { private readonly Catcher catcher; - private readonly DrawablePool trailPool; + private readonly DrawablePool trailPool; - private readonly Container dashTrails; - private readonly Container hyperDashTrails; - private readonly Container endGlowSprites; + private readonly Container dashTrails; + private readonly Container hyperDashTrails; + private readonly Container endGlowSprites; private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; @@ -83,10 +83,10 @@ namespace osu.Game.Rulesets.Catch.UI InternalChildren = new Drawable[] { - trailPool = new DrawablePool(30), - dashTrails = new Container { RelativeSizeAxes = Axes.Both }, - hyperDashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR }, - endGlowSprites = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR }, + trailPool = new DrawablePool(30), + dashTrails = new Container { RelativeSizeAxes = Axes.Both }, + hyperDashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR }, + endGlowSprites = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR }, }; } @@ -116,9 +116,9 @@ namespace osu.Game.Rulesets.Catch.UI Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50); } - private CatcherTrailSprite createTrailSprite(Container target) + private CatcherTrail createTrailSprite(Container target) { - CatcherTrailSprite sprite = trailPool.Get(); + CatcherTrail sprite = trailPool.Get(); sprite.AnimationState = catcher.CurrentState; sprite.Scale = catcher.Scale; From df16d4baccff180aadb9a441d9315f11f1e67811 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Jun 2021 20:26:33 +0900 Subject: [PATCH 328/670] Remove `CurrentTexture` from catcher --- .../Skinning/Default/DefaultCatcher.cs | 4 +--- osu.Game.Rulesets.Catch/Skinning/ICatcherSprite.cs | 12 ------------ .../Skinning/Legacy/LegacyCatcherNew.cs | 6 +----- .../Skinning/Legacy/LegacyCatcherOld.cs | 7 +------ osu.Game.Rulesets.Catch/UI/Catcher.cs | 3 --- 5 files changed, 3 insertions(+), 29 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch/Skinning/ICatcherSprite.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs index 364fc211a0..e423f21b98 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs @@ -12,12 +12,10 @@ using osu.Game.Rulesets.Catch.UI; namespace osu.Game.Rulesets.Catch.Skinning.Default { - public class DefaultCatcher : CompositeDrawable, ICatcherSprite + public class DefaultCatcher : CompositeDrawable { public Bindable CurrentState { get; } = new Bindable(); - public Texture CurrentTexture => sprite.Texture; - private readonly Sprite sprite; private readonly Dictionary textures = new Dictionary(); diff --git a/osu.Game.Rulesets.Catch/Skinning/ICatcherSprite.cs b/osu.Game.Rulesets.Catch/Skinning/ICatcherSprite.cs deleted file mode 100644 index 073868e947..0000000000 --- a/osu.Game.Rulesets.Catch/Skinning/ICatcherSprite.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Textures; - -namespace osu.Game.Rulesets.Catch.Skinning -{ - public interface ICatcherSprite - { - Texture CurrentTexture { get; } - } -} diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs index 2bf8b28aa2..9df87c92ea 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs @@ -9,21 +9,17 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Catch.UI; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { - public class LegacyCatcherNew : CompositeDrawable, ICatcherSprite + public class LegacyCatcherNew : CompositeDrawable { [Resolved] private Bindable currentState { get; set; } - public Texture CurrentTexture => (currentDrawable as TextureAnimation)?.CurrentFrame ?? (currentDrawable as Sprite)?.Texture; - private readonly Dictionary drawables = new Dictionary(); private Drawable currentDrawable; diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs index a8948d2ed0..3e679171b2 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs @@ -3,19 +3,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { - public class LegacyCatcherOld : CompositeDrawable, ICatcherSprite + public class LegacyCatcherOld : CompositeDrawable { - public Texture CurrentTexture => (InternalChild as TextureAnimation)?.CurrentFrame ?? (InternalChild as Sprite)?.Texture; - public LegacyCatcherOld() { RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index dce89a9dae..1f01dbabb5 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Textures; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -93,8 +92,6 @@ namespace osu.Game.Rulesets.Catch.UI /// public const float ALLOWED_CATCH_RANGE = 0.8f; - internal Texture CurrentTexture => ((ICatcherSprite)body.Drawable).CurrentTexture; - private bool dashing; public bool Dashing From cb1e2e3d9785748718a37c43f4b01c8e9c704342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Jun 2021 21:51:32 +0200 Subject: [PATCH 329/670] Improve xmldoc --- .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index d75954d77f..94cc7ed095 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -25,8 +25,12 @@ namespace osu.Game.Rulesets.UI.Scrolling private Direction scrollingAxis => direction.Value == ScrollingDirection.Left || direction.Value == ScrollingDirection.Right ? Direction.Horizontal : Direction.Vertical; /// - /// Whether the scrolling direction is the positive-to-negative direction in the local coordinate. + /// The scrolling axis is inverted if objects temporally farther in the future have a smaller position value across the scrolling axis. /// + /// + /// is inverted, because given two objects, one of which is at the current time and one of which is 1000ms in the future, + /// in the current time instant the future object is spatially above the current object, and therefore has a smaller value of the Y coordinate of its position. + /// private bool axisInverted => direction.Value == ScrollingDirection.Down || direction.Value == ScrollingDirection.Right; /// @@ -58,7 +62,7 @@ namespace osu.Game.Rulesets.UI.Scrolling } /// - /// Given a position at , return the time of the object corresponding the position. + /// Given a position at , return the time of the object corresponding to the position. /// /// /// If there are multiple valid time values, one arbitrary time is returned. From 9d9c5902bbe616ffcfdc53b116f907cdb3644f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Jun 2021 22:46:56 +0200 Subject: [PATCH 330/670] Temporarily disable `dotnet format` to unblock CI --- appveyor.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a4a0cedc66..845751ef07 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,7 +17,11 @@ build: publish_nuget: true after_build: - ps: dotnet tool restore - - ps: dotnet format --dry-run --check + + # Temporarily disabled until the tool is upgraded to 5.0. + # The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7. + # - ps: dotnet format --dry-run --check + - ps: .\InspectCode.ps1 test: assemblies: From f6c6eea6dce32a50514fb3713f4ef301c0f1c1ba Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 15 Jun 2021 11:14:35 +0900 Subject: [PATCH 331/670] Make PresentScore() only consider replay hash --- osu.Game/OsuGame.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 019d3b3cd0..1466d685d6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -426,9 +426,8 @@ namespace osu.Game { // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database // to ensure all the required data for presenting a replay are present. - var databasedScoreInfo = score.OnlineScoreID != null - ? ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID) - : ScoreManager.Query(s => s.Hash == score.Hash); + // Todo: This should use the OnlineScoreID if available, however lazer scores are imported without an OnlineScoreID for the time being (see Player.ImportScore()). + var databasedScoreInfo = ScoreManager.Query(s => s.Hash == score.Hash); if (databasedScoreInfo == null) { From bbf00226898dd7c374a17712866f3d8fa40266f1 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 15 Jun 2021 13:11:07 +0900 Subject: [PATCH 332/670] Use natural anchor for `TimeAtPosition` and `PositionAtTime` The natural anchor is the end of the scrolling direction (e.g. Bottom for Down scrolling). --- .../Scrolling/ScrollingHitObjectContainer.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 94cc7ed095..3b15bc2cdf 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public double TimeAtPosition(float localPosition, double currentTime) { - float scrollPosition = axisInverted ? scrollLength - localPosition : localPosition; + float scrollPosition = axisInverted ? -localPosition : localPosition; return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength); } @@ -81,8 +81,10 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) { - Vector2 localPosition = ToLocalSpace(screenSpacePosition); - return TimeAtPosition(scrollingAxis == Direction.Horizontal ? localPosition.X : localPosition.Y, Time.Current); + Vector2 pos = ToLocalSpace(screenSpacePosition); + float localPosition = scrollingAxis == Direction.Horizontal ? pos.X : pos.Y; + localPosition -= axisInverted ? scrollLength : 0; + return TimeAtPosition(localPosition, Time.Current); } /// @@ -91,7 +93,7 @@ namespace osu.Game.Rulesets.UI.Scrolling public float PositionAtTime(double time, double currentTime) { float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength); - return axisInverted ? scrollLength - scrollPosition : scrollPosition; + return axisInverted ? -scrollPosition : scrollPosition; } /// @@ -106,6 +108,7 @@ namespace osu.Game.Rulesets.UI.Scrolling public Vector2 ScreenSpacePositionAtTime(double time) { float localPosition = PositionAtTime(time, Time.Current); + localPosition += axisInverted ? scrollLength : 0; return scrollingAxis == Direction.Horizontal ? ToScreenSpace(new Vector2(localPosition, DrawHeight / 2)) : ToScreenSpace(new Vector2(DrawWidth / 2, localPosition)); @@ -236,14 +239,10 @@ namespace osu.Game.Rulesets.UI.Scrolling { float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime); - // The position returned from `PositionAtTime` is assuming the `TopLeft` anchor. - // A correction is needed because the hit objects are using a different anchor for each direction (e.g. `BottomCentre` for `Bottom` direction). - float anchorCorrection = axisInverted ? scrollLength : 0; - if (scrollingAxis == Direction.Horizontal) - hitObject.X = position - anchorCorrection; + hitObject.X = position; else - hitObject.Y = position - anchorCorrection; + hitObject.Y = position; } } } From d0e57f7dd943f8266ddd6267e009bf5424bcc621 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 15 Jun 2021 13:20:33 +0900 Subject: [PATCH 333/670] Use `HitObject` instead of DHO for mania selection blueprint layout - Fix moving selected hold note between columns will cause a crash --- .../Editor/TestSceneManiaHitObjectComposer.cs | 6 +-- .../Edit/Blueprints/HoldNoteNoteOverlay.cs | 43 ------------------- .../Edit/Blueprints/HoldNotePosition.cs | 11 ----- .../Blueprints/HoldNoteSelectionBlueprint.cs | 32 +++++--------- .../Blueprints/ManiaSelectionBlueprint.cs | 26 +++++------ .../Edit/Blueprints/NoteSelectionBlueprint.cs | 9 ---- 6 files changed, 24 insertions(+), 103 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteOverlay.cs delete mode 100644 osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index 8474279b01..01d80881fa 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -12,7 +12,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Edit; -using osu.Game.Rulesets.Mania.Edit.Blueprints; +using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Skinning.Default; @@ -184,8 +184,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft)); AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft)); - AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition); - AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition); + AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition); + AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition); } private void setScrollStep(ScrollingDirection direction) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteOverlay.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteOverlay.cs deleted file mode 100644 index 6933571be8..0000000000 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteOverlay.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; -using osu.Game.Rulesets.Mania.Objects.Drawables; - -namespace osu.Game.Rulesets.Mania.Edit.Blueprints -{ - public class HoldNoteNoteOverlay : CompositeDrawable - { - private readonly HoldNoteSelectionBlueprint holdNoteBlueprint; - private readonly HoldNotePosition position; - - public HoldNoteNoteOverlay(HoldNoteSelectionBlueprint holdNoteBlueprint, HoldNotePosition position) - { - this.holdNoteBlueprint = holdNoteBlueprint; - this.position = position; - - InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X }; - } - - protected override void Update() - { - base.Update(); - - var drawableObject = holdNoteBlueprint.DrawableObject; - - // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly. - if (drawableObject.IsLoaded) - { - DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)drawableObject.Head : drawableObject.Tail; - - Anchor = note.Anchor; - Origin = note.Origin; - - Size = note.DrawSize; - Position = note.DrawPosition; - } - } - } -} diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs deleted file mode 100644 index 219dad566d..0000000000 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePosition.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Mania.Edit.Blueprints -{ - public enum HoldNotePosition - { - Start, - End - } -} diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index d04c5cd4aa..5259fcbd5f 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -2,14 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osuTK; @@ -17,13 +16,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint { - public new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject; - - private readonly IBindable direction = new Bindable(); - [Resolved] private OsuColour colours { get; set; } + private EditNotePiece head; + private EditNotePiece tail; + public HoldNoteSelectionBlueprint(HoldNote hold) : base(hold) { @@ -32,12 +30,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [BackgroundDependencyLoader] private void load(IScrollingInfo scrollingInfo) { - direction.BindTo(scrollingInfo.Direction); - InternalChildren = new Drawable[] { - new HoldNoteNoteOverlay(this, HoldNotePosition.Start), - new HoldNoteNoteOverlay(this, HoldNotePosition.End), + head = new EditNotePiece { RelativeSizeAxes = Axes.X }, + tail = new EditNotePiece { RelativeSizeAxes = Axes.X }, new Container { RelativeSizeAxes = Axes.Both, @@ -58,21 +54,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { base.Update(); - // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly. - if (DrawableObject.IsLoaded) - { - Size = DrawableObject.DrawSize + new Vector2(0, DrawableObject.Tail.DrawHeight); - - // This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do - // When scrolling upwards our origin is already at the top of the head note (which is the intended location), - // but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note) - if (direction.Value == ScrollingDirection.Down) - Y -= DrawableObject.Tail.DrawHeight; - } + head.Y = HitObjectContainer.PositionAtTime(HitObject.Head.StartTime, HitObject.StartTime); + tail.Y = HitObjectContainer.PositionAtTime(HitObject.Tail.StartTime, HitObject.StartTime); + Height = HitObjectContainer.LengthAtTime(HitObject.StartTime, HitObject.EndTime) + tail.DrawHeight; } public override Quad SelectionQuad => ScreenSpaceDrawQuad; - public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre; + public override Vector2 ScreenSpaceSelectionPoint => head.ScreenSpaceDrawQuad.Centre; } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index e744bd3c83..bd1e5c22b3 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -5,20 +5,22 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; -using osuTK; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { public abstract class ManiaSelectionBlueprint : HitObjectSelectionBlueprint where T : ManiaHitObject { - public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject; + [Resolved] + private HitObjectComposer composer { get; set; } [Resolved] private IScrollingInfo scrollingInfo { get; set; } + protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)composer.Playfield).GetColumn(HitObject.Column).HitObjectContainer; + protected ManiaSelectionBlueprint(T hitObject) : base(hitObject) { @@ -29,19 +31,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { base.Update(); - Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero)); - } + var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Anchor = Origin = anchor; + foreach (var child in InternalChildren) + child.Anchor = child.Origin = anchor; - public override void Show() - { - DrawableObject.AlwaysAlive = true; - base.Show(); - } - - public override void Hide() - { - DrawableObject.AlwaysAlive = false; - base.Hide(); + Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition; + Width = HitObjectContainer.DrawWidth; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs index e2b6ee0048..e7a03905d2 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs @@ -14,14 +14,5 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X }); } - - protected override void Update() - { - base.Update(); - - // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly. - if (DrawableObject.IsLoaded) - Size = DrawableObject.DrawSize; - } } } From eb4c093371267c6446d6d37ed8a51cfb42690db6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 15 Jun 2021 14:06:17 +0900 Subject: [PATCH 334/670] Use hash as fallback --- osu.Game/OsuGame.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1466d685d6..b226932555 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -427,7 +427,12 @@ namespace osu.Game // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database // to ensure all the required data for presenting a replay are present. // Todo: This should use the OnlineScoreID if available, however lazer scores are imported without an OnlineScoreID for the time being (see Player.ImportScore()). - var databasedScoreInfo = ScoreManager.Query(s => s.Hash == score.Hash); + ScoreInfo databasedScoreInfo = null; + + if (score.OnlineScoreID != null) + databasedScoreInfo = ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID); + + databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash); if (databasedScoreInfo == null) { From 579a4aa9c8032da0c18fda6f7de4ea34abd33157 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 15 Jun 2021 14:10:09 +0900 Subject: [PATCH 335/670] Remove comment --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b226932555..da104852e3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -426,7 +426,6 @@ namespace osu.Game { // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database // to ensure all the required data for presenting a replay are present. - // Todo: This should use the OnlineScoreID if available, however lazer scores are imported without an OnlineScoreID for the time being (see Player.ImportScore()). ScoreInfo databasedScoreInfo = null; if (score.OnlineScoreID != null) From ef96ceb4abc61821768585e269ce2fc81582c43b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 15 Jun 2021 14:43:04 +0900 Subject: [PATCH 336/670] Introduce `IPlayfieldProvider` --- .../Edit/Blueprints/ManiaSelectionBlueprint.cs | 4 ++-- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 3 ++- osu.Game/Rulesets/Edit/IPlayfieldProvider.cs | 12 ++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Rulesets/Edit/IPlayfieldProvider.cs diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index bd1e5c22b3..1b5cb03204 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -14,12 +14,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints where T : ManiaHitObject { [Resolved] - private HitObjectComposer composer { get; set; } + private IPlayfieldProvider playfieldProvider { get; set; } [Resolved] private IScrollingInfo scrollingInfo { get; set; } - protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)composer.Playfield).GetColumn(HitObject.Column).HitObjectContainer; + protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfieldProvider.Playfield).GetColumn(HitObject.Column).HitObjectContainer; protected ManiaSelectionBlueprint(T hitObject) : base(hitObject) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b47cf97a4d..67c18b7e3c 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -415,7 +415,8 @@ namespace osu.Game.Rulesets.Edit /// [Cached(typeof(HitObjectComposer))] [Cached(typeof(IPositionSnapProvider))] - public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider + [Cached(typeof(IPlayfieldProvider))] + public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider, IPlayfieldProvider { protected HitObjectComposer() { diff --git a/osu.Game/Rulesets/Edit/IPlayfieldProvider.cs b/osu.Game/Rulesets/Edit/IPlayfieldProvider.cs new file mode 100644 index 0000000000..4bfd4d2728 --- /dev/null +++ b/osu.Game/Rulesets/Edit/IPlayfieldProvider.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Edit +{ + public interface IPlayfieldProvider + { + Playfield Playfield { get; } + } +} From 403aa433cfcd5f49f20873d68df46f2eaf6d825d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 15 Jun 2021 15:14:14 +0900 Subject: [PATCH 337/670] Rewrite mania selection blueprint test scene --- .../ManiaSelectionBlueprintTestScene.cs | 48 ++++++++++++++----- .../TestSceneHoldNoteSelectionBlueprint.cs | 40 ++-------------- .../Editor/TestSceneNoteSelectionBlueprint.cs | 22 ++------- 3 files changed, 42 insertions(+), 68 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs index 176fbba921..36749fad1c 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs @@ -1,31 +1,53 @@ // 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.Graphics; -using osu.Framework.Timing; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; -using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Tests.Editor { - public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene + [Cached(typeof(IPlayfieldProvider))] + public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene, IPlayfieldProvider { - [Cached(Type = typeof(IAdjustableClock))] - private readonly IAdjustableClock clock = new StopwatchClock(); + protected override Container Content => blueprints ?? base.Content; + + private readonly Container blueprints; + + public Playfield Playfield { get; } + + private readonly ScrollingTestContainer scrollingTestContainer; + + protected ScrollingDirection Direction + { + set => scrollingTestContainer.Direction = value; + } protected ManiaSelectionBlueprintTestScene() { - Add(new Column(0) + var stageDefinitions = new List { new StageDefinition { Columns = 1 } }; + base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Down) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AccentColour = Color4.OrangeRed, - Clock = new FramedClock(new StopwatchClock()), // No scroll - }); + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + Playfield = new ManiaPlayfield(stageDefinitions) + { + RelativeSizeAxes = Axes.Both, + }, + blueprints = new Container + { + RelativeSizeAxes = Axes.Both + } + } + }; } - - public ManiaPlayfield Playfield => null; } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs index 5e99264d7d..3cf9c6ad65 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs @@ -1,56 +1,24 @@ // 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.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene { - private readonly DrawableHoldNote drawableObject; - - protected override Container Content => content ?? base.Content; - private readonly Container content; - public TestSceneHoldNoteSelectionBlueprint() { - var holdNote = new HoldNote { Column = 0, Duration = 1000 }; + var holdNote = new HoldNote { Column = 0, Duration = 500 }; holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - Width = 50, - Child = drawableObject = new DrawableHoldNote(holdNote) - { - Height = 300, - AccentColour = { Value = OsuColour.Gray(0.3f) } - } - }; - - AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableObject); - } - - protected override void Update() - { - base.Update(); - - foreach (var nested in drawableObject.NestedHitObjects) - { - double finalPosition = (nested.HitObject.StartTime - drawableObject.HitObject.StartTime) / drawableObject.HitObject.Duration; - nested.Y = (float)(-finalPosition * content.DrawHeight); - } + var drawableHitObject = new DrawableHoldNote(holdNote); + Playfield.Add(drawableHitObject); + AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableHitObject); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs index 9c3ad0b4ff..dc33e30de7 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs @@ -1,40 +1,24 @@ // 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.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Tests.Visual; -using osuTK; namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene { - protected override Container Content => content ?? base.Content; - private readonly Container content; - public TestSceneNoteSelectionBlueprint() { var note = new Note { Column = 0 }; note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - DrawableNote drawableObject; - - base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(50, 20), - Child = drawableObject = new DrawableNote(note) - }; - - AddBlueprint(new NoteSelectionBlueprint(note), drawableObject); + var drawableHitObject = new DrawableNote(note); + Playfield.Add(drawableHitObject); + AddBlueprint(new NoteSelectionBlueprint(note), drawableHitObject); } } } From a431b4eeda6531401581c459a133671a51621555 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 15 Jun 2021 15:22:36 +0900 Subject: [PATCH 338/670] Add scrolling direction toggle for mania selection blueprint test scene --- .../ManiaSelectionBlueprintTestScene.cs | 8 +++++--- .../TestSceneHoldNoteSelectionBlueprint.cs | 19 ++++++++++++++----- .../Editor/TestSceneNoteSelectionBlueprint.cs | 18 +++++++++++++----- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs index 36749fad1c..e5abbc7246 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs @@ -30,10 +30,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor set => scrollingTestContainer.Direction = value; } - protected ManiaSelectionBlueprintTestScene() + protected ManiaSelectionBlueprintTestScene(int columns) { - var stageDefinitions = new List { new StageDefinition { Columns = 1 } }; - base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Down) + var stageDefinitions = new List { new StageDefinition { Columns = columns } }; + base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Up) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor } } }; + + AddToggleStep("Downward scroll", b => Direction = b ? ScrollingDirection.Down : ScrollingDirection.Up); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs index 3cf9c6ad65..9953b8e3c0 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs @@ -12,13 +12,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene { public TestSceneHoldNoteSelectionBlueprint() + : base(4) { - var holdNote = new HoldNote { Column = 0, Duration = 500 }; - holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + for (int i = 0; i < 4; i++) + { + var holdNote = new HoldNote + { + Column = i, + StartTime = i * 100, + Duration = 500 + }; + holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - var drawableHitObject = new DrawableHoldNote(holdNote); - Playfield.Add(drawableHitObject); - AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableHitObject); + var drawableHitObject = new DrawableHoldNote(holdNote); + Playfield.Add(drawableHitObject); + AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableHitObject); + } } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs index dc33e30de7..3586eecc44 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs @@ -12,13 +12,21 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene { public TestSceneNoteSelectionBlueprint() + : base(4) { - var note = new Note { Column = 0 }; - note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + for (int i = 0; i < 4; i++) + { + var note = new Note + { + Column = i, + StartTime = i * 200, + }; + note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - var drawableHitObject = new DrawableNote(note); - Playfield.Add(drawableHitObject); - AddBlueprint(new NoteSelectionBlueprint(note), drawableHitObject); + var drawableHitObject = new DrawableNote(note); + Playfield.Add(drawableHitObject); + AddBlueprint(new NoteSelectionBlueprint(note), drawableHitObject); + } } } } From f39dbb8b6e3dcdbc838d42a9a7a9a69bf1b0fe05 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 15 Jun 2021 21:42:09 +0900 Subject: [PATCH 339/670] Upgrade cfs --- .config/dotnet-tools.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 84673fe2ba..e72bed602e 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -27,7 +27,7 @@ ] }, "codefilesanity": { - "version": "15.0.0", + "version": "0.0.36", "commands": [ "CodeFileSanity" ] @@ -39,4 +39,4 @@ ] } } -} +} \ No newline at end of file From e29e2328f89d14f79bcf1d09fce5b7126098b90a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 15 Jun 2021 21:45:02 +0900 Subject: [PATCH 340/670] Inline inspection actions into appveyor.yml --- appveyor.yml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 845751ef07..accc913bf5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,28 +1,35 @@ clone_depth: 1 version: '{branch}-{build}' image: Visual Studio 2019 +cache: + - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml' + dotnet_csproj: patch: true file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects version: '0.0.{build}' -cache: - - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml' + before_build: - - ps: dotnet --info # Useful when version mismatch between CI and local - - ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects + - cmd: dotnet --info # Useful when version mismatch between CI and local + - cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects + build: project: osu.sln parallel: true verbosity: minimal publish_nuget: true + after_build: - - ps: dotnet tool restore + - cmd: dotnet tool restore # Temporarily disabled until the tool is upgraded to 5.0. # The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7. - # - ps: dotnet format --dry-run --check + # - cmd: dotnet format --dry-run --check + + - cmd: dotnet CodeFileSanity + - cmd: dotnet jb inspectcode "osu.Desktop.slnf" --output="temp/inspectcodereport.xml" --caches-home="temp/inspectcode" --verbosity=WARN + - cmd: dotnet nvika parsereport "temp/inspectcodereport.xml" --treatwarningsaserrors - - ps: .\InspectCode.ps1 test: assemblies: except: From 9fcf10536435688241f783ef2f39160e0141f529 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 15 Jun 2021 21:46:23 +0900 Subject: [PATCH 341/670] Remove cake --- .config/dotnet-tools.json | 6 ------ .vscode/launch.json | 14 ------------- InspectCode.ps1 | 27 -------------------------- build/Desktop.proj | 17 ---------------- build/InspectCode.cake | 41 --------------------------------------- cake.config | 5 ----- 6 files changed, 110 deletions(-) delete mode 100644 InspectCode.ps1 delete mode 100644 build/Desktop.proj delete mode 100644 build/InspectCode.cake delete mode 100644 cake.config diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index e72bed602e..1dca8b3859 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -2,12 +2,6 @@ "version": 1, "isRoot": true, "tools": { - "cake.tool": { - "version": "0.35.0", - "commands": [ - "dotnet-cake" - ] - }, "dotnet-format": { "version": "3.1.37601", "commands": [ diff --git a/.vscode/launch.json b/.vscode/launch.json index afd997f91d..1b590008cd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -113,20 +113,6 @@ "cwd": "${workspaceRoot}", "preLaunchTask": "Build benchmarks", "console": "internalConsole" - }, - { - "name": "Cake: Debug Script", - "type": "coreclr", - "request": "launch", - "program": "${workspaceRoot}/build/tools/Cake.CoreCLR/0.30.0/Cake.dll", - "args": [ - "${workspaceRoot}/build/build.cake", - "--debug", - "--verbosity=diagnostic" - ], - "cwd": "${workspaceRoot}/build", - "stopAtEntry": true, - "externalConsole": false } ] } diff --git a/InspectCode.ps1 b/InspectCode.ps1 deleted file mode 100644 index 6ed935fdbb..0000000000 --- a/InspectCode.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -[CmdletBinding()] -Param( - [string]$Target, - [string]$Configuration, - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity, - [switch]$ShowDescription, - [Alias("WhatIf", "Noop")] - [switch]$DryRun, - [Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)] - [string[]]$ScriptArgs -) - -# Build Cake arguments -$cakeArguments = ""; -if ($Target) { $cakeArguments += "-target=$Target" } -if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } -if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } -if ($ShowDescription) { $cakeArguments += "-showdescription" } -if ($DryRun) { $cakeArguments += "-dryrun" } -if ($Experimental) { $cakeArguments += "-experimental" } -$cakeArguments += $ScriptArgs - -dotnet tool restore -dotnet cake ./build/InspectCode.cake --bootstrap -dotnet cake ./build/InspectCode.cake $cakeArguments -exit $LASTEXITCODE \ No newline at end of file diff --git a/build/Desktop.proj b/build/Desktop.proj deleted file mode 100644 index b1c6b065e8..0000000000 --- a/build/Desktop.proj +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/build/InspectCode.cake b/build/InspectCode.cake deleted file mode 100644 index 6836d9071b..0000000000 --- a/build/InspectCode.cake +++ /dev/null @@ -1,41 +0,0 @@ -#addin "nuget:?package=CodeFileSanity&version=0.0.36" - -/////////////////////////////////////////////////////////////////////////////// -// ARGUMENTS -/////////////////////////////////////////////////////////////////////////////// - -var target = Argument("target", "CodeAnalysis"); -var configuration = Argument("configuration", "Release"); - -var rootDirectory = new DirectoryPath(".."); -var sln = rootDirectory.CombineWithFilePath("osu.sln"); -var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf"); - -/////////////////////////////////////////////////////////////////////////////// -// TASKS -/////////////////////////////////////////////////////////////////////////////// - -Task("InspectCode") - .Does(() => { - var inspectcodereport = "inspectcodereport.xml"; - var cacheDir = "inspectcode"; - var verbosity = AppVeyor.IsRunningOnAppVeyor ? "WARN" : "INFO"; // Don't flood CI output - - DotNetCoreTool(rootDirectory.FullPath, - "jb", $@"inspectcode ""{desktopSlnf}"" --output=""{inspectcodereport}"" --caches-home=""{cacheDir}"" --verbosity={verbosity}"); - DotNetCoreTool(rootDirectory.FullPath, "nvika", $@"parsereport ""{inspectcodereport}"" --treatwarningsaserrors"); - }); - -Task("CodeFileSanity") - .Does(() => { - ValidateCodeSanity(new ValidateCodeSanitySettings { - RootDirectory = rootDirectory.FullPath, - IsAppveyorBuild = AppVeyor.IsRunningOnAppVeyor - }); - }); - -Task("CodeAnalysis") - .IsDependentOn("CodeFileSanity") - .IsDependentOn("InspectCode"); - -RunTarget(target); \ No newline at end of file diff --git a/cake.config b/cake.config deleted file mode 100644 index 187d825591..0000000000 --- a/cake.config +++ /dev/null @@ -1,5 +0,0 @@ - -[Nuget] -Source=https://api.nuget.org/v3/index.json -UseInProcessClient=true -LoadDependencies=true From 04e8703eeea79e71790a4f0efc8c1ece02a788f0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 15 Jun 2021 21:59:18 +0900 Subject: [PATCH 342/670] Add github actions workflows --- .config/.github/workflows/ci.yml | 95 ++++++++++++++++++++++ .config/.github/workflows/report-nunit.yml | 31 +++++++ 2 files changed, 126 insertions(+) create mode 100644 .config/.github/workflows/ci.yml create mode 100644 .config/.github/workflows/report-nunit.yml diff --git a/.config/.github/workflows/ci.yml b/.config/.github/workflows/ci.yml new file mode 100644 index 0000000000..0be3f64ab3 --- /dev/null +++ b/.config/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +on: [push, pull_request] +name: Continuous Integration + +jobs: + test: + name: Test + runs-on: ${{matrix.os.fullname}} + env: + OSU_EXECUTION_MODE: ${{matrix.threadingMode}} + strategy: + fail-fast: false + matrix: + os: + - { prettyname: Windows, fullname: windows-latest } + - { prettyname: macOS, fullname: macos-latest } + - { prettyname: Linux, fullname: ubuntu-latest } + threadingMode: ['SingleThread', 'MultiThreaded'] + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install .NET 5.0.x + uses: actions/setup-dotnet@v1 + with: + dotnet-version: "5.0.x" + + # FIXME: libavformat is not included in Ubuntu. Let's fix that. + # https://github.com/ppy/osu-framework/issues/4349 + # Remove this once https://github.com/actions/virtual-environments/issues/3306 has been resolved. + - name: Install libavformat-dev + if: ${{matrix.os.fullname == 'ubuntu-latest'}} + run: | + sudo apt-get update && \ + sudo apt-get -y install libavformat-dev + + - name: Compile + run: dotnet build -c Debug -warnaserror osu.Desktop.slnf + + - name: Test + run: dotnet test $pwd/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" + shell: pwsh + + # Attempt to upload results even if test fails. + # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always + - name: Upload Test Results + uses: actions/upload-artifact@v2 + if: ${{ always() }} + with: + name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}} + path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx + + inspect-code: + name: Code Quality + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + # FIXME: Tools won't run in .NET 5.0 unless you install 3.1.x LTS side by side. + # https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e + - name: Install .NET 3.1.x LTS + uses: actions/setup-dotnet@v1 + with: + dotnet-version: "3.1.x" + + - name: Install .NET 5.0.x + uses: actions/setup-dotnet@v1 + with: + dotnet-version: "5.0.x" + + - name: Restore Tools + run: dotnet tool restore + + - name: Restore Packages + run: dotnet restore + + - name: CodeFileSanity + run: | + # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround. + # FIXME: Suppress warnings from templates project + dotnet codefilesanity | while read -r line; do + echo "::warning::$line" + done + + # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded. + # - name: .NET Format (Dry Run) + # run: dotnet format --dry-run --check + + - name: InspectCode + run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN + + - name: ReSharper + uses: glassechidna/resharper-action@master + with: + report: ${{github.workspace}}/inspectcodereport.xml diff --git a/.config/.github/workflows/report-nunit.yml b/.config/.github/workflows/report-nunit.yml new file mode 100644 index 0000000000..381d2d49c5 --- /dev/null +++ b/.config/.github/workflows/report-nunit.yml @@ -0,0 +1,31 @@ +# This is a workaround to allow PRs to report their coverage. This will run inside the base repository. +# See: +# * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories +# * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token +name: Annotate CI run with test results +on: + workflow_run: + workflows: ["Continuous Integration"] + types: + - completed +jobs: + annotate: + name: Annotate CI run with test results + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion != 'cancelled' }} + strategy: + fail-fast: false + matrix: + os: + - { prettyname: Windows } + - { prettyname: macOS } + - { prettyname: Linux } + threadingMode: ['SingleThread', 'MultiThreaded'] + steps: + - name: Annotate CI run with test results + uses: dorny/test-reporter@v1.4.2 + with: + artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}} + name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}}) + path: "*.trx" + reporter: dotnet-trx From 5283948a6d2021d208609003fab4c553f4af9ffa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 15 Jun 2021 22:05:13 +0900 Subject: [PATCH 343/670] Fix incorrect directory --- {.config/.github => .github}/workflows/ci.yml | 0 {.config/.github => .github}/workflows/report-nunit.yml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {.config/.github => .github}/workflows/ci.yml (100%) rename {.config/.github => .github}/workflows/report-nunit.yml (100%) diff --git a/.config/.github/workflows/ci.yml b/.github/workflows/ci.yml similarity index 100% rename from .config/.github/workflows/ci.yml rename to .github/workflows/ci.yml diff --git a/.config/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml similarity index 100% rename from .config/.github/workflows/report-nunit.yml rename to .github/workflows/report-nunit.yml From a85a592f70245b01f8bde3db136f913912da67c4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 15 Jun 2021 16:16:25 +0300 Subject: [PATCH 344/670] Add lookup for spinner background colour --- osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs index 4e6d3ef0e4..f7ba8b9fc4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs @@ -7,6 +7,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { SliderTrackOverride, SliderBorder, - SliderBall + SliderBall, + SpinnerBackground, } } From 52145c9237815f56c211f9e6d7780b646ee98a58 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 15 Jun 2021 16:17:05 +0300 Subject: [PATCH 345/670] Assign skinnable colour to `spinner-background` with correct default --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs index 19cb55c16e..d80e061662 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { @@ -40,6 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Anchor = Anchor.TopCentre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-background"), + Colour = source.GetConfig(OsuSkinColour.SpinnerBackground)?.Value ?? new Color4(100, 100, 100, 255), Scale = new Vector2(SPRITE_SCALE), Y = SPINNER_Y_CENTRE, }, From a4c4867d6a6b49db710b467ead9b2ec5afef0f0e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 15 Jun 2021 22:40:31 +0900 Subject: [PATCH 346/670] Add scripts for running inspections locally --- InspectCode.ps1 | 11 +++++++++++ InspectCode.sh | 6 ++++++ appveyor.yml | 10 +--------- 3 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 InspectCode.ps1 create mode 100755 InspectCode.sh diff --git a/InspectCode.ps1 b/InspectCode.ps1 new file mode 100644 index 0000000000..8316f48ff3 --- /dev/null +++ b/InspectCode.ps1 @@ -0,0 +1,11 @@ +dotnet tool restore + +# Temporarily disabled until the tool is upgraded to 5.0. + # The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7. + # - cmd: dotnet format --dry-run --check + +dotnet CodeFileSanity +dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN +dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors + +exit $LASTEXITCODE diff --git a/InspectCode.sh b/InspectCode.sh new file mode 100755 index 0000000000..cf2bc18175 --- /dev/null +++ b/InspectCode.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +dotnet tool restore +dotnet CodeFileSanity +dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN +dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors diff --git a/appveyor.yml b/appveyor.yml index accc913bf5..5be73f9875 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,15 +20,7 @@ build: publish_nuget: true after_build: - - cmd: dotnet tool restore - - # Temporarily disabled until the tool is upgraded to 5.0. - # The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7. - # - cmd: dotnet format --dry-run --check - - - cmd: dotnet CodeFileSanity - - cmd: dotnet jb inspectcode "osu.Desktop.slnf" --output="temp/inspectcodereport.xml" --caches-home="temp/inspectcode" --verbosity=WARN - - cmd: dotnet nvika parsereport "temp/inspectcodereport.xml" --treatwarningsaserrors + - ps: .\InspectCode.ps1 test: assemblies: From e79e1bbcc0c694fc179c63018a59bc1df15e3a71 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 15 Jun 2021 22:53:43 +0900 Subject: [PATCH 347/670] Fix malformed database test failing in single-threaded mode --- osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index a47631a83b..8f5ebf53bd 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -113,7 +113,6 @@ namespace osu.Game.Tests.Collections.IO await importCollectionsFromStream(osu, ms); } - Assert.That(host.UpdateThread.Running, Is.True); Assert.That(exceptionThrown, Is.False); Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(0)); } From 9d168b19c9c081af3f3daac920917e60a105e59d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 02:15:25 +0900 Subject: [PATCH 348/670] Switch to non-beta release --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9c9d2e1a82..9f6bf0d2f4 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From a549aebb3fbe9aa28b3b10dbd911c79f2e40d50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Jun 2021 22:32:26 +0200 Subject: [PATCH 349/670] Reword HD scale multiplier comment --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 00fcf5fa59..8a764a21bb 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -19,13 +19,16 @@ namespace osu.Game.Rulesets.Taiko.Mods public override string Description => @"Beats fade out before you hit them!"; public override double ScoreMultiplier => 1.06; - // In stable Taiko, hit position is 160, so playfield is essentially 160 pixels shorter - // than actual screen width. Normalized screen height is 480, so on a 4:3 screen the - // playfield ratio will actually be (640 - 160) / 480 = 1 - // For custom resolutions (x:y), screen width with normalized height becomes 480 * x / y instead, - // and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3 - // The following is 4:3 playfield ratio divided by 16:9 playfield ratio + /// + /// In stable taiko, the hit position is 160, so the active playfield is essentially 160 pixels shorter + /// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the + /// playfield ratio of the active area up to the hit position will actually be (640 - 160) / 480 = 1. + /// For custom resolutions/aspect ratios (x:y), the screen width given the normalized height becomes 480 * x / y instead, + /// and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3. + /// This constant is equal to the playfield ratio on 4:3 screens divided by the playfield ratio on 16:9 screens. + /// private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0); + private BeatmapDifficulty difficulty; private ControlPointInfo controlPointInfo; From 259e6cad4dd5e14111765d89b7030d834d2dae82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Jun 2021 22:33:27 +0200 Subject: [PATCH 350/670] Rearrange and rename member --- .../Mods/TaikoModHidden.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 8a764a21bb..fd076b8765 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -19,6 +19,16 @@ namespace osu.Game.Rulesets.Taiko.Mods public override string Description => @"Beats fade out before you hit them!"; public override double ScoreMultiplier => 1.06; + [SettingSource("Fade-out Time", "The bigger this multiplier is, the sooner the notes will start fading out")] + public BindableNumber FadeOutTimeMultiplier { get; } = new BindableDouble + { + MinValue = 0.5, + MaxValue = 1.5, + Default = 1.0, + Value = 1.0, + Precision = 0.01, + }; + /// /// In stable taiko, the hit position is 160, so the active playfield is essentially 160 pixels shorter /// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the @@ -32,16 +42,6 @@ namespace osu.Game.Rulesets.Taiko.Mods private BeatmapDifficulty difficulty; private ControlPointInfo controlPointInfo; - [SettingSource("Fade-out Time", "The bigger this multiplier is, the sooner the notes will start fading out")] - public BindableNumber VisibilityMod { get; } = new BindableDouble - { - MinValue = 0.5, - MaxValue = 1.5, - Default = 1.0, - Value = 1.0, - Precision = 0.01, - }; - protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { ApplyNormalVisibilityState(hitObject, state); @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Mods } // I *think* it's like this because stable's default velocity multiplier is 1.4 - var preempt = 14000 / MultiplierAt(hitObject.HitObject.StartTime) * VisibilityMod.Value; + var preempt = 14000 / MultiplierAt(hitObject.HitObject.StartTime) * FadeOutTimeMultiplier.Value; var start = hitObject.HitObject.StartTime - preempt * 0.6; var duration = preempt * 0.3; From b0549187df1339998fbf48267e74ae81c6233b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Jun 2021 22:57:20 +0200 Subject: [PATCH 351/670] Apply pre-empt formula which is closer to stable --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index fd076b8765..613c16baa2 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Mods /// private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0); - private BeatmapDifficulty difficulty; + private double originalSliderMultiplier; private ControlPointInfo controlPointInfo; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { var beatLength = controlPointInfo.TimingPointAt(position)?.BeatLength; var speedMultiplier = controlPointInfo.DifficultyPointAt(position)?.SpeedMultiplier; - return difficulty.SliderMultiplier * (speedMultiplier ?? 1.0) * TimingControlPoint.DEFAULT_BEAT_LENGTH / (beatLength ?? TimingControlPoint.DEFAULT_BEAT_LENGTH); + return originalSliderMultiplier * (speedMultiplier ?? 1.0) * TimingControlPoint.DEFAULT_BEAT_LENGTH / (beatLength ?? TimingControlPoint.DEFAULT_BEAT_LENGTH); } protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) @@ -68,8 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Mods return; } - // I *think* it's like this because stable's default velocity multiplier is 1.4 - var preempt = 14000 / MultiplierAt(hitObject.HitObject.StartTime) * FadeOutTimeMultiplier.Value; + var preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime) * FadeOutTimeMultiplier.Value; var start = hitObject.HitObject.StartTime - preempt * 0.6; var duration = preempt * 0.3; @@ -91,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public void ApplyToDifficulty(BeatmapDifficulty difficulty) { - this.difficulty = difficulty; + originalSliderMultiplier = difficulty.SliderMultiplier; difficulty.SliderMultiplier /= hd_sv_scale; } From 57f0c47dedb97d9fdce434ced9f1f8942baafd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Jun 2021 23:00:09 +0200 Subject: [PATCH 352/670] Ezplain slider multiplier adjustment --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 613c16baa2..5b7dca09a5 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -91,6 +91,11 @@ namespace osu.Game.Rulesets.Taiko.Mods public void ApplyToDifficulty(BeatmapDifficulty difficulty) { originalSliderMultiplier = difficulty.SliderMultiplier; + + // the hidden mod on stable had an added playfield cover that essentially forced a 4:3 playfield ratio, by cutting off all objects past that size. + // lazer currently uses a playfield adjustment container which keeps a 16:9 ratio. + // therefore, increase the slider multiplier proportionally so that the notes stay on the screen for the same amount of time as on stable. + // note that this will means that the notes will scroll faster as they have a longer distance to travel on the screen in that same amount of time. difficulty.SliderMultiplier /= hd_sv_scale; } From 30703d518c055e3f6e5656f5bdd23952dbbc0c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Jun 2021 23:19:33 +0200 Subject: [PATCH 353/670] Add failing assert for seasonal background equality --- .../Visual/Background/TestSceneSeasonalBackgroundLoader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs index dc5a4f4a3e..0bd1263076 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs @@ -161,15 +161,18 @@ namespace osu.Game.Tests.Visual.Background private void loadNextBackground() { + SeasonalBackground previousBackground = null; SeasonalBackground background = null; AddStep("create next background", () => { + previousBackground = (SeasonalBackground)backgroundContainer.SingleOrDefault(); background = backgroundLoader.LoadNextBackground(); LoadComponentAsync(background, bg => backgroundContainer.Child = bg); }); AddUntilStep("background loaded", () => background.IsLoaded); + AddAssert("background is different", () => !background.Equals(previousBackground)); } private void assertAnyBackground() From 022b1a28d5d491bad38676eb961ad12f491c74bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Jun 2021 23:21:48 +0200 Subject: [PATCH 354/670] Add missing equality implementation for seasonal backgrounds The equality operator is used to determine whether the next background in the cycle should be loaded, to avoid pointless loads of the same background several times (see #13362 and #13393). Its omission in the latter pull caused seasonal backgrounds to no longer cycle. Closes #13508. --- .../Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index a48da37804..f01a26a3a8 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -99,5 +99,14 @@ namespace osu.Game.Graphics.Backgrounds // ensure we're not loading in without a transition. this.FadeInFromZero(200, Easing.InOutSine); } + + public override bool Equals(Background other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return other.GetType() == GetType() + && ((SeasonalBackground)other).url == url; + } } } From 8c558610abe06b6ed6f23eb91bd83f16f390fc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Jun 2021 00:34:39 +0200 Subject: [PATCH 355/670] Fix hitobjects expiring before fully judged with hidden --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 5b7dca09a5..15fc8c130e 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.Mods // DrawableHitObject sets LifetimeEnd to LatestTransformEndTime if it isn't manually changed. // in order for the object to not be killed before its actual end time (as the latest transform ends earlier), set lifetime end explicitly. - hitObject.LifetimeEnd = state == ArmedState.Idle + hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged ? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) : hitObject.HitStateUpdateTime; } From 6be41e497a2a9556ac4fc8334e9a5bc90fe4193a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 11:25:00 +0900 Subject: [PATCH 356/670] Fix possible nullref in difficulty recommender --- osu.Game/Beatmaps/DifficultyRecommender.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index 340c47d89b..ca910e70b8 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -101,10 +101,20 @@ namespace osu.Game.Beatmaps /// Rulesets ordered descending by their respective recommended difficulties. /// The currently selected ruleset will always be first. /// - private IEnumerable orderedRulesets => - recommendedDifficultyMapping - .OrderByDescending(pair => pair.Value).Select(pair => pair.Key).Where(r => !r.Equals(ruleset.Value)) - .Prepend(ruleset.Value); + private IEnumerable orderedRulesets + { + get + { + if (LoadState < LoadState.Ready || ruleset.Value == null) + return Enumerable.Empty(); + + return recommendedDifficultyMapping + .OrderByDescending(pair => pair.Value) + .Select(pair => pair.Key) + .Where(r => !r.Equals(ruleset.Value)) + .Prepend(ruleset.Value); + } + } private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { From a5261f0cb3c7209bfc36ba157c02c80020ec18fa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 11:48:41 +0900 Subject: [PATCH 357/670] Add difficulty recommender instantly --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 019d3b3cd0..7f23dfc7f8 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -712,7 +712,6 @@ namespace osu.Game PostNotification = n => notifications.Post(n), }, Add, true); - loadComponentSingleFile(difficultyRecommender, Add); loadComponentSingleFile(stableImportManager, Add); loadComponentSingleFile(screenshotManager, Add); @@ -755,6 +754,7 @@ namespace osu.Game chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; + Add(difficultyRecommender); Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); From 451ce04d19126a4c38c096e90fa97a94231e81dd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 13:22:46 +0900 Subject: [PATCH 358/670] Make Resharper inspections fail CI job As per https://github.com/ppy/osu-framework/pull/4514. --- .config/dotnet-tools.json | 4 ++-- .github/workflows/ci.yml | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 1dca8b3859..b3f7c67c51 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -14,8 +14,8 @@ "jb" ] }, - "nvika": { - "version": "2.0.0", + "smoogipoo.nvika": { + "version": "1.0.1", "commands": [ "nvika" ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0be3f64ab3..ed3e99cb61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,5 @@ jobs: - name: InspectCode run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN - - name: ReSharper - uses: glassechidna/resharper-action@master - with: - report: ${{github.workspace}}/inspectcodereport.xml + - name: NVika + run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors From fa00d07107a9079af55ef6d2ea4a9a3c16c7e29e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 13:26:36 +0900 Subject: [PATCH 359/670] Upgrade osu-resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index c020b1d783..490e43b5e6 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index a7bd5f2e9f..8eeaad1127 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 5b3efb4ba4..db442238ce 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 4c5268694efca63bed526a5f0a2b57ecef4a3699 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 13:46:13 +0900 Subject: [PATCH 360/670] Localise some of the BeatmapListingOverlay --- .../BeatmapListingSearchControl.cs | 17 +++++----- .../BeatmapListing/BeatmapSearchFilterRow.cs | 6 ++-- ...BeatmapSearchMultipleSelectionFilterRow.cs | 5 +-- .../BeatmapSearchRulesetFilterRow.cs | 3 +- .../BeatmapSearchScoreFilterRow.cs | 32 +++++++++++++++---- .../Overlays/BeatmapListing/FilterTabItem.cs | 3 +- osu.Game/Overlays/BeatmapListingOverlay.cs | 3 +- 7 files changed, 47 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 97ccb66599..0626f236b8 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; using osuTK.Graphics; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -126,15 +127,15 @@ namespace osu.Game.Overlays.BeatmapListing Padding = new MarginPadding { Horizontal = 10 }, Children = new Drawable[] { - generalFilter = new BeatmapSearchMultipleSelectionFilterRow(@"General"), + generalFilter = new BeatmapSearchMultipleSelectionFilterRow(BeatmapsStrings.ListingSearchFiltersGeneral), modeFilter = new BeatmapSearchRulesetFilterRow(), - categoryFilter = new BeatmapSearchFilterRow(@"Categories"), - genreFilter = new BeatmapSearchFilterRow(@"Genre"), - languageFilter = new BeatmapSearchFilterRow(@"Language"), - extraFilter = new BeatmapSearchMultipleSelectionFilterRow(@"Extra"), + categoryFilter = new BeatmapSearchFilterRow(BeatmapsStrings.ListingSearchFiltersStatus), + genreFilter = new BeatmapSearchFilterRow(BeatmapsStrings.ListingSearchFiltersGenre), + languageFilter = new BeatmapSearchFilterRow(BeatmapsStrings.ListingSearchFiltersLanguage), + extraFilter = new BeatmapSearchMultipleSelectionFilterRow(BeatmapsStrings.ListingSearchFiltersExtra), ranksFilter = new BeatmapSearchScoreFilterRow(), - playedFilter = new BeatmapSearchFilterRow(@"Played"), - explicitContentFilter = new BeatmapSearchFilterRow(@"Explicit Content"), + playedFilter = new BeatmapSearchFilterRow(BeatmapsStrings.ListingSearchFiltersPlayed), + explicitContentFilter = new BeatmapSearchFilterRow(BeatmapsStrings.ListingSearchFiltersNsfw), } } } @@ -172,7 +173,7 @@ namespace osu.Game.Overlays.BeatmapListing public BeatmapSearchTextBox() { - PlaceholderText = @"type in keywords..."; + PlaceholderText = BeatmapsStrings.ListingSearchPrompt; } protected override bool OnKeyDown(KeyDownEvent e) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index 01bcbd3244..4c831543fe 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -11,8 +11,8 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osuTK; -using Humanizer; using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Localisation; namespace osu.Game.Overlays.BeatmapListing { @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.BeatmapListing set => current.Current = value; } - public BeatmapSearchFilterRow(string headerName) + public BeatmapSearchFilterRow(LocalisableString header) { Drawable filter; AutoSizeAxes = Axes.Y; @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapListing Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Font = OsuFont.GetFont(size: 13), - Text = headerName.Titleize() + Text = header }, filter = CreateFilter() } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 5dfa8e6109..e0632ace58 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osuTK; namespace osu.Game.Overlays.BeatmapListing @@ -19,8 +20,8 @@ namespace osu.Game.Overlays.BeatmapListing private MultipleSelectionFilter filter; - public BeatmapSearchMultipleSelectionFilterRow(string headerName) - : base(headerName) + public BeatmapSearchMultipleSelectionFilterRow(LocalisableString header) + : base(header) { Current.BindTo(filter.Current); } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs index a8dc088e52..c2d0eea80c 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; namespace osu.Game.Overlays.BeatmapListing @@ -10,7 +11,7 @@ namespace osu.Game.Overlays.BeatmapListing public class BeatmapSearchRulesetFilterRow : BeatmapSearchFilterRow { public BeatmapSearchRulesetFilterRow() - : base(@"Mode") + : base(BeatmapsStrings.ListingSearchFiltersMode) { } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs index 804962adfb..abfffe907f 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs @@ -1,9 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; namespace osu.Game.Overlays.BeatmapListing @@ -11,7 +13,7 @@ namespace osu.Game.Overlays.BeatmapListing public class BeatmapSearchScoreFilterRow : BeatmapSearchMultipleSelectionFilterRow { public BeatmapSearchScoreFilterRow() - : base(@"Rank Achieved") + : base(BeatmapsStrings.ListingSearchFiltersRank) { } @@ -31,18 +33,36 @@ namespace osu.Game.Overlays.BeatmapListing { } - protected override string LabelFor(ScoreRank value) + protected override LocalisableString LabelFor(ScoreRank value) { switch (value) { case ScoreRank.XH: - return @"Silver SS"; + return BeatmapsStrings.RankXH; + + case ScoreRank.X: + return BeatmapsStrings.RankX; case ScoreRank.SH: - return @"Silver S"; + return BeatmapsStrings.RankSH; + + case ScoreRank.S: + return BeatmapsStrings.RankS; + + case ScoreRank.A: + return BeatmapsStrings.RankA; + + case ScoreRank.B: + return BeatmapsStrings.RankB; + + case ScoreRank.C: + return BeatmapsStrings.RankC; + + case ScoreRank.D: + return BeatmapsStrings.RankD; default: - return value.GetDescription(); + throw new ArgumentException("Unsupported value.", nameof(value)); } } } diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index f02b515755..d64ee59682 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -66,7 +67,7 @@ namespace osu.Game.Overlays.BeatmapListing /// /// Returns the label text to be used for the supplied . /// - protected virtual string LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); + protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); private void updateState() { diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 5df7a4650e..5e65cd9488 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -18,6 +18,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing.Panels; +using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -232,7 +233,7 @@ namespace osu.Game.Overlays { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = @"... nope, nothing found.", + Text = BeatmapsStrings.ListingSearchNotFoundQuote, } } }); From 73e443a0d9c5bc67192fef049d33dc8f477decb5 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 14:01:12 +0900 Subject: [PATCH 361/670] Add comments --- osu.Game.Rulesets.Catch/UI/CatcherTrail.cs | 6 ++++++ osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs index 90fb59db9a..f273ef23ac 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs @@ -7,6 +7,12 @@ using osuTK; namespace osu.Game.Rulesets.Catch.UI { + /// + /// A trail of the catcher. + /// It also represents a hyper dash afterimage. + /// + // TODO: Trails shouldn't be animated when the skin has an animated catcher. + // The animation should be frozen at the animation frame at the time of the trail generation. public class CatcherTrail : PoolableDrawable { public CatcherAnimationState AnimationState diff --git a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs index 5bd97858b2..1038af7a48 100644 --- a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs +++ b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs @@ -10,6 +10,10 @@ using osuTK; namespace osu.Game.Rulesets.Catch.UI { + /// + /// The visual representation of the . + /// It includes the body part of the catcher and the catcher plate. + /// public class SkinnableCatcher : SkinnableDrawable { [Cached] From 2ce487bdacf8dc105d12a63f6613535585bb4823 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 14:24:07 +0900 Subject: [PATCH 362/670] Rename mod and fix easing mappings / naming --- .../Mods/OsuModApproachCircle.cs | 73 ------------ .../Mods/OsuModDifferentApproach.cs | 110 ++++++++++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- 3 files changed, 111 insertions(+), 74 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModDifferentApproach.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs deleted file mode 100644 index 8e0175b5a3..0000000000 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachCircle.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Game.Configuration; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables; - -namespace osu.Game.Rulesets.Osu.Mods -{ - internal class OsuModApproachCircle : Mod, IApplicableToDrawableHitObjects - { - public override string Name => "Approach Circle"; - public override string Acronym => "AC"; - public override string Description => "Never trust the approach circles..."; - public override double ScoreMultiplier => 1; - public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; - - [SettingSource("Easing", "Change the easing type of the approach circles.", 0)] - public Bindable BindableEasing { get; } = new Bindable(); - - [SettingSource("Scale the size", "Change the factor of the approach circle scale.", 1)] - public BindableFloat Scale { get; } = new BindableFloat - { - Precision = 0.1f, - MinValue = 2, - MaxValue = 10, - Default = 4, - Value = 4 - }; - - public void ApplyToDrawableHitObjects(IEnumerable drawables) - { - drawables.ForEach(drawable => - { - drawable.ApplyCustomUpdateState += (drawableHitObj, state) => - { - if (!(drawableHitObj is DrawableHitCircle hitCircle)) return; - - var obj = hitCircle.HitObject; - - hitCircle.BeginAbsoluteSequence(obj.StartTime - obj.TimePreempt, true); - hitCircle.ApproachCircle.ScaleTo(Scale.Value); - - hitCircle.ApproachCircle.FadeIn(Math.Min(obj.TimeFadeIn, obj.TimePreempt)); - - hitCircle.ApproachCircle.ScaleTo(1f, obj.TimePreempt, (Easing)BindableEasing.Value); - - hitCircle.ApproachCircle.Expire(true); - }; - }); - } - - internal enum ReadableEasing - { - Accelerate = 2, - Accelerate2 = 6, - Accelerate3 = 12, - AccelerateInAfterDeceleraingeOut = 29, - CasualBounces = 32, - CasualBouncesWhileAccelerating = 24, - Decelerate = 1, - DecelerateAfterAccelerating = 8, - Default = 0, - } - } -} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifferentApproach.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifferentApproach.cs new file mode 100644 index 0000000000..db612b6269 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifferentApproach.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Mods +{ + internal class OsuModDifferentApproach : Mod, IApplicableToDrawableHitObjects + { + public override string Name => "Approach Different"; + public override string Acronym => "AD"; + public override string Description => "Never trust the approach circles..."; + public override double ScoreMultiplier => 1; + public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; + + [SettingSource("Easing", "Change the animation curve of the approach circles.", 0)] + public Bindable BindableEasing { get; } = new Bindable(); + + [SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 1)] + public BindableFloat Scale { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 2, + MaxValue = 10, + Default = 4, + Value = 4 + }; + + public void ApplyToDrawableHitObjects(IEnumerable drawables) + { + drawables.ForEach(drawable => + { + drawable.ApplyCustomUpdateState += (drawableHitObj, state) => + { + if (!(drawableHitObj is DrawableHitCircle hitCircle)) return; + + var obj = hitCircle.HitObject; + + hitCircle.BeginAbsoluteSequence(obj.StartTime - obj.TimePreempt, true); + hitCircle.ApproachCircle.ScaleTo(Scale.Value); + + hitCircle.ApproachCircle.FadeIn(Math.Min(obj.TimeFadeIn, obj.TimePreempt)); + + hitCircle.ApproachCircle.ScaleTo(1f, obj.TimePreempt, getEasing(BindableEasing.Value)); + + hitCircle.ApproachCircle.Expire(true); + }; + }); + } + + private Easing getEasing(ApproachCircleEasing approachEasing) + { + switch (approachEasing) + { + default: + return Easing.None; + + case ApproachCircleEasing.Accelerate1: + return Easing.In; + + case ApproachCircleEasing.Accelerate2: + return Easing.InCubic; + + case ApproachCircleEasing.Accelerate3: + return Easing.InQuint; + + case ApproachCircleEasing.Gravity: + return Easing.InBack; + + case ApproachCircleEasing.Decelerate1: + return Easing.Out; + + case ApproachCircleEasing.Decelerate2: + return Easing.OutCubic; + + case ApproachCircleEasing.Decelerate3: + return Easing.OutQuint; + + case ApproachCircleEasing.InOut1: + return Easing.InOutCubic; + + case ApproachCircleEasing.InOut2: + return Easing.InOutQuint; + } + } + + public enum ApproachCircleEasing + { + Default, + Accelerate1, + Accelerate2, + Accelerate3, + Gravity, + Decelerate1, + Decelerate2, + Decelerate3, + InOut1, + InOut2, + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 0f7ee6ade2..217803ee7b 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new ModWindUp(), new ModWindDown()), new OsuModTraceable(), new OsuModBarrelRoll(), - new OsuModApproachCircle(), + new OsuModDifferentApproach(), }; case ModType.System: From b1dd502e062298c00022cefbf7bbe107d58ad032 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 15:09:42 +0900 Subject: [PATCH 363/670] Rename class to match new name --- .../{OsuModDifferentApproach.cs => OsuModApproachDifferent.cs} | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Rulesets.Osu/Mods/{OsuModDifferentApproach.cs => OsuModApproachDifferent.cs} (97%) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifferentApproach.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs similarity index 97% rename from osu.Game.Rulesets.Osu/Mods/OsuModDifferentApproach.cs rename to osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index db612b6269..87358112b3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifferentApproach.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModDifferentApproach : Mod, IApplicableToDrawableHitObjects + public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObjects { public override string Name => "Approach Different"; public override string Acronym => "AD"; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 217803ee7b..1b9bcd19fd 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new ModWindUp(), new ModWindDown()), new OsuModTraceable(), new OsuModBarrelRoll(), - new OsuModDifferentApproach(), + new OsuModApproachDifferent(), }; case ModType.System: From f6f1a068b252bc2e72b364a82f6231dbdbd73912 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 15:15:12 +0900 Subject: [PATCH 364/670] Rename "easing" references to be "style" instead --- .../Mods/OsuModApproachDifferent.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index 87358112b3..cd718a7b18 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs @@ -22,8 +22,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; - [SettingSource("Easing", "Change the animation curve of the approach circles.", 0)] - public Bindable BindableEasing { get; } = new Bindable(); + [SettingSource("Style", "Change the animation style of the approach circles.", 0)] + public Bindable Style { get; } = new Bindable(); [SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 1)] public BindableFloat Scale { get; } = new BindableFloat @@ -50,50 +50,50 @@ namespace osu.Game.Rulesets.Osu.Mods hitCircle.ApproachCircle.FadeIn(Math.Min(obj.TimeFadeIn, obj.TimePreempt)); - hitCircle.ApproachCircle.ScaleTo(1f, obj.TimePreempt, getEasing(BindableEasing.Value)); + hitCircle.ApproachCircle.ScaleTo(1f, obj.TimePreempt, getEasing(Style.Value)); hitCircle.ApproachCircle.Expire(true); }; }); } - private Easing getEasing(ApproachCircleEasing approachEasing) + private Easing getEasing(AnimationStyle approachEasing) { switch (approachEasing) { default: return Easing.None; - case ApproachCircleEasing.Accelerate1: + case AnimationStyle.Accelerate1: return Easing.In; - case ApproachCircleEasing.Accelerate2: + case AnimationStyle.Accelerate2: return Easing.InCubic; - case ApproachCircleEasing.Accelerate3: + case AnimationStyle.Accelerate3: return Easing.InQuint; - case ApproachCircleEasing.Gravity: + case AnimationStyle.Gravity: return Easing.InBack; - case ApproachCircleEasing.Decelerate1: + case AnimationStyle.Decelerate1: return Easing.Out; - case ApproachCircleEasing.Decelerate2: + case AnimationStyle.Decelerate2: return Easing.OutCubic; - case ApproachCircleEasing.Decelerate3: + case AnimationStyle.Decelerate3: return Easing.OutQuint; - case ApproachCircleEasing.InOut1: + case AnimationStyle.InOut1: return Easing.InOutCubic; - case ApproachCircleEasing.InOut2: + case AnimationStyle.InOut2: return Easing.InOutQuint; } } - public enum ApproachCircleEasing + public enum AnimationStyle { Default, Accelerate1, From 3c3ff8be0d0594a029850678f823c471d40a6890 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 15:58:07 +0900 Subject: [PATCH 365/670] Localise beatmap listing enum values --- .../Graphics/UserInterface/OsuTabControl.cs | 2 +- .../Graphics/UserInterface/PageTabControl.cs | 3 +- .../Overlays/BeatmapListing/FilterTabItem.cs | 2 +- .../Overlays/BeatmapListing/SearchCategory.cs | 43 +++++++++++++ .../Overlays/BeatmapListing/SearchExplicit.cs | 23 +++++++ .../Overlays/BeatmapListing/SearchExtra.cs | 22 +++++++ .../Overlays/BeatmapListing/SearchGeneral.cs | 25 ++++++++ .../Overlays/BeatmapListing/SearchGenre.cs | 58 ++++++++++++++++++ .../Overlays/BeatmapListing/SearchLanguage.cs | 61 +++++++++++++++++++ .../Overlays/BeatmapListing/SearchPlayed.cs | 34 +++++++++-- .../Overlays/BeatmapListing/SortCriteria.cs | 41 +++++++++++++ osu.Game/Overlays/OverlaySortTabControl.cs | 2 +- osu.Game/Overlays/TabControlOverlayHeader.cs | 14 ++++- 13 files changed, 321 insertions(+), 9 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index dbcce9a84a..0c220336a5 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -160,7 +160,7 @@ namespace osu.Game.Graphics.UserInterface Margin = new MarginPadding { Top = 5, Bottom = 5 }, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetDescription() ?? value.ToString(), + Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetLocalisableDescription() ?? value.ToString(), Font = OsuFont.GetFont(size: 14) }, Bar = new Box diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index d05a08108a..1ba9ad53bb 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.UserInterface @@ -81,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } - protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString(); + protected virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString(); protected override bool OnHover(HoverEvent e) { diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index d64ee59682..46cb1e822f 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.BeatmapListing /// /// Returns the label text to be used for the supplied . /// - protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); + protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString(); private void updateState() { diff --git a/osu.Game/Overlays/BeatmapListing/SearchCategory.cs b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs index 84859bf5b5..8a9df76af3 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchCategory.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs @@ -1,10 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.BeatmapListing { + [LocalisableEnum(typeof(SearchCategoryEnumLocalisationMapper))] public enum SearchCategory { Any, @@ -23,4 +27,43 @@ namespace osu.Game.Overlays.BeatmapListing [Description("My Maps")] Mine, } + + public class SearchCategoryEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(SearchCategory value) + { + switch (value) + { + case SearchCategory.Any: + return BeatmapsStrings.StatusAny; + + case SearchCategory.Leaderboard: + return BeatmapsStrings.StatusLeaderboard; + + case SearchCategory.Ranked: + return BeatmapsStrings.StatusRanked; + + case SearchCategory.Qualified: + return BeatmapsStrings.StatusQualified; + + case SearchCategory.Loved: + return BeatmapsStrings.StatusLoved; + + case SearchCategory.Favourites: + return BeatmapsStrings.StatusFavourites; + + case SearchCategory.Pending: + return BeatmapsStrings.StatusPending; + + case SearchCategory.Graveyard: + return BeatmapsStrings.StatusGraveyard; + + case SearchCategory.Mine: + return BeatmapsStrings.StatusMine; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs b/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs index 3e57cdd48c..78e6a4e094 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs @@ -1,11 +1,34 @@ // 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.Localisation; +using osu.Game.Resources.Localisation.Web; + namespace osu.Game.Overlays.BeatmapListing { + [LocalisableEnum(typeof(SearchExplicitEnumLocalisationMapper))] public enum SearchExplicit { Hide, Show } + + public class SearchExplicitEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(SearchExplicit value) + { + switch (value) + { + case SearchExplicit.Hide: + return BeatmapsStrings.NsfwExclude; + + case SearchExplicit.Show: + return BeatmapsStrings.NsfwInclude; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs index af37e3264f..4b3fb6e833 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs @@ -1,10 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.BeatmapListing { + [LocalisableEnum(typeof(SearchExtraEnumLocalisationMapper))] public enum SearchExtra { [Description("Has Video")] @@ -13,4 +17,22 @@ namespace osu.Game.Overlays.BeatmapListing [Description("Has Storyboard")] Storyboard } + + public class SearchExtraEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(SearchExtra value) + { + switch (value) + { + case SearchExtra.Video: + return BeatmapsStrings.ExtraVideo; + + case SearchExtra.Storyboard: + return BeatmapsStrings.ExtraStoryboard; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs index 175942c626..b4c629f7fa 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs @@ -1,10 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.BeatmapListing { + [LocalisableEnum(typeof(SearchGeneralEnumLocalisationMapper))] public enum SearchGeneral { [Description("Recommended difficulty")] @@ -16,4 +20,25 @@ namespace osu.Game.Overlays.BeatmapListing [Description("Subscribed mappers")] Follows } + + public class SearchGeneralEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(SearchGeneral value) + { + switch (value) + { + case SearchGeneral.Recommended: + return BeatmapsStrings.GeneralRecommended; + + case SearchGeneral.Converts: + return BeatmapsStrings.GeneralConverts; + + case SearchGeneral.Follows: + return BeatmapsStrings.GeneralFollows; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs index de437fac3e..b2709ecd2e 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs @@ -1,10 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.BeatmapListing { + [LocalisableEnum(typeof(SearchGenreEnumLocalisationMapper))] public enum SearchGenre { Any = 0, @@ -26,4 +30,58 @@ namespace osu.Game.Overlays.BeatmapListing Folk = 13, Jazz = 14 } + + public class SearchGenreEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(SearchGenre value) + { + switch (value) + { + case SearchGenre.Any: + return BeatmapsStrings.GenreAny; + + case SearchGenre.Unspecified: + return BeatmapsStrings.GenreUnspecified; + + case SearchGenre.VideoGame: + return BeatmapsStrings.GenreVideoGame; + + case SearchGenre.Anime: + return BeatmapsStrings.GenreAnime; + + case SearchGenre.Rock: + return BeatmapsStrings.GenreRock; + + case SearchGenre.Pop: + return BeatmapsStrings.GenrePop; + + case SearchGenre.Other: + return BeatmapsStrings.GenreOther; + + case SearchGenre.Novelty: + return BeatmapsStrings.GenreNovelty; + + case SearchGenre.HipHop: + return BeatmapsStrings.GenreHipHop; + + case SearchGenre.Electronic: + return BeatmapsStrings.GenreElectronic; + + case SearchGenre.Metal: + return BeatmapsStrings.GenreMetal; + + case SearchGenre.Classical: + return BeatmapsStrings.GenreClassical; + + case SearchGenre.Folk: + return BeatmapsStrings.GenreFolk; + + case SearchGenre.Jazz: + return BeatmapsStrings.GenreJazz; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs index 015cee8ce3..352383d576 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs @@ -1,10 +1,14 @@ // 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.Localisation; using osu.Framework.Utils; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.BeatmapListing { + [LocalisableEnum(typeof(SearchLanguageEnumLocalisationMapper))] [HasOrderedElements] public enum SearchLanguage { @@ -53,4 +57,61 @@ namespace osu.Game.Overlays.BeatmapListing [Order(13)] Other } + + public class SearchLanguageEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(SearchLanguage value) + { + switch (value) + { + case SearchLanguage.Any: + return BeatmapsStrings.LanguageAny; + + case SearchLanguage.Unspecified: + return BeatmapsStrings.LanguageUnspecified; + + case SearchLanguage.English: + return BeatmapsStrings.LanguageEnglish; + + case SearchLanguage.Japanese: + return BeatmapsStrings.LanguageJapanese; + + case SearchLanguage.Chinese: + return BeatmapsStrings.LanguageChinese; + + case SearchLanguage.Instrumental: + return BeatmapsStrings.LanguageInstrumental; + + case SearchLanguage.Korean: + return BeatmapsStrings.LanguageKorean; + + case SearchLanguage.French: + return BeatmapsStrings.LanguageFrench; + + case SearchLanguage.German: + return BeatmapsStrings.LanguageGerman; + + case SearchLanguage.Swedish: + return BeatmapsStrings.LanguageSwedish; + + case SearchLanguage.Spanish: + return BeatmapsStrings.LanguageSpanish; + + case SearchLanguage.Italian: + return BeatmapsStrings.LanguageItalian; + + case SearchLanguage.Russian: + return BeatmapsStrings.LanguageRussian; + + case SearchLanguage.Polish: + return BeatmapsStrings.LanguagePolish; + + case SearchLanguage.Other: + return BeatmapsStrings.GenreOther; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs index eb7fb46158..93c0644d45 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs @@ -1,12 +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; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; + namespace osu.Game.Overlays.BeatmapListing { - public enum SearchPlayed +[LocalisableEnum(typeof(SearchPlayedEnumLocalisationMapper))] +public enum SearchPlayed +{ + Any, + Played, + Unplayed +} + +public class SearchPlayedEnumLocalisationMapper : EnumLocalisationMapper +{ + public override LocalisableString Map(SearchPlayed value) { - Any, - Played, - Unplayed + switch (value) + { + case SearchPlayed.Any: + return BeatmapsStrings.PlayedAny; + + case SearchPlayed.Played: + return BeatmapsStrings.PlayedPlayed; + + case SearchPlayed.Unplayed: + return BeatmapsStrings.PlayedUnplayed; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } } } +} diff --git a/osu.Game/Overlays/BeatmapListing/SortCriteria.cs b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs index e409cbdda7..5ea885eecc 100644 --- a/osu.Game/Overlays/BeatmapListing/SortCriteria.cs +++ b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs @@ -1,8 +1,13 @@ // 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.Localisation; +using osu.Game.Resources.Localisation.Web; + namespace osu.Game.Overlays.BeatmapListing { + [LocalisableEnum(typeof(SortCriteriaLocalisationMapper))] public enum SortCriteria { Title, @@ -14,4 +19,40 @@ namespace osu.Game.Overlays.BeatmapListing Favourites, Relevance } + + public class SortCriteriaLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(SortCriteria value) + { + switch (value) + { + case SortCriteria.Title: + return BeatmapsStrings.ListingSearchSortingTitle; + + case SortCriteria.Artist: + return BeatmapsStrings.ListingSearchSortingArtist; + + case SortCriteria.Difficulty: + return BeatmapsStrings.ListingSearchSortingDifficulty; + + case SortCriteria.Ranked: + return BeatmapsStrings.ListingSearchSortingRanked; + + case SortCriteria.Rating: + return BeatmapsStrings.ListingSearchSortingRating; + + case SortCriteria.Plays: + return BeatmapsStrings.ListingSearchSortingPlays; + + case SortCriteria.Favourites: + return BeatmapsStrings.ListingSearchSortingFavourites; + + case SortCriteria.Relevance: + return BeatmapsStrings.ListingSearchSortingRelevance; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs index 0ebabd424f..5ece3e4019 100644 --- a/osu.Game/Overlays/OverlaySortTabControl.cs +++ b/osu.Game/Overlays/OverlaySortTabControl.cs @@ -143,7 +143,7 @@ namespace osu.Game.Overlays Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = (value as Enum)?.GetDescription() ?? value.ToString() + Text = (value as Enum)?.GetLocalisableDescription() ?? value.ToString() } } }); diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index 7798dfa576..e6f7e250a7 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -106,7 +106,19 @@ namespace osu.Game.Overlays public OverlayHeaderTabItem(T value) : base(value) { - Text.Text = ((Value as Enum)?.GetDescription() ?? Value.ToString()).ToLower(); + if (!(Value is Enum enumValue)) + Text.Text = Value.ToString().ToLower(); + else + { + var localisableDescription = enumValue.GetLocalisableDescription(); + var nonLocalisableDescription = enumValue.GetDescription(); + + // If localisable == non-localisable, then we must have a basic string, so .ToLower() is used. + Text.Text = localisableDescription.Equals(nonLocalisableDescription) + ? nonLocalisableDescription.ToLower() + : localisableDescription; + } + Text.Font = OsuFont.GetFont(size: 14); Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation Bar.Margin = new MarginPadding { Bottom = bar_height }; From 117e94bc94c2094d403cb0518f6fb5ecb2758b64 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 15:58:17 +0900 Subject: [PATCH 366/670] Allow setting `Entry` of `PoolableDrawableWithLifetime` It is more convenient than using the constructor because the only limited kind of expression is allowed in a base constructor call. Also, the object initializer syntax can be used. --- .../Objects/Drawables/DrawableHitObject.cs | 7 ++-- .../Pooling/PoolableDrawableWithLifetime.cs | 35 ++++++++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 7fc35fc778..a0717ec38e 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -156,10 +156,11 @@ namespace osu.Game.Rulesets.Objects.Drawables /// If null, a hitobject is expected to be later applied via (or automatically via pooling). /// protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null) - : base(initialHitObject != null ? new SyntheticHitObjectEntry(initialHitObject) : null) { - if (Entry != null) - ensureEntryHasResult(); + if (initialHitObject == null) return; + + Entry = new SyntheticHitObjectEntry(initialHitObject); + ensureEntryHasResult(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index 4440ca8d21..d5d1a7b55c 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; +using osu.Framework.Graphics; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Pooling; @@ -16,14 +17,32 @@ namespace osu.Game.Rulesets.Objects.Pooling /// The type storing state and controlling this drawable. public abstract class PoolableDrawableWithLifetime : PoolableDrawable where TEntry : LifetimeEntry { + private TEntry? entry; + /// /// The entry holding essential state of this . /// - public TEntry? Entry { get; private set; } + /// + /// If a non-null value is set before loading is started, the entry is applied when the loading is completed. + /// + public TEntry? Entry + { + get => entry; + + set + { + if (LoadState < LoadState.Ready) + entry = value; + else if (value != null) + Apply(value); + else if (HasEntryApplied) + free(); + } + } /// /// Whether is applied to this . - /// When an initial entry is specified in the constructor, is set but not applied until loading is completed. + /// When an is set during initialization, it is not applied until loading is completed. /// protected bool HasEntryApplied { get; private set; } @@ -65,7 +84,7 @@ namespace osu.Game.Rulesets.Objects.Pooling { base.LoadAsyncComplete(); - // Apply the initial entry given in the constructor. + // Apply the initial entry. if (Entry != null && !HasEntryApplied) Apply(Entry); } @@ -79,7 +98,7 @@ namespace osu.Game.Rulesets.Objects.Pooling if (HasEntryApplied) free(); - Entry = entry; + this.entry = entry; entry.LifetimeChanged += setLifetimeFromEntry; setLifetimeFromEntry(entry); @@ -113,12 +132,12 @@ namespace osu.Game.Rulesets.Objects.Pooling private void free() { - Debug.Assert(Entry != null && HasEntryApplied); + Debug.Assert(entry != null && HasEntryApplied); - OnFree(Entry); + OnFree(entry); - Entry.LifetimeChanged -= setLifetimeFromEntry; - Entry = null; + entry.LifetimeChanged -= setLifetimeFromEntry; + entry = null; base.LifetimeStart = double.MinValue; base.LifetimeEnd = double.MaxValue; From 55859938b183c950b94625c552c66478a6ec3168 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 16:01:29 +0900 Subject: [PATCH 367/670] Use object initializer syntax for hit object application in tests --- .../Gameplay/TestSceneDrawableHitObject.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index da0d57f9d1..0ce71696bd 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -44,11 +44,9 @@ namespace osu.Game.Tests.Gameplay { TestDrawableHitObject dho = null; TestLifetimeEntry entry = null; - AddStep("Create DHO", () => + AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject { - dho = new TestDrawableHitObject(null); - dho.Apply(entry = new TestLifetimeEntry(new HitObject())); - Child = dho; + Entry = entry = new TestLifetimeEntry(new HitObject()) }); AddStep("KeepAlive = true", () => @@ -81,12 +79,10 @@ namespace osu.Game.Tests.Gameplay AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET); TestDrawableHitObject dho = null; - AddStep("Create DHO", () => + AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject { - dho = new TestDrawableHitObject(null); - dho.Apply(entry); - Child = dho; - dho.SetLifetimeStartOnApply = true; + Entry = entry, + SetLifetimeStartOnApply = true }); AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY); @@ -97,11 +93,9 @@ namespace osu.Game.Tests.Gameplay { TestDrawableHitObject dho = null; TestLifetimeEntry entry = null; - AddStep("Create DHO", () => + AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject { - dho = new TestDrawableHitObject(null); - dho.Apply(entry = new TestLifetimeEntry(new HitObject())); - Child = dho; + Entry = entry = new TestLifetimeEntry(new HitObject()) }); AddStep("Set entry lifetime", () => @@ -135,7 +129,7 @@ namespace osu.Game.Tests.Gameplay public bool SetLifetimeStartOnApply; - public TestDrawableHitObject(HitObject hitObject) + public TestDrawableHitObject(HitObject hitObject = null) : base(hitObject) { } From a5c09454e61814c8bf3b05aff6f80ff4f1d29003 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 16:16:18 +0900 Subject: [PATCH 368/670] Remove unnecessary configuration --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 15fc8c130e..aeb71ccaf1 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -1,11 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -19,16 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Mods public override string Description => @"Beats fade out before you hit them!"; public override double ScoreMultiplier => 1.06; - [SettingSource("Fade-out Time", "The bigger this multiplier is, the sooner the notes will start fading out")] - public BindableNumber FadeOutTimeMultiplier { get; } = new BindableDouble - { - MinValue = 0.5, - MaxValue = 1.5, - Default = 1.0, - Value = 1.0, - Precision = 0.01, - }; - /// /// In stable taiko, the hit position is 160, so the active playfield is essentially 160 pixels shorter /// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the @@ -68,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Mods return; } - var preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime) * FadeOutTimeMultiplier.Value; + var preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime); var start = hitObject.HitObject.StartTime - preempt * 0.6; var duration = preempt * 0.3; From 1632450918c9af1955f32fdc1a6a676550087440 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 16:17:14 +0900 Subject: [PATCH 369/670] Add comments --- osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs index 1038af7a48..fc34ba4c8b 100644 --- a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs +++ b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs @@ -16,6 +16,9 @@ namespace osu.Game.Rulesets.Catch.UI /// public class SkinnableCatcher : SkinnableDrawable { + /// + /// This is used by skin elements to determine which texture of the catcher is used. + /// [Cached] public readonly Bindable AnimationState = new Bindable(); From b087c95581e960f80fa26af370aceb15dadf251e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 16:14:42 +0900 Subject: [PATCH 370/670] Use a frozen clock for catcher trails --- osu.Game.Rulesets.Catch/UI/CatcherTrail.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs index f273ef23ac..80522ab36b 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; +using osu.Framework.Timing; using osuTK; namespace osu.Game.Rulesets.Catch.UI @@ -11,8 +12,6 @@ namespace osu.Game.Rulesets.Catch.UI /// A trail of the catcher. /// It also represents a hyper dash afterimage. /// - // TODO: Trails shouldn't be animated when the skin has an animated catcher. - // The animation should be frozen at the animation frame at the time of the trail generation. public class CatcherTrail : PoolableDrawable { public CatcherAnimationState AnimationState @@ -27,7 +26,12 @@ namespace osu.Game.Rulesets.Catch.UI Size = new Vector2(CatcherArea.CATCHER_SIZE); Origin = Anchor.TopCentre; Blending = BlendingParameters.Additive; - InternalChild = body = new SkinnableCatcher(); + InternalChild = body = new SkinnableCatcher + { + // Using a frozen clock because trails should not be animated when the skin has an animated catcher. + // TODO: The animation should be frozen at the animation frame at the time of the trail generation. + Clock = new FramedClock(new ManualClock()), + }; } protected override void FreeAfterUse() From 6d6604e2f0c8c65570b8d99661894ede7d7f3b38 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 16:19:46 +0900 Subject: [PATCH 371/670] Fix incorrect indentation I used this for the o!f example. --- .../Overlays/BeatmapListing/SearchPlayed.cs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs index 93c0644d45..f24cf46c2d 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs @@ -7,32 +7,32 @@ using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.BeatmapListing { -[LocalisableEnum(typeof(SearchPlayedEnumLocalisationMapper))] -public enum SearchPlayed -{ - Any, - Played, - Unplayed -} - -public class SearchPlayedEnumLocalisationMapper : EnumLocalisationMapper -{ - public override LocalisableString Map(SearchPlayed value) + [LocalisableEnum(typeof(SearchPlayedEnumLocalisationMapper))] + public enum SearchPlayed { - switch (value) + Any, + Played, + Unplayed + } + + public class SearchPlayedEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(SearchPlayed value) { - case SearchPlayed.Any: - return BeatmapsStrings.PlayedAny; + switch (value) + { + case SearchPlayed.Any: + return BeatmapsStrings.PlayedAny; - case SearchPlayed.Played: - return BeatmapsStrings.PlayedPlayed; + case SearchPlayed.Played: + return BeatmapsStrings.PlayedPlayed; - case SearchPlayed.Unplayed: - return BeatmapsStrings.PlayedUnplayed; + case SearchPlayed.Unplayed: + return BeatmapsStrings.PlayedUnplayed; - default: - throw new ArgumentOutOfRangeException(nameof(value), value, null); + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } } } } -} From fafd936c93903fd773c1d671282e1747647b1c44 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 16:21:36 +0900 Subject: [PATCH 372/670] Localise "sort by" string in overlays --- osu.Game/Overlays/OverlaySortTabControl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs index 0ebabd424f..15c43eeb01 100644 --- a/osu.Game/Overlays/OverlaySortTabControl.cs +++ b/osu.Game/Overlays/OverlaySortTabControl.cs @@ -18,6 +18,7 @@ using JetBrains.Annotations; using System; using osu.Framework.Extensions; using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays { @@ -54,7 +55,7 @@ namespace osu.Game.Overlays Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = @"Sort by" + Text = SortStrings.Default }, CreateControl().With(c => { From 5944c45f55314f962fc207dff1ae89448252dafa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 16:24:30 +0900 Subject: [PATCH 373/670] Specify types explicitly and don't handle non-nullable values with fallbacks --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 13 +++++++------ osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index aeb71ccaf1..f787a75c51 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -37,9 +37,10 @@ namespace osu.Game.Rulesets.Taiko.Mods protected double MultiplierAt(double position) { - var beatLength = controlPointInfo.TimingPointAt(position)?.BeatLength; - var speedMultiplier = controlPointInfo.DifficultyPointAt(position)?.SpeedMultiplier; - return originalSliderMultiplier * (speedMultiplier ?? 1.0) * TimingControlPoint.DEFAULT_BEAT_LENGTH / (beatLength ?? TimingControlPoint.DEFAULT_BEAT_LENGTH); + double beatLength = controlPointInfo.TimingPointAt(position).BeatLength; + double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier; + + return originalSliderMultiplier * speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength; } protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) @@ -56,9 +57,9 @@ namespace osu.Game.Rulesets.Taiko.Mods return; } - var preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime); - var start = hitObject.HitObject.StartTime - preempt * 0.6; - var duration = preempt * 0.3; + double preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime); + double start = hitObject.HitObject.StartTime - preempt * 0.6; + double duration = preempt * 0.3; using (hitObject.BeginAbsoluteSequence(start)) { diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index d3a4b635f5..25d0843a71 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Lists; @@ -66,6 +67,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time to find the difficulty control point at. /// The difficulty control point. + [NotNull] public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT); /// @@ -73,6 +75,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time to find the effect control point at. /// The effect control point. + [NotNull] public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT); /// @@ -80,6 +83,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time to find the sound control point at. /// The sound control point. + [NotNull] public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT); /// @@ -87,6 +91,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time to find the timing control point at. /// The timing control point. + [NotNull] public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT); /// From 18343160cfee4f5a1d7dde5296d1b4502ce655df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 16:28:57 +0900 Subject: [PATCH 374/670] Reword comments slightly --- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index f787a75c51..434069291c 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public override double ScoreMultiplier => 1.06; /// - /// In stable taiko, the hit position is 160, so the active playfield is essentially 160 pixels shorter + /// In osu-stable, the hit position is 160, so the active playfield is essentially 160 pixels shorter /// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the /// playfield ratio of the active area up to the hit position will actually be (640 - 160) / 480 = 1. /// For custom resolutions/aspect ratios (x:y), the screen width given the normalized height becomes 480 * x / y instead, @@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Mods private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0); private double originalSliderMultiplier; + private ControlPointInfo controlPointInfo; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) @@ -79,12 +80,13 @@ namespace osu.Game.Rulesets.Taiko.Mods public void ApplyToDifficulty(BeatmapDifficulty difficulty) { + // needs to be read after all processing has been run (TaikoBeatmapConverter applies an adjustment which would otherwise be omitted). originalSliderMultiplier = difficulty.SliderMultiplier; - // the hidden mod on stable had an added playfield cover that essentially forced a 4:3 playfield ratio, by cutting off all objects past that size. - // lazer currently uses a playfield adjustment container which keeps a 16:9 ratio. - // therefore, increase the slider multiplier proportionally so that the notes stay on the screen for the same amount of time as on stable. - // note that this will means that the notes will scroll faster as they have a longer distance to travel on the screen in that same amount of time. + // osu-stable has an added playfield cover that essentially forces a 4:3 playfield ratio, by cutting off all objects past that size. + // This is not yet implemented; instead a playfield adjustment container is present which maintains a 16:9 ratio. + // For now, increase the slider multiplier proportionally so that the notes stay on the screen for the same amount of time as on stable. + // Note that this means that the notes will scroll faster as they have a longer distance to travel on the screen in that same amount of time. difficulty.SliderMultiplier /= hd_sv_scale; } From 98e0e89d3f9a2358312a56d23046759e43bb180b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 16:32:59 +0900 Subject: [PATCH 375/670] Nest adjustments for readability --- .../Mods/TaikoModHidden.cs | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 434069291c..0fd3625a93 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -49,28 +49,23 @@ namespace osu.Game.Rulesets.Taiko.Mods switch (hitObject) { case DrawableDrumRollTick _: - break; - case DrawableHit _: + double preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime); + double start = hitObject.HitObject.StartTime - preempt * 0.6; + double duration = preempt * 0.3; + + using (hitObject.BeginAbsoluteSequence(start)) + { + hitObject.FadeOut(duration); + + // DrawableHitObject sets LifetimeEnd to LatestTransformEndTime if it isn't manually changed. + // in order for the object to not be killed before its actual end time (as the latest transform ends earlier), set lifetime end explicitly. + hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged + ? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + : hitObject.HitStateUpdateTime; + } + break; - - default: - return; - } - - double preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime); - double start = hitObject.HitObject.StartTime - preempt * 0.6; - double duration = preempt * 0.3; - - using (hitObject.BeginAbsoluteSequence(start)) - { - hitObject.FadeOut(duration); - - // DrawableHitObject sets LifetimeEnd to LatestTransformEndTime if it isn't manually changed. - // in order for the object to not be killed before its actual end time (as the latest transform ends earlier), set lifetime end explicitly. - hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged - ? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) - : hitObject.HitStateUpdateTime; } } From 64bb1f381bdbc2647c1883fb386686fe7b0943cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Jun 2021 10:25:30 +0200 Subject: [PATCH 376/670] Add more languages to settings dropdown --- osu.Game/Localisation/Language.cs | 99 ++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index a3e845f229..0ad6b45104 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.cs @@ -10,7 +10,104 @@ namespace osu.Game.Localisation [Description(@"English")] en, + // TODO: Requires Arabic glyphs to be added to resources (and possibly also RTL support). + // [Description(@"اَلْعَرَبِيَّةُ")] + // ar, + + // TODO: Some accented glyphs are missing. Revisit when adding Inter. + // [Description(@"Беларуская мова")] + // be, + + [Description(@"Български")] + bg, + + [Description(@"Česky")] + cs, + + [Description(@"Dansk")] + da, + + [Description(@"Deutsch")] + de, + + // TODO: Some accented glyphs are missing. Revisit when adding Inter. + // [Description(@"Ελληνικά")] + // el, + + [Description(@"español")] + es, + + [Description(@"Suomi")] + fi, + + [Description(@"français")] + fr, + + [Description(@"Magyar")] + hu, + + [Description(@"Bahasa Indonesia")] + id, + + [Description(@"Italiano")] + it, + [Description(@"日本語")] - ja + ja, + + [Description(@"한국어")] + ko, + + [Description(@"Nederlands")] + nl, + + [Description(@"Norsk")] + no, + + [Description(@"polski")] + pl, + + [Description(@"Português")] + pt, + + [Description(@"Português (Brasil)")] + pt_br, + + [Description(@"Română")] + ro, + + [Description(@"Русский")] + ru, + + [Description(@"Slovenčina")] + sk, + + [Description(@"Svenska")] + se, + + [Description(@"ไทย")] + th, + + [Description(@"Tagalog")] + tl, + + [Description(@"Türkçe")] + tr, + + // TODO: Some accented glyphs are missing. Revisit when adding Inter. + // [Description(@"Українська мова")] + // uk, + + [Description(@"Tiếng Việt")] + vn, + + [Description(@"简体中文")] + zh, + + [Description(@"繁體中文(香港)")] + zh_hk, + + [Description(@"繁體中文(台灣)")] + zh_tw } } From d298e95df74eea7986245006bc9355dd9b024c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Jun 2021 10:25:50 +0200 Subject: [PATCH 377/670] Limit maximum height of settings enum dropdowns --- osu.Game/Overlays/Settings/SettingsEnumDropdown.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index c77d14632b..9987a0c607 100644 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs @@ -19,6 +19,8 @@ namespace osu.Game.Overlays.Settings Margin = new MarginPadding { Top = 5 }; RelativeSizeAxes = Axes.X; } + + protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200); } } } From ee5f4f18568e69d7263aba6381c0035874d4a232 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 17:27:43 +0900 Subject: [PATCH 378/670] Remove default (and make default "Gravity") --- osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index cd718a7b18..d01c036768 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs @@ -95,16 +95,15 @@ namespace osu.Game.Rulesets.Osu.Mods public enum AnimationStyle { - Default, + Gravity, + InOut1, + InOut2, Accelerate1, Accelerate2, Accelerate3, - Gravity, Decelerate1, Decelerate2, Decelerate3, - InOut1, - InOut2, } } } From 0c1023da3115e3741703f2e4eb719077ac2374ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 17:27:52 +0900 Subject: [PATCH 379/670] Simplify transform logic --- .../Mods/OsuModApproachDifferent.cs | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index d01c036768..8f772e88ac 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs @@ -39,27 +39,23 @@ namespace osu.Game.Rulesets.Osu.Mods { drawables.ForEach(drawable => { - drawable.ApplyCustomUpdateState += (drawableHitObj, state) => + drawable.ApplyCustomUpdateState += (drawableObject, state) => { - if (!(drawableHitObj is DrawableHitCircle hitCircle)) return; + if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return; - var obj = hitCircle.HitObject; + var hitCircle = drawableHitCircle.HitObject; - hitCircle.BeginAbsoluteSequence(obj.StartTime - obj.TimePreempt, true); - hitCircle.ApproachCircle.ScaleTo(Scale.Value); + drawableHitCircle.ApproachCircle.ClearTransforms(targetMember: nameof(Scale)); - hitCircle.ApproachCircle.FadeIn(Math.Min(obj.TimeFadeIn, obj.TimePreempt)); - - hitCircle.ApproachCircle.ScaleTo(1f, obj.TimePreempt, getEasing(Style.Value)); - - hitCircle.ApproachCircle.Expire(true); + using (drawableHitCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt)) + drawableHitCircle.ApproachCircle.ScaleTo(Scale.Value).ScaleTo(1f, hitCircle.TimePreempt, getEasing(Style.Value)); }; }); } - private Easing getEasing(AnimationStyle approachEasing) + private Easing getEasing(AnimationStyle style) { - switch (approachEasing) + switch (style) { default: return Easing.None; From 7891ee4f32ea77b659c33a6d6b7f7e537eee7112 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 17:32:30 +0900 Subject: [PATCH 380/670] Change order of settings to make scrolling easier There's an issue with dropdown menus nested inside a scroll view being very frustrating to scroll to off-screen items. This works around that to some extent by giving the user more "parent-scrollable" space to mouse wheel or drag over. --- .../Mods/OsuModApproachDifferent.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index 8f772e88ac..3e638c4833 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.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 System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; @@ -22,19 +21,17 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; - [SettingSource("Style", "Change the animation style of the approach circles.", 0)] - public Bindable Style { get; } = new Bindable(); - - [SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 1)] - public BindableFloat Scale { get; } = new BindableFloat + [SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)] + public BindableFloat Scale { get; } = new BindableFloat(4) { Precision = 0.1f, MinValue = 2, MaxValue = 10, - Default = 4, - Value = 4 }; + [SettingSource("Style", "Change the animation style of the approach circles.", 1)] + public Bindable Style { get; } = new Bindable(); + public void ApplyToDrawableHitObjects(IEnumerable drawables) { drawables.ForEach(drawable => From 521077b7148eb896792ed5c1a451074ab8957746 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 17:44:18 +0900 Subject: [PATCH 381/670] Make `getRulesetTransformedSkin` private --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 8a27899e89..1953bd499b 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Skinning Ruleset = ruleset; Beatmap = beatmap; - InternalChild = new BeatmapSkinProvidingContainer(GetRulesetTransformedSkin(beatmapSkin)) + InternalChild = new BeatmapSkinProvidingContainer(getRulesetTransformedSkin(beatmapSkin)) { Child = Content = new Container { @@ -54,17 +54,17 @@ namespace osu.Game.Skinning { SkinSources.Clear(); - SkinSources.Add(GetRulesetTransformedSkin(skinManager.CurrentSkin.Value)); + SkinSources.Add(getRulesetTransformedSkin(skinManager.CurrentSkin.Value)); // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. if (skinManager.CurrentSkin.Value is LegacySkin && skinManager.CurrentSkin.Value != skinManager.DefaultLegacySkin) - SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultLegacySkin)); + SkinSources.Add(getRulesetTransformedSkin(skinManager.DefaultLegacySkin)); if (skinManager.CurrentSkin.Value != skinManager.DefaultSkin) - SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultSkin)); + SkinSources.Add(getRulesetTransformedSkin(skinManager.DefaultSkin)); } - protected ISkin GetRulesetTransformedSkin(ISkin skin) + private ISkin getRulesetTransformedSkin(ISkin skin) { if (skin == null) return null; From 2b0e6b6b5181c5d65f48fba003da63a034d52b65 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 17:44:21 +0900 Subject: [PATCH 382/670] Don't invoke "completed" action for test scene virtual track `MusicController` tries to play the next music when a track is completed. In test scenes, we want to keep the virtual track, not random songs. --- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index a4c78f24e3..98aad821ce 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -350,7 +350,7 @@ namespace osu.Game.Tests.Visual if (CurrentTime >= Length) { Stop(); - RaiseCompleted(); + // `RaiseCompleted` is not called here to prevent transitioning to the next song. } } } From cc5145a131fd25b1bab52c7fcddbac047681d4ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Jun 2021 11:10:03 +0200 Subject: [PATCH 383/670] Fix languages with a sub-language part not working properly --- osu.Game/Extensions/LanguageExtensions.cs | 33 +++++++++++++++++++ osu.Game/OsuGame.cs | 3 +- .../Sections/General/LanguageSettings.cs | 6 ++-- 3 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Extensions/LanguageExtensions.cs diff --git a/osu.Game/Extensions/LanguageExtensions.cs b/osu.Game/Extensions/LanguageExtensions.cs new file mode 100644 index 0000000000..b67e7fb6fc --- /dev/null +++ b/osu.Game/Extensions/LanguageExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Globalization; +using osu.Game.Localisation; + +namespace osu.Game.Extensions +{ + /// + /// Conversion utilities for the enum. + /// + public static class LanguageExtensions + { + /// + /// Returns the culture code of the that corresponds to the supplied . + /// + /// + /// This is required as enum member names are not allowed to contain hyphens. + /// + public static string ToCultureCode(this Language language) + => language.ToString().Replace("_", "-"); + + /// + /// Attempts to parse the supplied to a value. + /// + /// The code of the culture to parse. + /// The parsed . Valid only if the return value of the method is . + /// Whether the parsing succeeded. + public static bool TryParseCultureCode(string cultureCode, out Language language) + => Enum.TryParse(cultureCode.Replace("-", "_"), out language); + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 02e724a451..6eda4ff425 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -50,6 +50,7 @@ using osu.Game.Updater; using osu.Game.Utils; using LogLevel = osu.Framework.Logging.LogLevel; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Skinning.Editor; @@ -580,7 +581,7 @@ namespace osu.Game foreach (var language in Enum.GetValues(typeof(Language)).OfType()) { - var cultureCode = language.ToString(); + var cultureCode = language.ToCultureCode(); Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode)); } diff --git a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs index c2767f61b4..dfcdb8e340 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs @@ -1,11 +1,11 @@ // 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.Bindables; using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Game.Extensions; using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.General @@ -35,11 +35,11 @@ namespace osu.Game.Overlays.Settings.Sections.General }, }; - if (!Enum.TryParse(frameworkLocale.Value, out var locale)) + if (!LanguageExtensions.TryParseCultureCode(frameworkLocale.Value, out var locale)) locale = Language.en; languageSelection.Current.Value = locale; - languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToString()); + languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode()); } } } From e69bb67afe6e1ab615319844dfdf6ac3dfccd530 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 18:42:28 +0900 Subject: [PATCH 384/670] Add `IApplicableToDrawableHitObject` that is taking a single DHO Less cumbersome to implement than old version taking an enumerable. The implementation was always using `foreach` for the enumerable. The new interface is not used yet. --- .../Rulesets/Mods/IApplicableToDrawableHitObject.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs index 5630315770..a774ad5924 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mods @@ -9,13 +10,19 @@ namespace osu.Game.Rulesets.Mods /// /// An interface for s that can be applied to s. /// - public interface IApplicableToDrawableHitObjects : IApplicableMod + public interface IApplicableToDrawableHitObject : IApplicableMod { /// - /// Applies this to a list of s. + /// Applies this to a . /// This will only be invoked with top-level s. Access if adjusting nested objects is necessary. /// - /// The list of s to apply to. + void ApplyToDrawableHitObject(DrawableHitObject drawable); + } + + public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject + { void ApplyToDrawableHitObjects(IEnumerable drawables); + + void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield()); } } From 67d8e0059f8ffa84de6f3811dc57bdfb988663ec Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 18:46:29 +0900 Subject: [PATCH 385/670] Use singular `IApplicableToDrawableHitObject` for consumers --- .../TestSceneDrawableHitObjects.cs | 4 ++-- osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs | 4 ++-- osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs | 4 ++-- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 4 ++-- osu.Game/Rulesets/UI/DrawableRuleset.cs | 7 +++++-- osu.Game/Rulesets/UI/Playfield.cs | 4 ++-- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 3e4995482d..fd6a9c7b7b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Catch.Tests private void addToPlayfield(DrawableCatchHitObject drawable) { - foreach (var mod in SelectedMods.Value.OfType()) - mod.ApplyToDrawableHitObjects(new[] { drawable }); + foreach (var mod in SelectedMods.Value.OfType()) + mod.ApplyToDrawableHitObject(drawable); drawableRuleset.Playfield.Add(drawable); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 58e46b6687..07acd5b6e2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -75,8 +75,8 @@ namespace osu.Game.Rulesets.Osu.Tests var drawable = CreateDrawableHitCircle(circle, auto); - foreach (var mod in SelectedMods.Value.OfType()) - mod.ApplyToDrawableHitObjects(new[] { drawable }); + foreach (var mod in SelectedMods.Value.OfType()) + mod.ApplyToDrawableHitObject(drawable); return drawable; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index fc5fcf2358..81902c25af 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -335,8 +335,8 @@ namespace osu.Game.Rulesets.Osu.Tests var drawable = CreateDrawableSlider(slider); - foreach (var mod in SelectedMods.Value.OfType()) - mod.ApplyToDrawableHitObjects(new[] { drawable }); + foreach (var mod in SelectedMods.Value.OfType()) + mod.ApplyToDrawableHitObject(drawable); drawable.OnNewResult += onNewResult; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index b21b7a6f4a..2dea9837f3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -85,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Tests Scale = new Vector2(0.75f) }; - foreach (var mod in SelectedMods.Value.OfType()) - mod.ApplyToDrawableHitObjects(new[] { drawableSpinner }); + foreach (var mod in SelectedMods.Value.OfType()) + mod.ApplyToDrawableHitObject(drawableSpinner); return drawableSpinner; } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 0ab8b94e3f..8dcc1ca164 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -199,8 +199,11 @@ namespace osu.Game.Rulesets.UI Playfield.PostProcess(); - foreach (var mod in Mods.OfType()) - mod.ApplyToDrawableHitObjects(Playfield.AllHitObjects); + foreach (var mod in Mods.OfType()) + { + foreach (var drawableHitObject in Playfield.AllHitObjects) + mod.ApplyToDrawableHitObject(drawableHitObject); + } } public override void RequestResume(Action continueResume) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index b154288dba..52aecb27de 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -356,8 +356,8 @@ namespace osu.Game.Rulesets.UI // This is done before Apply() so that the state is updated once when the hitobject is applied. if (mods != null) { - foreach (var m in mods.OfType()) - m.ApplyToDrawableHitObjects(dho.Yield()); + foreach (var m in mods.OfType()) + m.ApplyToDrawableHitObject(dho); } } From 379b84ba22b54d76033b16c58628bdce258a92b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 18:51:17 +0900 Subject: [PATCH 386/670] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 490e43b5e6..1f60f02fb1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8eeaad1127..68ffb87c6c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -34,7 +34,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index db442238ce..8aa79762fc 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From af80418ee8c7cd012beb819cbb96aa6ae7226566 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 18:52:01 +0900 Subject: [PATCH 387/670] Implement `IApplicableToDrawableHitObject` for mods A breaking change in `ModWithVisibilityAdjustment` if the method was overriden. --- .../Mods/OsuModBarrelRoll.cs | 22 ++++++--------- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 28 ++++++++----------- .../Mods/OsuModFlashlight.cs | 10 ++----- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 14 ++++------ .../Mods/ModWithVisibilityAdjustment.cs | 21 ++++++-------- 5 files changed, 38 insertions(+), 57 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs index 9ae9653e9b..9e71f657ce 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.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 osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -9,22 +8,19 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModBarrelRoll : ModBarrelRoll, IApplicableToDrawableHitObjects + public class OsuModBarrelRoll : ModBarrelRoll, IApplicableToDrawableHitObject { - public void ApplyToDrawableHitObjects(IEnumerable drawables) + public void ApplyToDrawableHitObject(DrawableHitObject d) { - foreach (var d in drawables) + d.OnUpdate += _ => { - d.OnUpdate += _ => + switch (d) { - switch (d) - { - case DrawableHitCircle circle: - circle.CirclePiece.Rotation = -CurrentRotation; - break; - } - }; - } + case DrawableHitCircle circle: + circle.CirclePiece.Rotation = -CurrentRotation; + break; + } + }; } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 77dea5b0dc..e04a30d06c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.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.Bindables; using osu.Game.Configuration; @@ -15,7 +14,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset + public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset { [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true); @@ -54,24 +53,21 @@ namespace osu.Game.Rulesets.Osu.Mods osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); } - public void ApplyToDrawableHitObjects(IEnumerable drawables) + public void ApplyToDrawableHitObject(DrawableHitObject obj) { - foreach (var obj in drawables) + switch (obj) { - switch (obj) - { - case DrawableSlider slider: - slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value; - break; + case DrawableSlider slider: + slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value; + break; - case DrawableSliderHead head: - head.TrackFollowCircle = !NoSliderHeadMovement.Value; - break; + case DrawableSliderHead head: + head.TrackFollowCircle = !NoSliderHeadMovement.Value; + break; - case DrawableSliderTail tail: - tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value; - break; - } + case DrawableSliderTail tail: + tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value; + break; } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index 683b35f282..300a9d48aa 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input; @@ -19,7 +17,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModFlashlight : ModFlashlight, IApplicableToDrawableHitObjects + public class OsuModFlashlight : ModFlashlight, IApplicableToDrawableHitObject { public override double ScoreMultiplier => 1.12; @@ -31,12 +29,10 @@ namespace osu.Game.Rulesets.Osu.Mods public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(); - public void ApplyToDrawableHitObjects(IEnumerable drawables) + public void ApplyToDrawableHitObject(DrawableHitObject drawable) { - foreach (var s in drawables.OfType()) - { + if (drawable is DrawableSlider s) s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange; - } } public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index b12d735474..c7f4811701 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; @@ -13,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects + public class OsuModSpunOut : Mod, IApplicableToDrawableHitObject { public override string Name => "Spun Out"; public override string Acronym => "SO"; @@ -23,15 +22,12 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 0.9; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; - public void ApplyToDrawableHitObjects(IEnumerable drawables) + public void ApplyToDrawableHitObject(DrawableHitObject hitObject) { - foreach (var hitObject in drawables) + if (hitObject is DrawableSpinner spinner) { - if (hitObject is DrawableSpinner spinner) - { - spinner.HandleUserInput = false; - spinner.OnUpdate += onSpinnerUpdate; - } + spinner.HandleUserInput = false; + spinner.OnUpdate += onSpinnerUpdate; } } diff --git a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs index 5b119b5e46..b58ee5ff36 100644 --- a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs +++ b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods /// A which applies visibility adjustments to s /// with an optional increased visibility adjustment depending on the user's "increase first object visibility" setting. /// - public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObjects + public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObject { /// /// The first adjustable object. @@ -73,19 +73,16 @@ namespace osu.Game.Rulesets.Mods } } - public virtual void ApplyToDrawableHitObjects(IEnumerable drawables) + public virtual void ApplyToDrawableHitObject(DrawableHitObject dho) { - foreach (var dho in drawables) + dho.ApplyCustomUpdateState += (o, state) => { - dho.ApplyCustomUpdateState += (o, state) => - { - // Increased visibility is applied to the entire first object, including all of its nested hitobjects. - if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject)) - ApplyIncreasedVisibilityState(o, state); - else - ApplyNormalVisibilityState(o, state); - }; - } + // Increased visibility is applied to the entire first object, including all of its nested hitobjects. + if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject)) + ApplyIncreasedVisibilityState(o, state); + else + ApplyNormalVisibilityState(o, state); + }; } /// From c59a3ce83f32dabedc0650912825a842723184d8 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 18:52:16 +0900 Subject: [PATCH 388/670] Obsolete plural `IApplicableToDrawableHitObjects` --- osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs index a774ad5924..93055e733d 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.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 System.Collections.Generic; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Objects.Drawables; @@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Mods void ApplyToDrawableHitObject(DrawableHitObject drawable); } + [Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216 public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject { void ApplyToDrawableHitObjects(IEnumerable drawables); From dc2e3ff89e7291054e39901715045ad8edfe4485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Jun 2021 12:01:58 +0200 Subject: [PATCH 389/670] Fix wrong language codes Confused them with the flags from the web-side source used. Tagalog (`tl`) still has no discernible effect, but that's because there are actually no localisation files in `osu-resources` for that language. Leave it be for now, as web has that entry, so it might mean that translations might be coming at some point in the future. --- osu.Game/Localisation/Language.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index 0ad6b45104..96bfde8596 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.cs @@ -83,7 +83,7 @@ namespace osu.Game.Localisation sk, [Description(@"Svenska")] - se, + sv, [Description(@"ไทย")] th, @@ -99,7 +99,7 @@ namespace osu.Game.Localisation // uk, [Description(@"Tiếng Việt")] - vn, + vi, [Description(@"简体中文")] zh, From 37babbde6a0db61c2d065d306066bd694252cdf7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 19:07:11 +0900 Subject: [PATCH 390/670] Simplify score filter row --- .../BeatmapSearchScoreFilterRow.cs | 35 +--------------- osu.Game/Scoring/ScoreRank.cs | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs index abfffe907f..b39934b56f 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs @@ -1,9 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; @@ -33,38 +33,7 @@ namespace osu.Game.Overlays.BeatmapListing { } - protected override LocalisableString LabelFor(ScoreRank value) - { - switch (value) - { - case ScoreRank.XH: - return BeatmapsStrings.RankXH; - - case ScoreRank.X: - return BeatmapsStrings.RankX; - - case ScoreRank.SH: - return BeatmapsStrings.RankSH; - - case ScoreRank.S: - return BeatmapsStrings.RankS; - - case ScoreRank.A: - return BeatmapsStrings.RankA; - - case ScoreRank.B: - return BeatmapsStrings.RankB; - - case ScoreRank.C: - return BeatmapsStrings.RankC; - - case ScoreRank.D: - return BeatmapsStrings.RankD; - - default: - throw new ArgumentException("Unsupported value.", nameof(value)); - } - } + protected override LocalisableString LabelFor(ScoreRank value) => value.GetLocalisableDescription(); } } } diff --git a/osu.Game/Scoring/ScoreRank.cs b/osu.Game/Scoring/ScoreRank.cs index 696d493830..f3b4551ff8 100644 --- a/osu.Game/Scoring/ScoreRank.cs +++ b/osu.Game/Scoring/ScoreRank.cs @@ -1,10 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Scoring { + [LocalisableEnum(typeof(ScoreRankEnumLocalisationMapper))] public enum ScoreRank { [Description(@"D")] @@ -31,4 +35,40 @@ namespace osu.Game.Scoring [Description(@"SS+")] XH, } + + public class ScoreRankEnumLocalisationMapper : EnumLocalisationMapper + { + public override LocalisableString Map(ScoreRank value) + { + switch (value) + { + case ScoreRank.XH: + return BeatmapsStrings.RankXH; + + case ScoreRank.X: + return BeatmapsStrings.RankX; + + case ScoreRank.SH: + return BeatmapsStrings.RankSH; + + case ScoreRank.S: + return BeatmapsStrings.RankS; + + case ScoreRank.A: + return BeatmapsStrings.RankA; + + case ScoreRank.B: + return BeatmapsStrings.RankB; + + case ScoreRank.C: + return BeatmapsStrings.RankC; + + case ScoreRank.D: + return BeatmapsStrings.RankD; + + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + } } From 2155a4da0a3257b2e3b8e3488eabed564bcdd987 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 19:52:58 +0900 Subject: [PATCH 391/670] Fix intermittent HUD test failure --- osu.Game/Skinning/SkinnableTargetContainer.cs | 6 ++++++ osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs index 1338462dd6..53b142f09a 100644 --- a/osu.Game/Skinning/SkinnableTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetContainer.cs @@ -18,6 +18,8 @@ namespace osu.Game.Skinning private readonly BindableList components = new BindableList(); + public bool ComponentsLoaded { get; private set; } + public SkinnableTargetContainer(SkinnableTarget target) { Target = target; @@ -30,6 +32,7 @@ namespace osu.Game.Skinning { ClearInternal(); components.Clear(); + ComponentsLoaded = false; content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer; @@ -39,8 +42,11 @@ namespace osu.Game.Skinning { AddInternal(wrapper); components.AddRange(wrapper.Children.OfType()); + ComponentsLoaded = true; }); } + else + ComponentsLoaded = true; } /// diff --git a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs index b810bbf6ae..d74be70df8 100644 --- a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs +++ b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.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.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -47,6 +48,8 @@ namespace osu.Game.Tests.Visual LegacySkin.ResetDrawableTarget(t); t.Reload(); })); + + AddUntilStep("wait for components to load", () => this.ChildrenOfType().All(t => t.ComponentsLoaded)); } public class SkinProvidingPlayer : TestPlayer From a295421b64a81d1816a172040e8ffb9a31f46887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Jun 2021 13:27:02 +0200 Subject: [PATCH 392/670] Use language-specific lookup key for `Other` filter --- osu.Game/Overlays/BeatmapListing/SearchLanguage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs index 352383d576..fc176c305a 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs @@ -107,7 +107,7 @@ namespace osu.Game.Overlays.BeatmapListing return BeatmapsStrings.LanguagePolish; case SearchLanguage.Other: - return BeatmapsStrings.GenreOther; + return BeatmapsStrings.LanguageOther; default: throw new ArgumentOutOfRangeException(nameof(value), value, null); From 19f0e3d695c592ab882bd40e9309b7f7a393940b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Jun 2021 20:53:48 +0900 Subject: [PATCH 393/670] Add HighPerformanceSession --- osu.Game/OsuGame.cs | 5 ++ .../Performance/HighPerformanceSession.cs | 47 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 osu.Game/Performance/HighPerformanceSession.cs diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 6eda4ff425..0c4d035728 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -53,6 +53,7 @@ using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Localisation; +using osu.Game.Performance; using osu.Game.Skinning.Editor; namespace osu.Game @@ -488,6 +489,8 @@ namespace osu.Game protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); + protected virtual HighPerformanceSession CreateHighPerformanceSession() => new HighPerformanceSession(); + protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); #region Beatmap progression @@ -756,6 +759,8 @@ namespace osu.Game loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true); + loadComponentSingleFile(CreateHighPerformanceSession(), Add); + chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; Add(difficultyRecommender); diff --git a/osu.Game/Performance/HighPerformanceSession.cs b/osu.Game/Performance/HighPerformanceSession.cs new file mode 100644 index 0000000000..96e67669c5 --- /dev/null +++ b/osu.Game/Performance/HighPerformanceSession.cs @@ -0,0 +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 System.Runtime; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; + +namespace osu.Game.Performance +{ + public class HighPerformanceSession : Component + { + private readonly IBindable localUserPlaying = new Bindable(); + private GCLatencyMode originalGCMode; + + [BackgroundDependencyLoader] + private void load(OsuGame game) + { + localUserPlaying.BindTo(game.LocalUserPlaying); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + localUserPlaying.BindValueChanged(playing => + { + if (playing.NewValue) + EnableHighPerformanceSession(); + else + DisableHighPerformanceSession(); + }, true); + } + + protected virtual void EnableHighPerformanceSession() + { + originalGCMode = GCSettings.LatencyMode; + GCSettings.LatencyMode = GCLatencyMode.LowLatency; + } + + protected virtual void DisableHighPerformanceSession() + { + if (GCSettings.LatencyMode == GCLatencyMode.LowLatency) + GCSettings.LatencyMode = originalGCMode; + } + } +} From 90a13b8ed3dc203f2f585457fc8f2f66f563ece7 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 22:05:23 +0900 Subject: [PATCH 394/670] Use `IApplicableToDrawableHitObject` for `OsuModApproachDifferent` Replacing the obsolete interface. --- .../Mods/OsuModApproachDifferent.cs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index 3e638c4833..074fb7dbed 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.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.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; @@ -13,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObjects + public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject { public override string Name => "Approach Different"; public override string Acronym => "AD"; @@ -32,22 +30,19 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Style", "Change the animation style of the approach circles.", 1)] public Bindable Style { get; } = new Bindable(); - public void ApplyToDrawableHitObjects(IEnumerable drawables) + public void ApplyToDrawableHitObject(DrawableHitObject drawable) { - drawables.ForEach(drawable => + drawable.ApplyCustomUpdateState += (drawableObject, state) => { - drawable.ApplyCustomUpdateState += (drawableObject, state) => - { - if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return; + if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return; - var hitCircle = drawableHitCircle.HitObject; + var hitCircle = drawableHitCircle.HitObject; - drawableHitCircle.ApproachCircle.ClearTransforms(targetMember: nameof(Scale)); + drawableHitCircle.ApproachCircle.ClearTransforms(targetMember: nameof(Scale)); - using (drawableHitCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt)) - drawableHitCircle.ApproachCircle.ScaleTo(Scale.Value).ScaleTo(1f, hitCircle.TimePreempt, getEasing(Style.Value)); - }; - }); + using (drawableHitCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt)) + drawableHitCircle.ApproachCircle.ScaleTo(Scale.Value).ScaleTo(1f, hitCircle.TimePreempt, getEasing(Style.Value)); + }; } private Easing getEasing(AnimationStyle style) From 5ebf570ec4cdde860928b5c42da17695e3185d49 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 16 Jun 2021 16:48:30 +0300 Subject: [PATCH 395/670] Revert `GetRulesetTransformedSkin` accessibility change This reverts commit 521077b7148eb896792ed5c1a451074ab8957746. Forgot to do it when I made this `protected`, but subclasses in test scenes require this. --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 10 +++++----- osu.Game/Tests/Visual/TestPlayer.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 1953bd499b..8a27899e89 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Skinning Ruleset = ruleset; Beatmap = beatmap; - InternalChild = new BeatmapSkinProvidingContainer(getRulesetTransformedSkin(beatmapSkin)) + InternalChild = new BeatmapSkinProvidingContainer(GetRulesetTransformedSkin(beatmapSkin)) { Child = Content = new Container { @@ -54,17 +54,17 @@ namespace osu.Game.Skinning { SkinSources.Clear(); - SkinSources.Add(getRulesetTransformedSkin(skinManager.CurrentSkin.Value)); + SkinSources.Add(GetRulesetTransformedSkin(skinManager.CurrentSkin.Value)); // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. if (skinManager.CurrentSkin.Value is LegacySkin && skinManager.CurrentSkin.Value != skinManager.DefaultLegacySkin) - SkinSources.Add(getRulesetTransformedSkin(skinManager.DefaultLegacySkin)); + SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultLegacySkin)); if (skinManager.CurrentSkin.Value != skinManager.DefaultSkin) - SkinSources.Add(getRulesetTransformedSkin(skinManager.DefaultSkin)); + SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultSkin)); } - private ISkin getRulesetTransformedSkin(ISkin skin) + protected ISkin GetRulesetTransformedSkin(ISkin skin) { if (skin == null) return null; diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index eecf8a2f6e..e1431b0658 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual base.UpdateSkins(); if (skin != null) - SkinSources.Insert(0, Ruleset.CreateLegacySkinProvider(skin, Beatmap)); + SkinSources.Insert(0, GetRulesetTransformedSkin(skin)); } } } From 4b926791b58344f6018941ac6463849bc742bdb5 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Wed, 16 Jun 2021 21:13:01 +0700 Subject: [PATCH 396/670] add inter font --- osu.Game/OsuGameBase.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 9c3adba342..abf8fbc4fb 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -195,6 +195,15 @@ namespace osu.Game AddFont(Resources, @"Fonts/Torus-SemiBold"); AddFont(Resources, @"Fonts/Torus-Bold"); + AddFont(Resources, @"Fonts/Inter-Regular"); + AddFont(Resources, @"Fonts/Inter-RegularItalic"); + AddFont(Resources, @"Fonts/Inter-Light"); + AddFont(Resources, @"Fonts/Inter-LightItalic"); + AddFont(Resources, @"Fonts/Inter-SemiBold"); + AddFont(Resources, @"Fonts/Inter-SemiBoldItalic"); + AddFont(Resources, @"Fonts/Inter-Bold"); + AddFont(Resources, @"Fonts/Inter-BoldItalic"); + AddFont(Resources, @"Fonts/Noto-Basic"); AddFont(Resources, @"Fonts/Noto-Hangul"); AddFont(Resources, @"Fonts/Noto-CJK-Basic"); From 0e9ca3df3c5b8a54d9808711bacc926e1e832183 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Wed, 16 Jun 2021 21:14:48 +0700 Subject: [PATCH 397/670] add remaining non-rtl language --- osu.Game/Localisation/Language.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index 96bfde8596..65541feedf 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.cs @@ -14,9 +14,8 @@ namespace osu.Game.Localisation // [Description(@"اَلْعَرَبِيَّةُ")] // ar, - // TODO: Some accented glyphs are missing. Revisit when adding Inter. - // [Description(@"Беларуская мова")] - // be, + [Description(@"Беларуская мова")] + be, [Description(@"Български")] bg, @@ -30,9 +29,8 @@ namespace osu.Game.Localisation [Description(@"Deutsch")] de, - // TODO: Some accented glyphs are missing. Revisit when adding Inter. - // [Description(@"Ελληνικά")] - // el, + [Description(@"Ελληνικά")] + el, [Description(@"español")] es, @@ -94,9 +92,8 @@ namespace osu.Game.Localisation [Description(@"Türkçe")] tr, - // TODO: Some accented glyphs are missing. Revisit when adding Inter. - // [Description(@"Українська мова")] - // uk, + [Description(@"Українська мова")] + uk, [Description(@"Tiếng Việt")] vi, From 52ddf08532fd4e31d042ec8a06d226814b769c52 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 16 Jun 2021 16:51:20 +0300 Subject: [PATCH 398/670] Consider not adding legacy skin transformers to non-legacy skins --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 8a27899e89..13664897ac 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -12,7 +12,7 @@ namespace osu.Game.Skinning { /// /// A type of that provides access to the beatmap skin and user skin, - /// each transformed with the ruleset's own skin transformer individually. + /// with each legacy skin source transformed with the ruleset's legacy skin transformer. /// public class RulesetSkinProvidingContainer : SkinProvidingContainer { @@ -66,7 +66,7 @@ namespace osu.Game.Skinning protected ISkin GetRulesetTransformedSkin(ISkin skin) { - if (skin == null) + if (!(skin is LegacySkin)) return null; var rulesetTransformed = Ruleset.CreateLegacySkinProvider(skin, Beatmap); From 74ad6f9117f12c834a96a296c026824f002e3e17 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 16 Jun 2021 17:24:30 +0300 Subject: [PATCH 399/670] Remove default skin from the ruleset skin sources That one doesn't need any changes to it, can be fetched from the `SkinManager` instead. --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 13664897ac..621e80ceb5 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -59,9 +59,6 @@ namespace osu.Game.Skinning // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. if (skinManager.CurrentSkin.Value is LegacySkin && skinManager.CurrentSkin.Value != skinManager.DefaultLegacySkin) SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultLegacySkin)); - - if (skinManager.CurrentSkin.Value != skinManager.DefaultSkin) - SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultSkin)); } protected ISkin GetRulesetTransformedSkin(ISkin skin) From 780388d174c63cb711d31377aaa521a4a3e29eb8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Jun 2021 03:48:25 +0300 Subject: [PATCH 400/670] Fix incorrect return value --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 621e80ceb5..f11acd981a 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Skinning protected ISkin GetRulesetTransformedSkin(ISkin skin) { if (!(skin is LegacySkin)) - return null; + return skin; var rulesetTransformed = Ruleset.CreateLegacySkinProvider(skin, Beatmap); if (rulesetTransformed != null) From 9dcd0bf311a7a869256cf3bd54965b9664b044fd Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 17 Jun 2021 10:07:52 +0900 Subject: [PATCH 401/670] Remove `IPlayfieldProvider` by caching `Playfield` --- .../Editor/ManiaSelectionBlueprintTestScene.cs | 5 ++--- .../Edit/Blueprints/ManiaSelectionBlueprint.cs | 5 +++-- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 11 +++++++++-- osu.Game/Rulesets/Edit/IPlayfieldProvider.cs | 12 ------------ 4 files changed, 14 insertions(+), 19 deletions(-) delete mode 100644 osu.Game/Rulesets/Edit/IPlayfieldProvider.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs index e5abbc7246..124e1a35f9 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI; @@ -14,13 +13,13 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests.Editor { - [Cached(typeof(IPlayfieldProvider))] - public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene, IPlayfieldProvider + public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene { protected override Container Content => blueprints ?? base.Content; private readonly Container blueprints; + [Cached(typeof(Playfield))] public Playfield Playfield { get; } private readonly ScrollingTestContainer scrollingTestContainer; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index 1b5cb03204..955336db57 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.Edit.Blueprints @@ -14,12 +15,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints where T : ManiaHitObject { [Resolved] - private IPlayfieldProvider playfieldProvider { get; set; } + private Playfield playfield { get; set; } [Resolved] private IScrollingInfo scrollingInfo { get; set; } - protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfieldProvider.Playfield).GetColumn(HitObject.Column).HitObjectContainer; + protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer; protected ManiaSelectionBlueprint(T hitObject) : base(hitObject) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 67c18b7e3c..a7005954b2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -43,6 +43,9 @@ namespace osu.Game.Rulesets.Edit protected readonly Ruleset Ruleset; + // Provides `Playfield` + private DependencyContainer dependencies; + [Resolved] protected EditorClock EditorClock { get; private set; } @@ -69,6 +72,9 @@ namespace osu.Game.Rulesets.Edit Ruleset = ruleset; } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => + dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + [BackgroundDependencyLoader] private void load() { @@ -88,6 +94,8 @@ namespace osu.Game.Rulesets.Edit return; } + dependencies.CacheAs(Playfield); + const float toolbar_width = 200; InternalChildren = new Drawable[] @@ -415,8 +423,7 @@ namespace osu.Game.Rulesets.Edit /// [Cached(typeof(HitObjectComposer))] [Cached(typeof(IPositionSnapProvider))] - [Cached(typeof(IPlayfieldProvider))] - public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider, IPlayfieldProvider + public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider { protected HitObjectComposer() { diff --git a/osu.Game/Rulesets/Edit/IPlayfieldProvider.cs b/osu.Game/Rulesets/Edit/IPlayfieldProvider.cs deleted file mode 100644 index 4bfd4d2728..0000000000 --- a/osu.Game/Rulesets/Edit/IPlayfieldProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.UI; - -namespace osu.Game.Rulesets.Edit -{ - public interface IPlayfieldProvider - { - Playfield Playfield { get; } - } -} From a4f362dca64bcae5db9eab7e45c49d4ea887ae49 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 17 Jun 2021 10:11:58 +0900 Subject: [PATCH 402/670] Remove lifetime override of `DrawableManiaHitObject` The `AlwaysAlive` logic is now in all DHOs and it is now not necessary (and potentially conflicting). --- .../Drawables/DrawableManiaHitObject.cs | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 380ab35339..3ec68bfb56 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -85,63 +85,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables AccentColour.UnbindFrom(ParentHitObject.AccentColour); } - private double computedLifetimeStart; - - public override double LifetimeStart - { - get => base.LifetimeStart; - set - { - computedLifetimeStart = value; - - if (!AlwaysAlive) - base.LifetimeStart = value; - } - } - - private double computedLifetimeEnd; - - public override double LifetimeEnd - { - get => base.LifetimeEnd; - set - { - computedLifetimeEnd = value; - - if (!AlwaysAlive) - base.LifetimeEnd = value; - } - } - - private bool alwaysAlive; - - /// - /// Whether this should always remain alive. - /// - internal bool AlwaysAlive - { - get => alwaysAlive; - set - { - if (alwaysAlive == value) - return; - - alwaysAlive = value; - - if (value) - { - // Set the base lifetimes directly, to avoid mangling the computed lifetimes - base.LifetimeStart = double.MinValue; - base.LifetimeEnd = double.MaxValue; - } - else - { - LifetimeStart = computedLifetimeStart; - LifetimeEnd = computedLifetimeEnd; - } - } - } - protected virtual void OnDirectionChanged(ValueChangedEvent e) { Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; From e7954ecb606e53e88063a227168d2d3fc5623ab6 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 17 Jun 2021 10:31:20 +0900 Subject: [PATCH 403/670] Use property instead of backing field consistently --- .../Objects/Pooling/PoolableDrawableWithLifetime.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index d5d1a7b55c..3ab85aa214 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -132,11 +132,11 @@ namespace osu.Game.Rulesets.Objects.Pooling private void free() { - Debug.Assert(entry != null && HasEntryApplied); + Debug.Assert(Entry != null && HasEntryApplied); - OnFree(entry); + OnFree(Entry); - entry.LifetimeChanged -= setLifetimeFromEntry; + Entry.LifetimeChanged -= setLifetimeFromEntry; entry = null; base.LifetimeStart = double.MinValue; base.LifetimeEnd = double.MaxValue; From 9d9892e99ed8290f0b5ad8d82e800efa84935473 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Jun 2021 04:38:55 +0300 Subject: [PATCH 404/670] Add legacy spinner approach circle implementation --- .../Skinning/Legacy/LegacySpinner.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 959589620b..259f16ca5e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected DrawableSpinner DrawableSpinner { get; private set; } + private Drawable approachCircle; + private Sprite spin; private Sprite clear; @@ -57,8 +59,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Depth = float.MinValue, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { + approachCircle = getSpinnerApproachCircle(source), spin = new Sprite { Anchor = Anchor.TopCentre, @@ -101,6 +104,25 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy }.With(s => s.Font = s.Font.With(fixedWidth: false)), } }); + + static Drawable getSpinnerApproachCircle(ISkinSource source) + { + var spinnerProvider = source.FindProvider(s => + s.GetTexture("spinner-circle") != null || + s.GetTexture("spinner-top") != null); + + if (spinnerProvider is DefaultLegacySkin) + return Empty(); + + return new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-approachcircle"), + Scale = new Vector2(SPRITE_SCALE * 1.86f), + Y = SPINNER_Y_CENTRE, + }; + } } private IBindable gainedBonus; @@ -175,6 +197,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); } + using (BeginAbsoluteSequence(d.HitObject.StartTime)) + approachCircle.ScaleTo(SPRITE_SCALE * 1.86f).ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration); + double spinFadeOutLength = Math.Min(400, d.HitObject.Duration); using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true)) From 249ae3141edb47ba78b1a994b53652015bfd3637 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 15:06:56 +0900 Subject: [PATCH 405/670] Add early/late tests for hit circles --- .../TestSceneHitCircle.cs | 28 ++++++++++++++----- .../TestSceneHitCircleComboChange.cs | 4 +-- .../TestSceneShaking.cs | 4 +-- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 58e46b6687..0fdf30d9f9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -37,6 +37,18 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true))); } + [Test] + public void TestHittingEarly() + { + AddStep("Hit stream early", () => SetContents(_ => testStream(5, true, -150))); + } + + [Test] + public void TestHittingLate() + { + AddStep("Hit stream late", () => SetContents(_ => testStream(5, true, 150))); + } + private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) { var drawable = createSingle(circleSize, auto, timeOffset, positionOffset); @@ -46,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Tests return playfield; } - private Drawable testStream(float circleSize, bool auto = false) + private Drawable testStream(float circleSize, bool auto = false, double hitOffset = 0) { var playfield = new TestOsuPlayfield(); @@ -54,14 +66,14 @@ namespace osu.Game.Rulesets.Osu.Tests for (int i = 0; i <= 1000; i += 100) { - playfield.Add(createSingle(circleSize, auto, i, pos)); + playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset)); pos.X += 50; } return playfield; } - private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset) + private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0) { positionOffset ??= Vector2.Zero; @@ -73,14 +85,14 @@ namespace osu.Game.Rulesets.Osu.Tests circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); - var drawable = CreateDrawableHitCircle(circle, auto); + var drawable = CreateDrawableHitCircle(circle, auto, hitOffset); foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToDrawableHitObjects(new[] { drawable }); return drawable; } - protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto) + protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0) => new TestDrawableHitCircle(circle, auto, hitOffset) { Depth = depthIndex++ }; @@ -88,18 +100,20 @@ namespace osu.Game.Rulesets.Osu.Tests protected class TestDrawableHitCircle : DrawableHitCircle { private readonly bool auto; + private readonly double hitOffset; - public TestDrawableHitCircle(HitCircle h, bool auto) + public TestDrawableHitCircle(HitCircle h, bool auto, double hitOffset) : base(h) { this.auto = auto; + this.hitOffset = hitOffset; } public void TriggerJudgement() => UpdateResult(true); protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (auto && !userTriggered && timeOffset > 0) + if (auto && !userTriggered && timeOffset > hitOffset) { // force success ApplyResult(r => r.Type = HitResult.Great); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs index 5695462859..ff600172d2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleComboChange.cs @@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Osu.Tests Scheduler.AddDelayed(() => comboIndex.Value++, 250, true); } - protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) + protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0) { circle.ComboIndexBindable.BindTo(comboIndex); circle.IndexInCurrentComboBindable.BindTo(comboIndex); - return base.CreateDrawableHitCircle(circle, auto); + return base.CreateDrawableHitCircle(circle, auto, hitOffset); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs index 7e973d0971..43900c9a5c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs @@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Osu.Tests return base.CreateBeatmapForSkinProvider(); } - protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) + protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0) { - var drawableHitObject = base.CreateDrawableHitCircle(circle, auto); + var drawableHitObject = base.CreateDrawableHitCircle(circle, auto, hitOffset); Debug.Assert(drawableHitObject.HitObject.HitWindows != null); From 6d0b3efa239db3013368e3df6e3a9c482397772c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 15:08:09 +0900 Subject: [PATCH 406/670] Reorganise existing tests into hits and misses --- .../TestSceneHitCircle.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 0fdf30d9f9..073e0540e5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -21,17 +21,11 @@ namespace osu.Game.Rulesets.Osu.Tests private int depthIndex; [Test] - public void TestVariousHitCircles() + public void TestHits() { - AddStep("Miss Big Single", () => SetContents(_ => testSingle(2))); - AddStep("Miss Medium Single", () => SetContents(_ => testSingle(5))); - AddStep("Miss Small Single", () => SetContents(_ => testSingle(7))); AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true))); AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true))); AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true))); - AddStep("Miss Big Stream", () => SetContents(_ => testStream(2))); - AddStep("Miss Medium Stream", () => SetContents(_ => testStream(5))); - AddStep("Miss Small Stream", () => SetContents(_ => testStream(7))); AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true))); AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true))); AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true))); @@ -43,6 +37,17 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Hit stream early", () => SetContents(_ => testStream(5, true, -150))); } + [Test] + public void TestMisses() + { + AddStep("Miss Big Single", () => SetContents(_ => testSingle(2))); + AddStep("Miss Medium Single", () => SetContents(_ => testSingle(5))); + AddStep("Miss Small Single", () => SetContents(_ => testSingle(7))); + AddStep("Miss Big Stream", () => SetContents(_ => testStream(2))); + AddStep("Miss Medium Stream", () => SetContents(_ => testStream(5))); + AddStep("Miss Small Stream", () => SetContents(_ => testStream(7))); + } + [Test] public void TestHittingLate() { From a46f730a6943cef55afc7e987ce0522367a6b6d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 15:08:22 +0900 Subject: [PATCH 407/670] Fix approach circle fade not running early on an early user hit Regressed in https://github.com/ppy/osu/pull/12153. Closes https://github.com/ppy/osu/issues/13531. --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 236af4b3f1..ca2e6578db 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -172,6 +172,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateStartTimeStateTransforms(); + // always fade out at the circle's start time (to match user expectations). ApproachCircle.FadeOut(50); } @@ -182,6 +183,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // todo: temporary / arbitrary, used for lifetime optimisation. this.Delay(800).FadeOut(); + // in the case of an early state change, the fade should be expedited to the current point in time. + if (HitStateUpdateTime < HitObject.StartTime) + ApproachCircle.FadeOut(50); + switch (state) { case ArmedState.Idle: From 8da0431fa076307fcaad9443304a98ef83bdb7cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 16:05:50 +0900 Subject: [PATCH 408/670] Improve code quality of `AuthorInfo` --- osu.Game/Overlays/BeatmapSet/AuthorInfo.cs | 70 +++++++++++----------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index 1ffcf9722a..971b34fcc4 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -22,8 +23,8 @@ namespace osu.Game.Overlays.BeatmapSet { private const float height = 50; - private readonly UpdateableAvatar avatar; - private readonly FillFlowContainer fields; + private UpdateableAvatar avatar; + private FillFlowContainer fields; private BeatmapSetInfo beatmapSet; @@ -35,41 +36,12 @@ namespace osu.Game.Overlays.BeatmapSet if (value == beatmapSet) return; beatmapSet = value; - - updateDisplay(); + Scheduler.AddOnce(updateDisplay); } } - private void updateDisplay() - { - avatar.User = BeatmapSet?.Metadata.Author; - - fields.Clear(); - if (BeatmapSet == null) - return; - - var online = BeatmapSet.OnlineInfo; - - fields.Children = new Drawable[] - { - new Field("mapped by", BeatmapSet.Metadata.Author, OsuFont.GetFont(weight: FontWeight.Regular, italics: true)), - new Field("submitted", online.Submitted, OsuFont.GetFont(weight: FontWeight.Bold)) - { - Margin = new MarginPadding { Top = 5 }, - }, - }; - - if (online.Ranked.HasValue) - { - fields.Add(new Field(online.Status.ToString().ToLowerInvariant(), online.Ranked.Value, OsuFont.GetFont(weight: FontWeight.Bold))); - } - else if (online.LastUpdated.HasValue) - { - fields.Add(new Field("last updated", online.LastUpdated.Value, OsuFont.GetFont(weight: FontWeight.Bold))); - } - } - - public AuthorInfo() + [BackgroundDependencyLoader] + private void load() { RelativeSizeAxes = Axes.X; Height = height; @@ -101,11 +73,37 @@ namespace osu.Game.Overlays.BeatmapSet Padding = new MarginPadding { Left = height + 5 }, }, }; + + Scheduler.AddOnce(updateDisplay); } - private void load() + private void updateDisplay() { - updateDisplay(); + avatar.User = BeatmapSet?.Metadata.Author; + + fields.Clear(); + if (BeatmapSet == null) + return; + + var online = BeatmapSet.OnlineInfo; + + fields.Children = new Drawable[] + { + new Field("mapped by", BeatmapSet.Metadata.Author, OsuFont.GetFont(weight: FontWeight.Regular, italics: true)), + new Field("submitted", online.Submitted, OsuFont.GetFont(weight: FontWeight.Bold)) + { + Margin = new MarginPadding { Top = 5 }, + }, + }; + + if (online.Ranked.HasValue) + { + fields.Add(new Field(online.Status.ToString().ToLowerInvariant(), online.Ranked.Value, OsuFont.GetFont(weight: FontWeight.Bold))); + } + else if (online.LastUpdated.HasValue) + { + fields.Add(new Field("last updated", online.LastUpdated.Value, OsuFont.GetFont(weight: FontWeight.Bold))); + } } private class Field : FillFlowContainer From 9495f87f0462f5279f1ba43ff51fc3aee22db272 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 16:07:19 +0900 Subject: [PATCH 409/670] Remove redundant `NotNull` attributes in `nullable` classes --- osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs | 3 --- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 2 -- osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs | 3 +-- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 7d6c76bc2f..ee72df4c10 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using MessagePack; using osu.Game.Online.API; @@ -28,11 +27,9 @@ namespace osu.Game.Online.Multiplayer [Key(3)] public string Name { get; set; } = "Unnamed room"; - [NotNull] [Key(4)] public IEnumerable RequiredMods { get; set; } = Enumerable.Empty(); - [NotNull] [Key(5)] public IEnumerable AllowedMods { get; set; } = Enumerable.Empty(); diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index c654127b94..a49a8f083c 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using MessagePack; using Newtonsoft.Json; using osu.Game.Online.API; @@ -35,7 +34,6 @@ namespace osu.Game.Online.Multiplayer /// Any mods applicable only to the local user. /// [Key(3)] - [NotNull] public IEnumerable Mods { get; set; } = Enumerable.Empty(); [IgnoreMember] diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index bc8994bbe5..d3ee10dd23 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Game.Input.Handlers; using osu.Game.Replays; @@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Replays } } - protected virtual bool IsImportant([NotNull] TFrame frame) => false; + protected virtual bool IsImportant(TFrame frame) => false; /// /// Update the current frame based on an incoming time value. From ccba6b5ac29045f41b84bec840a947c3f677bd64 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 17 Jun 2021 16:13:47 +0900 Subject: [PATCH 410/670] Fix test failures --- osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 073e0540e5..cf8b510ab6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -114,11 +114,11 @@ namespace osu.Game.Rulesets.Osu.Tests this.hitOffset = hitOffset; } - public void TriggerJudgement() => UpdateResult(true); + public void TriggerJudgement() => Schedule(() => UpdateResult(true)); protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (auto && !userTriggered && timeOffset > hitOffset) + if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false) { // force success ApplyResult(r => r.Type = HitResult.Great); From d9cc1c227b0a3112ee0ad8683bfc1841ebef5d00 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 16:25:55 +0900 Subject: [PATCH 411/670] Allow UpdateableAvatar to handle displaying username as tooltip --- .../Chat/Tabs/PrivateChannelTabItem.cs | 2 +- .../Profile/Header/TopHeaderContainer.cs | 3 +- .../Overlays/Toolbar/ToolbarUserButton.cs | 3 +- osu.Game/Users/Drawables/ClickableAvatar.cs | 41 +++++++++++++------ osu.Game/Users/Drawables/UpdateableAvatar.cs | 28 +++++++------ osu.Game/Users/ExtendedUserPanel.cs | 6 +-- 6 files changed, 48 insertions(+), 35 deletions(-) diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs index 00f46b0035..7c82420e08 100644 --- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Chat.Tabs Child = new DelayedLoadWrapper(avatar = new ClickableAvatar(value.Users.First()) { RelativeSizeAxes = Axes.Both, - OpenOnClick = { Value = false }, + OpenOnClick = false, }) { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index e0642d650c..419ae96738 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -58,12 +58,11 @@ namespace osu.Game.Overlays.Profile.Header Origin = Anchor.CentreLeft, Children = new Drawable[] { - avatar = new UpdateableAvatar + avatar = new UpdateableAvatar(openOnClick: false) { Size = new Vector2(avatar_size), Masking = true, CornerRadius = avatar_size * 0.25f, - OpenOnClick = { Value = false }, ShowGuestOnNull = false, }, new Container diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index db4e491d9a..165c095514 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -32,14 +32,13 @@ namespace osu.Game.Overlays.Toolbar Add(new OpaqueBackground { Depth = 1 }); - Flow.Add(avatar = new UpdateableAvatar + Flow.Add(avatar = new UpdateableAvatar(openOnClick: false) { Masking = true, Size = new Vector2(32), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, CornerRadius = 4, - OpenOnClick = { Value = false }, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 0fca9c7c9b..c3bf740108 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; @@ -13,16 +12,32 @@ namespace osu.Game.Users.Drawables { public class ClickableAvatar : Container { + private const string default_tooltip_text = "view profile"; + /// /// Whether to open the user's profile when clicked. /// - public readonly BindableBool OpenOnClick = new BindableBool(true); + public bool OpenOnClick + { + set => clickableArea.Enabled.Value = value; + } + + /// + /// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username. + /// Setting this to true exposes the username via tooltip for special cases where this is not true. + /// + public bool ShowUsernameTooltip + { + set => clickableArea.TooltipText = value ? (user?.Username ?? string.Empty) : default_tooltip_text; + } private readonly User user; [Resolved(CanBeNull = true)] private OsuGame game { get; set; } + private readonly ClickableArea clickableArea; + /// /// A clickable avatar for the specified user, with UI sounds included. /// If is true, clicking will open the user's profile. @@ -31,35 +46,35 @@ namespace osu.Game.Users.Drawables public ClickableAvatar(User user = null) { this.user = user; - } - [BackgroundDependencyLoader] - private void load(LargeTextureStore textures) - { - ClickableArea clickableArea; Add(clickableArea = new ClickableArea { RelativeSizeAxes = Axes.Both, Action = openProfile }); + } + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add); - - clickableArea.Enabled.BindTo(OpenOnClick); } private void openProfile() { - if (!OpenOnClick.Value) - return; - if (user?.Id > 1) game?.ShowUser(user.Id); } private class ClickableArea : OsuClickableContainer { - public override string TooltipText => Enabled.Value ? @"view profile" : null; + private string tooltip = default_tooltip_text; + + public override string TooltipText + { + get => Enabled.Value ? tooltip : null; + set => tooltip = value; + } protected override bool OnClick(ClickEvent e) { diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 927e48cb56..df724404e9 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -45,33 +44,38 @@ namespace osu.Game.Users.Drawables protected override double LoadDelay => 200; - /// - /// Whether to show a default guest representation on null user (as opposed to nothing). - /// - public bool ShowGuestOnNull = true; + private readonly bool openOnClick; + private readonly bool showUsernameTooltip; + private readonly bool showGuestOnNull; /// - /// Whether to open the user's profile when clicked. + /// Construct a new UpdateableAvatar. /// - public readonly BindableBool OpenOnClick = new BindableBool(true); - - public UpdateableAvatar(User user = null) + /// The initial user to display. + /// Whether to open the user's profile when clicked. + /// Whether to show the username rather than "view profile" on the tooltip. + /// Whether to show a default guest representation on null user (as opposed to nothing). + public UpdateableAvatar(User user = null, bool openOnClick = true, bool showUsernameTooltip = false, bool showGuestOnNull = true) { + this.openOnClick = openOnClick; + this.showUsernameTooltip = showUsernameTooltip; + this.showGuestOnNull = showGuestOnNull; + User = user; } protected override Drawable CreateDrawable(User user) { - if (user == null && !ShowGuestOnNull) + if (user == null && !showGuestOnNull) return null; var avatar = new ClickableAvatar(user) { + OpenOnClick = openOnClick, + ShowUsernameTooltip = showUsernameTooltip, RelativeSizeAxes = Axes.Both, }; - avatar.OpenOnClick.BindTo(OpenOnClick); - return avatar; } } diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index 2604815751..24317e6069 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -48,11 +48,7 @@ namespace osu.Game.Users statusIcon.FinishTransforms(); } - protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar - { - User = User, - OpenOnClick = { Value = false } - }; + protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false); protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) { From 808b2baa41e47dd4751affb7c1d4ee80d92ea23a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 16:26:07 +0900 Subject: [PATCH 412/670] Consume new behaviour to fix `UserTile` discrepancy Closes https://github.com/ppy/osu/issues/13522. --- osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs index 9aceb39a27..e531ddb0ec 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; using osu.Game.Users; @@ -91,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay.Components }); } - private class UserTile : CompositeDrawable, IHasTooltip + private class UserTile : CompositeDrawable { public User User { @@ -99,8 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Components set => avatar.User = value; } - public string TooltipText => User?.Username ?? string.Empty; - private readonly UpdateableAvatar avatar; public UserTile() @@ -116,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Components RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex(@"27252d"), }, - avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both }, + avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both }, }; } } From a0e5301c9f6488138464146267771c4da385b87b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 16:33:43 +0900 Subject: [PATCH 413/670] Update usages of `showGuestOnNull` --- osu.Game/Overlays/BeatmapSet/AuthorInfo.cs | 3 +-- osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs | 3 +-- osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index 1ffcf9722a..7c71e457d3 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -81,9 +81,8 @@ namespace osu.Game.Overlays.BeatmapSet AutoSizeAxes = Axes.Both, CornerRadius = 4, Masking = true, - Child = avatar = new UpdateableAvatar + Child = avatar = new UpdateableAvatar(showGuestOnNull: false) { - ShowGuestOnNull = false, Size = new Vector2(height), }, EdgeEffect = new EdgeEffectParameters diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index 9111a0cfc7..736366fb5c 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, } }, - avatar = new UpdateableAvatar + avatar = new UpdateableAvatar(showGuestOnNull: false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -75,7 +75,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Offset = new Vector2(0, 2), Radius = 1, }, - ShowGuestOnNull = false, }, new FillFlowContainer { diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 419ae96738..d751424367 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -58,12 +58,11 @@ namespace osu.Game.Overlays.Profile.Header Origin = Anchor.CentreLeft, Children = new Drawable[] { - avatar = new UpdateableAvatar(openOnClick: false) + avatar = new UpdateableAvatar(openOnClick: false, showGuestOnNull: false) { Size = new Vector2(avatar_size), Masking = true, CornerRadius = avatar_size * 0.25f, - ShowGuestOnNull = false, }, new Container { From 7cbebe6d11f4766420d6d0e48fd63cadec90dd52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 18:10:59 +0900 Subject: [PATCH 414/670] Rewrite xmldoc and inline comments for `PerformExit` --- osu.Game/Screens/Play/Player.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f9036780aa..ae8993cf06 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -512,19 +512,23 @@ namespace osu.Game.Screens.Play } /// - /// Exits the . + /// Attempts to complete a user request to exit gameplay. /// + /// + /// - This should only be called in response to a user interaction. Exiting is not guaranteed. + /// - This will interrupt any pending progression to the results screen, even if the transition has begun. + /// /// /// Whether the pause or fail dialog should be shown before performing an exit. - /// If true and a dialog is not yet displayed, the exit will be blocked the the relevant dialog will display instead. + /// If true and a dialog is not yet displayed, the exit will be blocked the relevant dialog will display instead. /// protected void PerformExit(bool showDialogFirst) { - // if a restart has been requested, cancel any pending completion (user has shown intent to restart). + // if an exit has been requested, cancel any pending completion (the user has showing intention to exit). completionProgressDelegate?.Cancel(); - // there is a chance that the exit was performed after the transition to results has started. - // we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process). + // there is a chance that an exit request occurs after the transition to results has already started. + // even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process). if (!this.IsCurrentScreen()) { ValidForResume = false; @@ -547,7 +551,7 @@ namespace osu.Game.Screens.Play return; } - // there's a chance the pausing is not supported in the current state, at which point immediate exit should be preferred. + // even if this call has requested a dialog, there is a chance the current player mode doesn't support pausing. if (pausingSupportedByCurrentState) { // in the case a dialog needs to be shown, attempt to pause and show it. @@ -563,6 +567,10 @@ namespace osu.Game.Screens.Play } } + // The actual exit is performed if + // - the pause / fail dialog was not requested + // - the pause / fail dialog was requested but is already displayed (user showing intention to exit). + // - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance. this.Exit(); } From 246ab41cc68e361ea9ba71430dbfd8b776fcad54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 18:11:15 +0900 Subject: [PATCH 415/670] Remove special casing for user exit during storyboard outro --- .../Visual/Gameplay/TestSceneStoryboardWithOutro.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 5ef3eff856..3ed274690e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -66,12 +66,12 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestStoryboardExitToSkipOutro() + public void TestStoryboardExitDuringOutroStillExits() { CreateTest(null); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddStep("exit via pause", () => Player.ExitViaPause()); - AddAssert("score shown", () => Player.IsScoreShown); + AddAssert("player exited", () => !Player.IsCurrentScreen() && Player.GetChildScreen() == null); } [TestCase(false)] diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ae8993cf06..b8d2b42992 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -559,12 +559,6 @@ namespace osu.Game.Screens.Play Pause(); return; } - - // if the score is ready for display but results screen has not been pushed yet (e.g. storyboard is still playing beyond gameplay), then transition to results screen instead of exiting. - if (prepareScoreForDisplayTask != null && completionProgressDelegate == null) - { - updateCompletionState(true); - } } // The actual exit is performed if From d03c6da60c9aad1477c5cc0fb6ffd127679cb86c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Jun 2021 19:13:34 +0900 Subject: [PATCH 416/670] Refactor and redocument `updateCompletionState()` and surrounding methods --- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 4 +- osu.Game/Screens/Play/Player.cs | 122 +++++++++++------- 2 files changed, 80 insertions(+), 46 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index a2ef715367..cc1fb0b321 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -54,9 +54,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); } - protected override void PrepareScoreForResults() + protected override void PrepareScoreForResults(Score score) { - base.PrepareScoreForResults(); + base.PrepareScoreForResults(score); Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b8d2b42992..6ef54db4ef 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -181,12 +181,6 @@ namespace osu.Game.Screens.Play DrawableRuleset.SetRecordTarget(Score); } - protected virtual void PrepareScoreForResults() - { - // perform one final population to ensure everything is up-to-date. - ScoreProcessor.PopulateScore(Score.ScoreInfo); - } - [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) { @@ -301,7 +295,7 @@ namespace osu.Game.Screens.Play DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded => { - if (storyboardEnded.NewValue && completionProgressDelegate == null) + if (storyboardEnded.NewValue && resultsDisplayDelegate == null) updateCompletionState(); }; @@ -525,7 +519,7 @@ namespace osu.Game.Screens.Play protected void PerformExit(bool showDialogFirst) { // if an exit has been requested, cancel any pending completion (the user has showing intention to exit). - completionProgressDelegate?.Cancel(); + resultsDisplayDelegate?.Cancel(); // there is a chance that an exit request occurs after the transition to results has already started. // even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process). @@ -628,7 +622,20 @@ namespace osu.Game.Screens.Play PerformExit(false); } - private ScheduledDelegate completionProgressDelegate; + /// + /// This delegate, when set, means the results screen has been queued to appear. + /// The display of the results screen may be delayed by any work being done in and . + /// + /// + /// Once set, this can *only* be cancelled by rewinding, ie. if ScoreProcessor.HasCompleted becomes false. + /// Even if the user requests an exit, it will forcefully proceed to the results screen (see special case in ). + /// + private ScheduledDelegate resultsDisplayDelegate; + + /// + /// A task which asynchronously prepares a completed score for display at results. + /// This may include performing net requests or importing the score into the database, generally to ensure things are in a sane state for the play session. + /// private Task prepareScoreForDisplayTask; /// @@ -638,57 +645,44 @@ namespace osu.Game.Screens.Play /// Thrown if this method is called more than once without changing state. private void updateCompletionState(bool skipStoryboardOutro = false) { - // screen may be in the exiting transition phase. + // If this player instance is already exiting upwards, don't attempt any kind of forward progress. if (!this.IsCurrentScreen()) return; + // Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled. + // TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar. + // Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run). + // In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done, + // but it still doesn't feel right that this exists here. if (!ScoreProcessor.HasCompleted.Value) { - completionProgressDelegate?.Cancel(); - completionProgressDelegate = null; + resultsDisplayDelegate?.Cancel(); + resultsDisplayDelegate = null; + ValidForResume = true; skipOutroOverlay.Hide(); return; } - if (completionProgressDelegate != null) - throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once"); + if (resultsDisplayDelegate != null) + throw new InvalidOperationException(@$"{nameof(updateCompletionState)} should never be fired more than once."); // Only show the completion screen if the player hasn't failed if (HealthProcessor.HasFailed) return; + // Setting this early in the process means that even if something were to go wrong in the order of events following, there + // is no chance that a user could return to the (already completed) Player instance from a child screen. ValidForResume = false; - // ensure we are not writing to the replay any more, as we are about to consume and store the score. + // Ensure we are not writing to the replay any more, as we are about to consume and store the score. DrawableRuleset.SetRecordTarget(null); - if (!Configuration.ShowResults) return; + // Asynchronously run score preparation operations (database import, online submission etc.). + prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults); - prepareScoreForDisplayTask ??= Task.Run(async () => - { - PrepareScoreForResults(); - - try - { - await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.Error(ex, "Score preparation failed!"); - } - - try - { - await ImportScore(Score).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.Error(ex, "Score import failed!"); - } - - return Score.ScoreInfo; - }); + if (!Configuration.ShowResults) + return; if (skipStoryboardOutro) { @@ -708,7 +702,33 @@ namespace osu.Game.Screens.Play scheduleCompletion(); } - private void scheduleCompletion() => completionProgressDelegate = Schedule(() => + private async Task prepareScoreForResults() + { + // ReSharper disable once MethodHasAsyncOverload + PrepareScoreForResults(Score); + + try + { + await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Error(ex, @"Score preparation failed!"); + } + + try + { + await ImportScore(Score).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Error(ex, @"Score import failed!"); + } + + return Score.ScoreInfo; + } + + private void scheduleCompletion() => resultsDisplayDelegate = Schedule(() => { if (!prepareScoreForDisplayTask.IsCompleted) { @@ -917,10 +937,11 @@ namespace osu.Game.Screens.Play { screenSuspension?.Expire(); - if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed) + // if the results screen is prepared to be displayed, forcefully show it on an exit request. + // usually if a user has completed a play session they do want to see results. and if they don't they can hit the same key a second time. + if (resultsDisplayDelegate != null && !resultsDisplayDelegate.Cancelled && !resultsDisplayDelegate.Completed) { - // proceed to result screen if beatmap already finished playing - completionProgressDelegate.RunTask(); + resultsDisplayDelegate.RunTask(); return true; } @@ -981,6 +1002,19 @@ namespace osu.Game.Screens.Play score.ScoreInfo.OnlineScoreID = onlineScoreId; } + /// + /// Prepare the for display at results. + /// + /// + /// This is run synchronously before is run. + /// + /// The to prepare. + protected virtual void PrepareScoreForResults(Score score) + { + // perform one final population to ensure everything is up-to-date. + ScoreProcessor.PopulateScore(score.ScoreInfo); + } + /// /// Prepare the for display at results. /// From 561dbea9e1c2ff77df25eee5499b62684b4ffa65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Jun 2021 15:26:50 +0200 Subject: [PATCH 417/670] Use xmldoc-specific syntax for nicer formatting --- osu.Game/Screens/Play/Player.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b8d2b42992..df3348c9d5 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -515,12 +515,14 @@ namespace osu.Game.Screens.Play /// Attempts to complete a user request to exit gameplay. /// /// - /// - This should only be called in response to a user interaction. Exiting is not guaranteed. - /// - This will interrupt any pending progression to the results screen, even if the transition has begun. + /// + /// This should only be called in response to a user interaction. Exiting is not guaranteed. + /// This will interrupt any pending progression to the results screen, even if the transition has begun. + /// /// /// /// Whether the pause or fail dialog should be shown before performing an exit. - /// If true and a dialog is not yet displayed, the exit will be blocked the relevant dialog will display instead. + /// If and a dialog is not yet displayed, the exit will be blocked the relevant dialog will display instead. /// protected void PerformExit(bool showDialogFirst) { From 10b0d066beb7e9e6fae56f10c7253739c6cecc86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Jun 2021 16:04:58 +0200 Subject: [PATCH 418/670] Reword comments slightly --- osu.Game/Screens/Play/Player.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index df3348c9d5..5ffe2bac49 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -522,11 +522,11 @@ namespace osu.Game.Screens.Play /// /// /// Whether the pause or fail dialog should be shown before performing an exit. - /// If and a dialog is not yet displayed, the exit will be blocked the relevant dialog will display instead. + /// If and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead. /// protected void PerformExit(bool showDialogFirst) { - // if an exit has been requested, cancel any pending completion (the user has showing intention to exit). + // if an exit has been requested, cancel any pending completion (the user has shown intention to exit). completionProgressDelegate?.Cancel(); // there is a chance that an exit request occurs after the transition to results has already started. From 9facfe89640fe31a14a7d119c84c153f37e70507 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 01:07:54 +0900 Subject: [PATCH 419/670] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Screens/Play/Player.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index fe5df0428e..d82450255d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -629,7 +629,7 @@ namespace osu.Game.Screens.Play /// The display of the results screen may be delayed by any work being done in and . /// /// - /// Once set, this can *only* be cancelled by rewinding, ie. if ScoreProcessor.HasCompleted becomes false. + /// Once set, this can *only* be cancelled by rewinding, ie. if ScoreProcessor.HasCompleted becomes . /// Even if the user requests an exit, it will forcefully proceed to the results screen (see special case in ). /// private ScheduledDelegate resultsDisplayDelegate; @@ -647,7 +647,7 @@ namespace osu.Game.Screens.Play /// Thrown if this method is called more than once without changing state. private void updateCompletionState(bool skipStoryboardOutro = false) { - // If this player instance is already exiting upwards, don't attempt any kind of forward progress. + // If this player instance is in the middle of an exit, don't attempt any kind of state update. if (!this.IsCurrentScreen()) return; From 3a1444e75d5e1455d72b6cd47100aa57ed7a9b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Jun 2021 19:02:56 +0200 Subject: [PATCH 420/670] Fix malformed xmldoc tag Oops. Made a typo in the PR suggestion. --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index d82450255d..aedb8d667e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -629,7 +629,7 @@ namespace osu.Game.Screens.Play /// The display of the results screen may be delayed by any work being done in and . /// /// - /// Once set, this can *only* be cancelled by rewinding, ie. if ScoreProcessor.HasCompleted becomes . + /// Once set, this can *only* be cancelled by rewinding, ie. if ScoreProcessor.HasCompleted becomes . /// Even if the user requests an exit, it will forcefully proceed to the results screen (see special case in ). /// private ScheduledDelegate resultsDisplayDelegate; From f282326f9a94faf41df5537ab4245dc66172f206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Jun 2021 19:04:52 +0200 Subject: [PATCH 421/670] Move score preparations back below `ShowResults` check --- osu.Game/Screens/Play/Player.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index aedb8d667e..9fe12c58de 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -680,12 +680,12 @@ namespace osu.Game.Screens.Play // Ensure we are not writing to the replay any more, as we are about to consume and store the score. DrawableRuleset.SetRecordTarget(null); - // Asynchronously run score preparation operations (database import, online submission etc.). - prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults); - if (!Configuration.ShowResults) return; + // Asynchronously run score preparation operations (database import, online submission etc.). + prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults); + if (skipStoryboardOutro) { scheduleCompletion(); From c9458fd9cec36e9d9407f4210fa39bf9bbef4ab1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Jun 2021 04:57:29 +0300 Subject: [PATCH 422/670] Hide spinner approach circle in "Hidden" mod --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 3 +++ osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 7 +++++++ osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 2752feb0a1..bec4c62e0f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -110,6 +110,9 @@ namespace osu.Game.Rulesets.Osu.Mods // hide elements we don't care about. // todo: hide background + using (spinner.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt)) + spinner.HideApproachCircle(); + using (spinner.BeginAbsoluteSequence(fadeStartTime)) spinner.FadeOut(fadeDuration); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 19cee61f26..942cc52c50 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; + internal readonly Bindable ApproachCircleVisibility = new Bindable(Visibility.Visible); + /// /// The amount of bonus score gained from spinning after the required number of spins, for display purposes. /// @@ -285,6 +287,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables updateBonusScore(); } + /// + /// Hides the spinner's approach circle if it has one. + /// + public void HideApproachCircle() => this.TransformBindableTo(ApproachCircleVisibility, Visibility.Hidden); + private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; private int wholeSpins; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 259f16ca5e..5b0c2d405b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -125,6 +125,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } } + private IBindable approachCircleVisibility; private IBindable gainedBonus; private IBindable spinsPerMinute; @@ -134,6 +135,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { base.LoadComplete(); + approachCircleVisibility = DrawableSpinner.ApproachCircleVisibility.GetBoundCopy(); + approachCircleVisibility.BindValueChanged(v => + { + approachCircle.Alpha = v.NewValue == Visibility.Hidden ? 0 : 1; + }, true); + gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy(); gainedBonus.BindValueChanged(bonus => { From 97cd1217a47985b1dd9052171905e5106a96a5fd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Jun 2021 13:06:13 +0900 Subject: [PATCH 423/670] Move IApplicableToDrawableHitObjects to its own file --- .../Mods/IApplicableToDrawableHitObject.cs | 11 ----------- .../Mods/IApplicableToDrawableHitObjects.cs | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs index 93055e733d..c8a9ff2f9a 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs @@ -1,9 +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 System.Collections.Generic; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mods @@ -19,12 +16,4 @@ namespace osu.Game.Rulesets.Mods /// void ApplyToDrawableHitObject(DrawableHitObject drawable); } - - [Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216 - public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject - { - void ApplyToDrawableHitObjects(IEnumerable drawables); - - void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield()); - } } diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs new file mode 100644 index 0000000000..7f926dd8b8 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs @@ -0,0 +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 System.Collections.Generic; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mods +{ + [Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216 + public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject + { + void ApplyToDrawableHitObjects(IEnumerable drawables); + + void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield()); + } +} From 5933e0d2d99a30ebc57b83a8dad81934746eabc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 13:17:32 +0900 Subject: [PATCH 424/670] Change `CheckCompatibleSet` to never deselect the current candidat when checking incompatibility --- osu.Game.Tests/Mods/ModUtilsTest.cs | 30 +++++++++++++++++++++++++++-- osu.Game/Utils/ModUtils.cs | 3 +++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 7384471c41..9f27289d7e 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -21,6 +21,14 @@ namespace osu.Game.Tests.Mods Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object })); } + [Test] + public void TestModIsCompatibleByItselfWithIncompatibleInterface() + { + var mod = new Mock(); + mod.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) }); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object })); + } + [Test] public void TestIncompatibleThroughTopLevel() { @@ -34,6 +42,20 @@ namespace osu.Game.Tests.Mods Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False); } + [Test] + public void TestIncompatibleThroughInterface() + { + var mod1 = new Mock(); + var mod2 = new Mock(); + + mod1.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) }); + mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) }); + + // Test both orderings. + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False); + } + [Test] public void TestMultiModIncompatibleWithTopLevel() { @@ -149,11 +171,15 @@ namespace osu.Game.Tests.Mods Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); } - public abstract class CustomMod1 : Mod + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } - public abstract class CustomMod2 : Mod + public abstract class CustomMod2 : Mod, IModCompatibilitySpecification + { + } + + public interface IModCompatibilitySpecification { } } diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 1c3558fc90..98766cb844 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -60,6 +60,9 @@ namespace osu.Game.Utils { foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m))) { + if (invalid == mod) + continue; + invalidMods ??= new List(); invalidMods.Add(invalid); } From 4de27429bc0dc65f988c8b1e67941464ce840bb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 13:17:55 +0900 Subject: [PATCH 425/670] Change `ModSelectOverlay` to never deselect the user triggered selection --- osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs | 2 +- osu.Game/Overlays/Mods/ModSection.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs b/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs index 78cd9bdae5..db76581108 100644 --- a/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Mods base.OnModSelected(mod); foreach (var section in ModSectionsContainer.Children) - section.DeselectTypes(mod.IncompatibleMods, true); + section.DeselectTypes(mod.IncompatibleMods, true, mod); } } } diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index aa8a5efd39..6e289dc8aa 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -159,12 +159,16 @@ namespace osu.Game.Overlays.Mods /// /// The types of s which should be deselected. /// Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow. - public void DeselectTypes(IEnumerable modTypes, bool immediate = false) + /// If this deselection is triggered by a user selection, this should contain the newly selected type. This type will never be deselected, even if it matches one provided in . + public void DeselectTypes(IEnumerable modTypes, bool immediate = false, Mod newSelection = null) { foreach (var button in Buttons) { if (button.SelectedMod == null) continue; + if (button.SelectedMod == newSelection) + continue; + foreach (var type in modTypes) { if (type.IsInstanceOfType(button.SelectedMod)) From 860626152aedc57dc941031e60067ae63c253181 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 12:54:40 +0900 Subject: [PATCH 426/670] Mark all mods which adjust approach circle as incompatible with each other Closes https://github.com/ppy/osu/issues/13543. --- osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs | 12 ++++++++++++ .../Mods/OsuModApproachDifferent.cs | 5 ++++- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 4 ++-- 6 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs diff --git a/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs new file mode 100644 index 0000000000..60a5825241 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Mods +{ + /// + /// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes. + /// + public interface IMutateApproachCircles + { + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index 074fb7dbed..526e29ad53 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; @@ -11,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject + public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles { public override string Name => "Approach Different"; public override string Acronym => "AD"; @@ -19,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; + [SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)] public BindableFloat Scale { get; } = new BindableFloat(4) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 2752feb0a1..a7c79aa2a0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -14,12 +14,12 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModHidden : ModHidden + public class OsuModHidden : ModHidden, IMutateApproachCircles { public override string Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => 1.06; - public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable), typeof(OsuModSpinIn) }; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; private const double fade_in_duration_multiplier = 0.4; private const double fade_out_duration_multiplier = 0.3; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index d1be162f73..6dfabed0df 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// Adjusts the size of hit objects during their fade in animation. /// - public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment + public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles { public override ModType Type => ModType.Fun; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods protected virtual float EndScale => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) }; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index 96ba58da23..d3ca2973f0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -12,7 +12,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpinIn : ModWithVisibilityAdjustment + public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles { public override string Name => "Spin In"; public override string Acronym => "SI"; @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; // todo: this mod should be able to be compatible with hidden with a bit of further implementation. - public override Type[] IncompatibleMods => new[] { typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) }; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; private const int rotate_offset = 360; private const float rotate_starting_width = 2; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 4b0939db16..84263221a7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModTraceable : ModWithVisibilityAdjustment + public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles { public override string Name => "Traceable"; public override string Acronym => "TC"; @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) }; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { From 5cf2ac78fcc1790b2556d11336c10496c7a94da8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Jun 2021 15:40:35 +0900 Subject: [PATCH 427/670] Adjust font namespaces --- osu.Game/OsuGameBase.cs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index abf8fbc4fb..05a53452f7 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -190,29 +190,29 @@ namespace osu.Game AddFont(Resources, @"Fonts/osuFont"); - AddFont(Resources, @"Fonts/Torus-Regular"); - AddFont(Resources, @"Fonts/Torus-Light"); - AddFont(Resources, @"Fonts/Torus-SemiBold"); - AddFont(Resources, @"Fonts/Torus-Bold"); + AddFont(Resources, @"Fonts/Torus/Torus-Regular"); + AddFont(Resources, @"Fonts/Torus/Torus-Light"); + AddFont(Resources, @"Fonts/Torus/Torus-SemiBold"); + AddFont(Resources, @"Fonts/Torus/Torus-Bold"); - AddFont(Resources, @"Fonts/Inter-Regular"); - AddFont(Resources, @"Fonts/Inter-RegularItalic"); - AddFont(Resources, @"Fonts/Inter-Light"); - AddFont(Resources, @"Fonts/Inter-LightItalic"); - AddFont(Resources, @"Fonts/Inter-SemiBold"); - AddFont(Resources, @"Fonts/Inter-SemiBoldItalic"); - AddFont(Resources, @"Fonts/Inter-Bold"); - AddFont(Resources, @"Fonts/Inter-BoldItalic"); + AddFont(Resources, @"Fonts/Inter/Inter-Regular"); + AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic"); + AddFont(Resources, @"Fonts/Inter/Inter-Light"); + AddFont(Resources, @"Fonts/Inter/Inter-LightItalic"); + AddFont(Resources, @"Fonts/Inter/Inter-SemiBold"); + AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic"); + AddFont(Resources, @"Fonts/Inter/Inter-Bold"); + AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic"); - AddFont(Resources, @"Fonts/Noto-Basic"); - AddFont(Resources, @"Fonts/Noto-Hangul"); - AddFont(Resources, @"Fonts/Noto-CJK-Basic"); - AddFont(Resources, @"Fonts/Noto-CJK-Compatibility"); - AddFont(Resources, @"Fonts/Noto-Thai"); + AddFont(Resources, @"Fonts/Noto/Noto-Basic"); + AddFont(Resources, @"Fonts/Noto/Noto-Hangul"); + AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic"); + AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility"); + AddFont(Resources, @"Fonts/Noto/Noto-Thai"); - AddFont(Resources, @"Fonts/Venera-Light"); - AddFont(Resources, @"Fonts/Venera-Bold"); - AddFont(Resources, @"Fonts/Venera-Black"); + AddFont(Resources, @"Fonts/Venera/Venera-Light"); + AddFont(Resources, @"Fonts/Venera/Venera-Bold"); + AddFont(Resources, @"Fonts/Venera/Venera-Black"); Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; From 2bf855fcca95d02d5a52746699a824ca9cebd5de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 15:45:12 +0900 Subject: [PATCH 428/670] Move all storyboard outro skip logic out of `updateCompletionState` This method should only be called to trigger the score completion portion of player progression. The storyboard skip/end logic is now handled separately in `progressToResults`. --- osu.Game/Screens/Play/Player.cs | 71 +++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9fe12c58de..c8ba554c5f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -295,12 +295,12 @@ namespace osu.Game.Screens.Play DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded => { - if (storyboardEnded.NewValue && resultsDisplayDelegate == null) - updateCompletionState(); + if (storyboardEnded.NewValue) + progressToResults(true); }; // Bind the judgement processors to ourselves - ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState()); + ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged); HealthProcessor.Failed += onFail; foreach (var mod in Mods.Value.OfType()) @@ -374,7 +374,7 @@ namespace osu.Game.Screens.Play }, skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0) { - RequestSkip = () => updateCompletionState(true), + RequestSkip = () => progressToResults(false), Alpha = 0 }, FailOverlay = new FailOverlay @@ -643,9 +643,8 @@ namespace osu.Game.Screens.Play /// /// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime. /// - /// If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it. /// Thrown if this method is called more than once without changing state. - private void updateCompletionState(bool skipStoryboardOutro = false) + private void scoreCompletionChanged(ValueChangedEvent completed) { // If this player instance is in the middle of an exit, don't attempt any kind of state update. if (!this.IsCurrentScreen()) @@ -656,7 +655,7 @@ namespace osu.Game.Screens.Play // Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run). // In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done, // but it still doesn't feel right that this exists here. - if (!ScoreProcessor.HasCompleted.Value) + if (!completed.NewValue) { resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate = null; @@ -667,7 +666,7 @@ namespace osu.Game.Screens.Play } if (resultsDisplayDelegate != null) - throw new InvalidOperationException(@$"{nameof(updateCompletionState)} should never be fired more than once."); + throw new InvalidOperationException(@$"{nameof(scoreCompletionChanged)} should never be fired more than once."); // Only show the completion screen if the player hasn't failed if (HealthProcessor.HasFailed) @@ -686,22 +685,49 @@ namespace osu.Game.Screens.Play // Asynchronously run score preparation operations (database import, online submission etc.). prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults); - if (skipStoryboardOutro) - { - scheduleCompletion(); - return; - } - bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; if (storyboardHasOutro) { + // if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending + // or the user pressing the skip outro button. skipOutroOverlay.Show(); return; } - using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) - scheduleCompletion(); + progressToResults(true); + } + + /// + /// Queue the results screen for display. + /// + /// + /// A final display will only occur once all work is completed in . + /// + /// Whether a minimum delay () should be added before the screen is displayed. + private void progressToResults(bool withDelay) + { + if (resultsDisplayDelegate != null) + return; + + double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0; + + resultsDisplayDelegate = new ScheduledDelegate(() => + { + if (prepareScoreForDisplayTask?.IsCompleted != true) + // if the asynchronous preparation has not completed, keep repeating this delegate. + return; + + resultsDisplayDelegate?.Cancel(); + + if (!this.IsCurrentScreen()) + // This player instance may already be in the process of exiting. + return; + + this.Push(CreateResults(prepareScoreForDisplayTask.Result)); + }, Time.Current + delay, 50); + + Scheduler.Add(resultsDisplayDelegate); } private async Task prepareScoreForResults() @@ -734,7 +760,18 @@ namespace osu.Game.Screens.Play { if (!prepareScoreForDisplayTask.IsCompleted) { - scheduleCompletion(); + resultsDisplayDelegate = Schedule(() => + { + if (!prepareScoreForDisplayTask.IsCompleted) + { + scheduleCompletion(); + return; + } + + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(prepareScoreForDisplayTask.Result)); + }); return; } From 752d0a9f0be0d5fa18580adf1bcfbb505e587d23 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 16:08:14 +0900 Subject: [PATCH 429/670] add sound to scroll-to-top button --- osu.Game/Graphics/Containers/OsuHoverContainer.cs | 4 +++- osu.Game/Graphics/UserInterface/HoverSampleSet.cs | 5 ++++- osu.Game/Overlays/OverlayScrollContainer.cs | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs index 67af79c763..ac66fd658a 100644 --- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs +++ b/osu.Game/Graphics/Containers/OsuHoverContainer.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Input.Events; using osuTK.Graphics; using System.Collections.Generic; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.Containers { @@ -20,7 +21,8 @@ namespace osu.Game.Graphics.Containers protected virtual IEnumerable EffectTargets => new[] { Content }; - public OsuHoverContainer() + public OsuHoverContainer(HoverSampleSet sampleSet = HoverSampleSet.Default) + : base(sampleSet) { Enabled.ValueChanged += e => { diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index c74ac90a4c..d74d13af95 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -20,6 +20,9 @@ namespace osu.Game.Graphics.UserInterface Toolbar, [Description("songselect")] - SongSelect + SongSelect, + + [Description("scrolltotop")] + ScrollToTop } } diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 0004719b87..c5b4cc3645 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; @@ -84,6 +85,7 @@ namespace osu.Game.Overlays private readonly Box background; public ScrollToTopButton() + : base(HoverSampleSet.ScrollToTop) { Size = new Vector2(50); Alpha = 0; From 7ef8eac7737c3fb82364fef8575eb173bff34f6f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 15:56:58 +0900 Subject: [PATCH 430/670] Remove unnecessary (and no longer correct) exception --- osu.Game/Screens/Play/Player.cs | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c8ba554c5f..5913e46836 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -665,9 +665,6 @@ namespace osu.Game.Screens.Play return; } - if (resultsDisplayDelegate != null) - throw new InvalidOperationException(@$"{nameof(scoreCompletionChanged)} should never be fired more than once."); - // Only show the completion screen if the player hasn't failed if (HealthProcessor.HasFailed) return; @@ -703,6 +700,7 @@ namespace osu.Game.Screens.Play ///
/// /// A final display will only occur once all work is completed in . + /// This means that even after calling this method, the results screen will never be shown until ScoreProcessor.HasCompleted becomes . /// /// Whether a minimum delay () should be added before the screen is displayed. private void progressToResults(bool withDelay) @@ -756,30 +754,6 @@ namespace osu.Game.Screens.Play return Score.ScoreInfo; } - private void scheduleCompletion() => resultsDisplayDelegate = Schedule(() => - { - if (!prepareScoreForDisplayTask.IsCompleted) - { - resultsDisplayDelegate = Schedule(() => - { - if (!prepareScoreForDisplayTask.IsCompleted) - { - scheduleCompletion(); - return; - } - - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(prepareScoreForDisplayTask.Result)); - }); - return; - } - - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(prepareScoreForDisplayTask.Result)); - }); - protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; #region Fail Logic From 06d1bd971cb5fdc37788efd49f8b8375074bde0e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:08:49 +0900 Subject: [PATCH 431/670] Default `DrawableStoryboard` to a completed state to avoid state change on empty storyboards --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index ca041da801..8a31e4576a 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -25,7 +25,7 @@ namespace osu.Game.Storyboards.Drawables ///
public IBindable HasStoryboardEnded => hasStoryboardEnded; - private readonly BindableBool hasStoryboardEnded = new BindableBool(); + private readonly BindableBool hasStoryboardEnded = new BindableBool(true); protected override Container Content { get; } From 3819a1f03b510759032f5dc5e967964dcd0bab52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:12:34 +0900 Subject: [PATCH 432/670] Remove exit override behaviour I don't actually know under what scenario this could have been hit, and actually caused expected behaviour. Consider that in the scenario I describe in the comment (which I added yesterday), the user is requesting a pause or exit which would be "cancelled showing the results instead". But in such a scenario, `PerformExit` would first be run, which cancels the `resultsDisplayDelegate` in the first place. The only special case would be pressing the close button on the window decoration? Which I don't think should be a special case in the first place, so I'm just going to remove this for the time being to keep things simple. --- osu.Game/Screens/Play/Player.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 5913e46836..5db9b47586 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -950,14 +950,6 @@ namespace osu.Game.Screens.Play { screenSuspension?.Expire(); - // if the results screen is prepared to be displayed, forcefully show it on an exit request. - // usually if a user has completed a play session they do want to see results. and if they don't they can hit the same key a second time. - if (resultsDisplayDelegate != null && !resultsDisplayDelegate.Cancelled && !resultsDisplayDelegate.Completed) - { - resultsDisplayDelegate.RunTask(); - return true; - } - // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // To resolve test failures, forcefully end playing synchronously when this screen exits. // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. From f3426e38b422590a0f1f97ac096742254391bd9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:18:20 +0900 Subject: [PATCH 433/670] Add note about delay parameter --- osu.Game/Screens/Play/Player.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 5db9b47586..f2d31e5dd2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -699,13 +699,19 @@ namespace osu.Game.Screens.Play /// Queue the results screen for display. ///
/// - /// A final display will only occur once all work is completed in . - /// This means that even after calling this method, the results screen will never be shown until ScoreProcessor.HasCompleted becomes . + /// A final display will only occur once all work is completed in . This means that even after calling this method, the results screen will never be shown until ScoreProcessor.HasCompleted becomes . + /// + /// Calling this method multiple times will have no effect. /// /// Whether a minimum delay () should be added before the screen is displayed. private void progressToResults(bool withDelay) { if (resultsDisplayDelegate != null) + // Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be + // accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued + // may take x00 more milliseconds than expected in the very rare edge case). + // + // If required we can handle this more correctly by rescheduling here. return; double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0; @@ -713,7 +719,7 @@ namespace osu.Game.Screens.Play resultsDisplayDelegate = new ScheduledDelegate(() => { if (prepareScoreForDisplayTask?.IsCompleted != true) - // if the asynchronous preparation has not completed, keep repeating this delegate. + // If the asynchronous preparation has not completed, keep repeating this delegate. return; resultsDisplayDelegate?.Cancel(); From 45122594e50ce23734e16be5332ee5b6603044cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:24:07 +0900 Subject: [PATCH 434/670] Remove the synchronous version of `PrepareScoreForResults` Replaces all existing usages with the `async` version. --- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 5 +++-- osu.Game/Screens/Play/Player.cs | 20 +++++-------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index cc1fb0b321..567ea6b988 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Screens; @@ -54,9 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); } - protected override void PrepareScoreForResults(Score score) + protected override async Task PrepareScoreForResultsAsync(Score score) { - base.PrepareScoreForResults(score); + await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9fe12c58de..70cac9d27a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -626,7 +626,7 @@ namespace osu.Game.Screens.Play /// /// This delegate, when set, means the results screen has been queued to appear. - /// The display of the results screen may be delayed by any work being done in and . + /// The display of the results screen may be delayed by any work being done in . /// /// /// Once set, this can *only* be cancelled by rewinding, ie. if ScoreProcessor.HasCompleted becomes . @@ -706,9 +706,6 @@ namespace osu.Game.Screens.Play private async Task prepareScoreForResults() { - // ReSharper disable once MethodHasAsyncOverload - PrepareScoreForResults(Score); - try { await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); @@ -1007,22 +1004,15 @@ namespace osu.Game.Screens.Play /// /// Prepare the for display at results. /// - /// - /// This is run synchronously before is run. - /// /// The to prepare. - protected virtual void PrepareScoreForResults(Score score) + /// A task that prepares the provided score. On completion, the score is assumed to be ready for display. + protected virtual Task PrepareScoreForResultsAsync(Score score) { // perform one final population to ensure everything is up-to-date. ScoreProcessor.PopulateScore(score.ScoreInfo); - } - /// - /// Prepare the for display at results. - /// - /// The to prepare. - /// A task that prepares the provided score. On completion, the score is assumed to be ready for display. - protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask; + return Task.CompletedTask; + } /// /// Creates the for a . From 19507e107e7185609db5b71c817e842b0cd92909 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:46:40 +0900 Subject: [PATCH 435/670] Reorder methods to make more sense --- osu.Game/Screens/Play/Player.cs | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 76f5e2336c..eab3770e27 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -695,6 +695,29 @@ namespace osu.Game.Screens.Play progressToResults(true); } + private async Task prepareScoreForResults() + { + try + { + await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Error(ex, @"Score preparation failed!"); + } + + try + { + await ImportScore(Score).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Error(ex, @"Score import failed!"); + } + + return Score.ScoreInfo; + } + /// /// Queue the results screen for display. /// @@ -734,29 +757,6 @@ namespace osu.Game.Screens.Play Scheduler.Add(resultsDisplayDelegate); } - private async Task prepareScoreForResults() - { - try - { - await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.Error(ex, @"Score preparation failed!"); - } - - try - { - await ImportScore(Score).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.Error(ex, @"Score import failed!"); - } - - return Score.ScoreInfo; - } - protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; #region Fail Logic From 0bc68a70181d1b09b5edbe59014d2a948a89c78b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:49:43 +0900 Subject: [PATCH 436/670] Move xmldoc to method --- osu.Game/Screens/Play/Player.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index eab3770e27..cadcc474b2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -679,7 +679,6 @@ namespace osu.Game.Screens.Play if (!Configuration.ShowResults) return; - // Asynchronously run score preparation operations (database import, online submission etc.). prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults); bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; @@ -695,6 +694,10 @@ namespace osu.Game.Screens.Play progressToResults(true); } + /// + /// Asynchronously run score preparation operations (database import, online submission etc.). + /// + /// The final score. private async Task prepareScoreForResults() { try From d06e52505a9b8105a8683ea31506a96f69528d26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 17:01:51 +0900 Subject: [PATCH 437/670] Fix thread safety of `KeyBindingStore.GetReadableKeyCombinationsFor` --- osu.Game/Input/RealmKeyBindingStore.cs | 21 ++++++++++++++------- osu.Game/OsuGame.cs | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index e2efd546e7..45b7cf355f 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -26,16 +26,23 @@ namespace osu.Game.Input /// /// The action to lookup. /// A set of display strings for all the user's key configuration for the action. - public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) + public IReadOnlyList GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in realmFactory.Context.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) - { - string str = action.KeyCombination.ReadableString(); + List combinations = new List(); - // even if found, the readable string may be empty for an unbound action. - if (str.Length > 0) - yield return str; + using (var context = realmFactory.GetForRead()) + { + foreach (var action in context.Realm.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) + { + string str = action.KeyCombination.ReadableString(); + + // even if found, the readable string may be empty for an unbound action. + if (str.Length > 0) + combinations.Add(str); + } } + + return combinations; } /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0c4d035728..907794d3bb 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -608,9 +608,9 @@ namespace osu.Game LocalConfig.LookupKeyBindings = l => { - var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l).ToArray(); + var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); - if (combinations.Length == 0) + if (combinations.Count == 0) return "none"; return string.Join(" or ", combinations); From d5a1524eb0e0d1d530f7cacbc1557faad1358b34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 17:12:01 +0900 Subject: [PATCH 438/670] Add missing `rulesetID` check for global action matching --- osu.Game/Input/RealmKeyBindingStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 45b7cf355f..9089169877 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -32,7 +32,7 @@ namespace osu.Game.Input using (var context = realmFactory.GetForRead()) { - foreach (var action in context.Realm.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) + foreach (var action in context.Realm.All().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction)) { string str = action.KeyCombination.ReadableString(); From 5c59195e35b422b7e2481e529b9f43c4ce6bc153 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 17:34:45 +0900 Subject: [PATCH 439/670] Remove beta package allowance --- Directory.Build.props | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index a91d423043..53ad973e47 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,16 +33,12 @@ DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway. This is required due to https://github.com/NuGet/Home/issues/5740 - NU5104: - This is triggered on osu.Game due to using a beta/prerelease version of realm. - Warning suppression can be removed after migrating off of a beta release. - CA9998: Microsoft.CodeAnalysis.FxCopAnalyzers has been deprecated. The entire package will be able to be removed after migrating to .NET 5, as analysers are shipped as part of the .NET 5 SDK anyway. --> - $(NoWarn);NU1701;NU5104;CA9998 + $(NoWarn);NU1701;CA9998 false From 36b5414b1d83693cbc5049a6a67d3ded89f87295 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 17:45:30 +0900 Subject: [PATCH 440/670] Update comment to hopefully explain a weird conditional better --- osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 5f500d3023..10376c1866 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -83,8 +83,9 @@ namespace osu.Game.Input.Bindings var defaults = DefaultKeyBindings.ToList(); if (ruleset != null && !ruleset.ID.HasValue) - // if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings. - // fallback to defaults instead. + // some tests instantiate a ruleset which is not present in the database. + // in these cases we still want key bindings to work, but matching to database instances would result in none being present, + // so let's populate the defaults directly. KeyBindings = defaults; else { From 4feb7c848f1e13e6030045c1fb06507fd0a58714 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 17:26:28 +0900 Subject: [PATCH 441/670] add sound to tab controls --- osu.Game/Graphics/UserInterface/HoverSampleSet.cs | 3 +++ osu.Game/Graphics/UserInterface/OsuTabControl.cs | 2 +- osu.Game/Graphics/UserInterface/PageTabControl.cs | 2 +- osu.Game/Overlays/OverlayTabControl.cs | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index d74d13af95..646131ed4f 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -22,6 +22,9 @@ namespace osu.Game.Graphics.UserInterface [Description("songselect")] SongSelect, + [Description("tabselect")] + TabSelect, + [Description("scrolltotop")] ScrollToTop } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 0c220336a5..c447d7f609 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -172,7 +172,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, }, - new HoverClickSounds() + new HoverClickSounds(HoverSampleSet.TabSelect) }; } diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index 1ba9ad53bb..a218c7bf52 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -76,7 +76,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, }, - new HoverClickSounds() + new HoverClickSounds(HoverSampleSet.TabSelect) }; Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); diff --git a/osu.Game/Overlays/OverlayTabControl.cs b/osu.Game/Overlays/OverlayTabControl.cs index a1cbf2c1e7..578cd703c7 100644 --- a/osu.Game/Overlays/OverlayTabControl.cs +++ b/osu.Game/Overlays/OverlayTabControl.cs @@ -99,7 +99,7 @@ namespace osu.Game.Overlays ExpandedSize = 5f, CollapsedSize = 0 }, - new HoverClickSounds() + new HoverClickSounds(HoverSampleSet.TabSelect) }; } From d462394635c67dbf24238fdccfb38916474f134a Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 18:57:40 +0900 Subject: [PATCH 442/670] add sound to dropdowns --- .../Graphics/UserInterface/HoverSampleSet.cs | 3 -- .../Graphics/UserInterface/OsuDropdown.cs | 29 ++++++++++++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index 646131ed4f..3a8187c8f4 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -13,9 +13,6 @@ namespace osu.Game.Graphics.UserInterface [Description("button")] Button, - [Description("softer")] - Soft, - [Description("toolbar")] Toolbar, diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 15fb00ccb0..24d062e059 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -4,6 +4,8 @@ using System.Linq; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -57,6 +59,9 @@ namespace osu.Game.Graphics.UserInterface { public override bool HandleNonPositionalInput => State == MenuState.Open; + private Sample sampleOpen; + private Sample sampleClose; + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring public OsuDropdownMenu() { @@ -69,9 +74,25 @@ namespace osu.Game.Graphics.UserInterface ItemsContainer.Padding = new MarginPadding(5); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); + sampleClose = audio.Samples.Get(@"UI/dropdown-close"); + } + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring - protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint); - protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint); + protected override void AnimateOpen() + { + this.FadeIn(300, Easing.OutQuint); + sampleOpen?.Play(); + } + + protected override void AnimateClose() + { + this.FadeOut(300, Easing.OutQuint); + sampleClose?.Play(); + } // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring protected override void UpdateSize(Vector2 newSize) @@ -155,7 +176,7 @@ namespace osu.Game.Graphics.UserInterface nonAccentSelectedColour = Color4.Black.Opacity(0.5f); updateColours(); - AddInternal(new HoverClickSounds(HoverSampleSet.Soft)); + AddInternal(new HoverSounds()); } protected override void UpdateForegroundColour() @@ -262,7 +283,7 @@ namespace osu.Game.Graphics.UserInterface }, }; - AddInternal(new HoverClickSounds()); + AddInternal(new HoverSounds()); } [BackgroundDependencyLoader] From 36d2199a0241173a8931fab9f17ef56a2191970e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Jun 2021 19:20:57 +0900 Subject: [PATCH 443/670] Add exception on Apply() while loading --- .../Pooling/PoolableDrawableWithLifetime.cs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index 3ab85aa214..faa82786cd 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -28,12 +28,9 @@ namespace osu.Game.Rulesets.Objects.Pooling public TEntry? Entry { get => entry; - set { - if (LoadState < LoadState.Ready) - entry = value; - else if (value != null) + if (value != null) Apply(value); else if (HasEntryApplied) free(); @@ -86,7 +83,7 @@ namespace osu.Game.Rulesets.Objects.Pooling // Apply the initial entry. if (Entry != null && !HasEntryApplied) - Apply(Entry); + apply(Entry); } /// @@ -95,16 +92,10 @@ namespace osu.Game.Rulesets.Objects.Pooling /// public void Apply(TEntry entry) { - if (HasEntryApplied) - free(); + if (LoadState == LoadState.Loading) + throw new InvalidOperationException($"Cannot apply a new {nameof(TEntry)} while currently loading."); - this.entry = entry; - entry.LifetimeChanged += setLifetimeFromEntry; - setLifetimeFromEntry(entry); - - OnApply(entry); - - HasEntryApplied = true; + apply(entry); } protected sealed override void FreeAfterUse() @@ -130,6 +121,20 @@ namespace osu.Game.Rulesets.Objects.Pooling { } + private void apply(TEntry entry) + { + if (HasEntryApplied) + free(); + + this.entry = entry; + entry.LifetimeChanged += setLifetimeFromEntry; + setLifetimeFromEntry(entry); + + OnApply(entry); + + HasEntryApplied = true; + } + private void free() { Debug.Assert(Entry != null && HasEntryApplied); From 36d51d5117e8a149de7d057fcf9e116d395133bf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Jun 2021 19:23:37 +0900 Subject: [PATCH 444/670] Don't set entry immediately --- .../Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index faa82786cd..3b261494ff 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -30,7 +30,9 @@ namespace osu.Game.Rulesets.Objects.Pooling get => entry; set { - if (value != null) + if (LoadState == LoadState.NotLoaded) + entry = value; + else if (value != null) Apply(value); else if (HasEntryApplied) free(); From 42c5a962fbbc9a0ff871c387c6d8345104b45276 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Jun 2021 19:27:10 +0900 Subject: [PATCH 445/670] Add xmldoc remark --- .../Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index 3b261494ff..9c6097a048 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Objects.Pooling /// /// /// If a non-null value is set before loading is started, the entry is applied when the loading is completed. + /// It is not valid to set an entry while this is loading. /// public TEntry? Entry { From 78c5ccda605a26883268359a792c0cd5013db750 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 19:18:57 +0900 Subject: [PATCH 446/670] Fix renaming a ruleset DLL causing a startup crash --- osu.Game/Rulesets/RulesetStore.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 0a34ca9598..06bb14ce17 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using Microsoft.EntityFrameworkCore.Internal; using osu.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; @@ -96,13 +97,25 @@ namespace osu.Game.Rulesets context.SaveChanges(); - // add any other modes var existingRulesets = context.RulesetInfo.ToList(); + // add any other rulesets which have assemblies present but are not yet in the database. foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) { if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) - context.RulesetInfo.Add(r.RulesetInfo); + { + var existingSameShortName = existingRulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName); + + if (existingSameShortName != null) + { + // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName. + // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one. + // in such cases, update the instantiation info of the existing entry to point to the new one. + existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo; + } + else + context.RulesetInfo.Add(r.RulesetInfo); + } } context.SaveChanges(); From 2dadc9d686071ae4e1df0e5c6859f584c28174ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 19:39:00 +0900 Subject: [PATCH 447/670] Remove unused using statement --- osu.Game/Rulesets/RulesetStore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 06bb14ce17..1f12f3dfeb 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using Microsoft.EntityFrameworkCore.Internal; using osu.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; From 953683044f4440051bd39f9ff294f172aa095dd0 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 21:00:08 +0900 Subject: [PATCH 448/670] fix checkbox sounds not being used for certain checkboxes --- .../UserInterface/OsuTabControlCheckbox.cs | 22 ++++++++++++++++--- osu.Game/Overlays/Comments/CommentsHeader.cs | 17 ++++++++++++++ osu.Game/Overlays/Comments/HeaderButton.cs | 2 -- osu.Game/Overlays/OverlaySortTabControl.cs | 2 ++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs index b66a4a58ce..c6121dcd17 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs @@ -4,6 +4,8 @@ using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -43,6 +45,8 @@ namespace osu.Game.Graphics.UserInterface } private const float transition_length = 500; + private Sample sampleChecked; + private Sample sampleUnchecked; public OsuTabControlCheckbox() { @@ -77,8 +81,7 @@ namespace osu.Game.Graphics.UserInterface Colour = Color4.White, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - }, - new HoverClickSounds() + } }; Current.ValueChanged += selected => @@ -91,10 +94,13 @@ namespace osu.Game.Graphics.UserInterface } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { if (accentColour == null) AccentColour = colours.Blue; + + sampleChecked = audio.Samples.Get(@"UI/check-on"); + sampleUnchecked = audio.Samples.Get(@"UI/check-off"); } protected override bool OnHover(HoverEvent e) @@ -111,6 +117,16 @@ namespace osu.Game.Graphics.UserInterface base.OnHoverLost(e); } + protected override void OnUserChange(bool value) + { + base.OnUserChange(value); + + if (value) + sampleChecked?.Play(); + else + sampleUnchecked?.Play(); + } + private void updateFade() { box.FadeTo(Current.Value || IsHovered ? 1 : 0, transition_length, Easing.OutQuint); diff --git a/osu.Game/Overlays/Comments/CommentsHeader.cs b/osu.Game/Overlays/Comments/CommentsHeader.cs index 0dd68bbd41..bf80655c3d 100644 --- a/osu.Game/Overlays/Comments/CommentsHeader.cs +++ b/osu.Game/Overlays/Comments/CommentsHeader.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Framework.Bindables; @@ -66,6 +68,8 @@ namespace osu.Game.Overlays.Comments public readonly BindableBool Checked = new BindableBool(); private readonly SpriteIcon checkboxIcon; + private Sample sampleChecked; + private Sample sampleUnchecked; public ShowDeletedButton() { @@ -93,6 +97,13 @@ namespace osu.Game.Overlays.Comments }); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleChecked = audio.Samples.Get(@"UI/check-on"); + sampleUnchecked = audio.Samples.Get(@"UI/check-off"); + } + protected override void LoadComplete() { Checked.BindValueChanged(isChecked => checkboxIcon.Icon = isChecked.NewValue ? FontAwesome.Solid.CheckSquare : FontAwesome.Regular.Square, true); @@ -102,6 +113,12 @@ namespace osu.Game.Overlays.Comments protected override bool OnClick(ClickEvent e) { Checked.Value = !Checked.Value; + + if (Checked.Value) + sampleChecked?.Play(); + else + sampleUnchecked?.Play(); + return true; } } diff --git a/osu.Game/Overlays/Comments/HeaderButton.cs b/osu.Game/Overlays/Comments/HeaderButton.cs index fdc8db35ab..65172aa57c 100644 --- a/osu.Game/Overlays/Comments/HeaderButton.cs +++ b/osu.Game/Overlays/Comments/HeaderButton.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Comments { @@ -39,7 +38,6 @@ namespace osu.Game.Overlays.Comments Origin = Anchor.Centre, Margin = new MarginPadding { Horizontal = 10 } }, - new HoverClickSounds(), }); } diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs index b230acca11..d4dde0db3f 100644 --- a/osu.Game/Overlays/OverlaySortTabControl.cs +++ b/osu.Game/Overlays/OverlaySortTabControl.cs @@ -148,6 +148,8 @@ namespace osu.Game.Overlays } } }); + + AddInternal(new HoverClickSounds()); } protected override void LoadComplete() From 6e4fc26e168b2d18b6ffbcd94e00d27011134c47 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 21:02:17 +0900 Subject: [PATCH 449/670] replace 'songselect' hover/click sounds with 'button' ones for now --- osu.Game/Graphics/UserInterface/HoverSampleSet.cs | 3 --- osu.Game/Screens/Select/FooterButton.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index 3a8187c8f4..b88f81a143 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -16,9 +16,6 @@ namespace osu.Game.Graphics.UserInterface [Description("toolbar")] Toolbar, - [Description("songselect")] - SongSelect, - [Description("tabselect")] TabSelect, diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index afb3943a09..c3fbd767ff 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select private readonly Box light; public FooterButton() - : base(HoverSampleSet.SongSelect) + : base(HoverSampleSet.Button) { AutoSizeAxes = Axes.Both; Shear = SHEAR; From 5ce52b2669201b85c788d325f007681d41dd14d8 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 21:14:51 +0900 Subject: [PATCH 450/670] fix ModButton duplicate click sound --- osu.Game/Overlays/Mods/ModButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 5e3733cd5e..70424101fd 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -302,7 +302,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopCentre, Font = OsuFont.GetFont(size: 18) }, - new HoverClickSounds(buttons: new[] { MouseButton.Left, MouseButton.Right }) + new HoverSounds() }; Mod = mod; From 390abccb4bf56503bda7b29934e649d0a60ee7a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 22:08:59 +0900 Subject: [PATCH 451/670] Add workaround for dropdowns playing close animation on first display --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 24d062e059..b97f12df02 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -81,9 +81,13 @@ namespace osu.Game.Graphics.UserInterface sampleClose = audio.Samples.Get(@"UI/dropdown-close"); } + // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. + private bool wasOpened; + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring protected override void AnimateOpen() { + wasOpened = true; this.FadeIn(300, Easing.OutQuint); sampleOpen?.Play(); } @@ -91,7 +95,8 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateClose() { this.FadeOut(300, Easing.OutQuint); - sampleClose?.Play(); + if (wasOpened) + sampleClose?.Play(); } // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring From 1f6b4b10ab93b5175ad1a0e222ca9a17a80ade30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 22:16:15 +0900 Subject: [PATCH 452/670] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1f60f02fb1..3a1e6ba9a3 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 68ffb87c6c..fe7214de38 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 8aa79762fc..01c9b27cc7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 1ec03bf6fac74b678a543849c90062808c73b181 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 22:25:24 +0900 Subject: [PATCH 453/670] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 490e43b5e6..54469eec43 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8eeaad1127..560e5e2493 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index db442238ce..7b3033db9c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 69c1cd5b3430ef536c5c220bd77ed7398c4bab3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 14:17:37 +0200 Subject: [PATCH 454/670] Add failing test case for hit circle animations disable --- .../Editor/TestSceneOsuEditorHitAnimations.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs new file mode 100644 index 0000000000..94ee5d7e14 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + [TestFixture] + public class TestSceneOsuEditorHitAnimations : TestSceneOsuEditor + { + [Resolved] + private OsuConfigManager config { get; set; } + + [Test] + public void TestHitCircleAnimationDisable() + { + HitCircle hitCircle = null; + + AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0)); + toggleAnimations(true); + seekSmoothlyTo(() => hitCircle.StartTime + 10); + + AddAssert("hit circle piece has transforms", () => + { + var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); + return getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime); + }); + + AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1)); + toggleAnimations(false); + seekSmoothlyTo(() => hitCircle.StartTime + 10); + + AddAssert("hit circle piece has no transforms", () => + { + var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); + return getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime); + }); + } + + private HitCircle getHitCircle(int index) + => EditorBeatmap.HitObjects.OfType().ElementAt(index); + + private DrawableHitObject getDrawableObjectFor(HitObject hitObject) + => this.ChildrenOfType().Single(ho => ho.HitObject == hitObject); + + private IEnumerable getTransformsRecursively(Drawable drawable) + => drawable.ChildrenOfType().SelectMany(d => d.Transforms); + + private void toggleAnimations(bool enabled) + => AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled)); + + private void seekSmoothlyTo(Func targetTime) + { + AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke())); + AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime)); + } + } +} From e2a370f60244a0ccac1450ed604a79a2be468a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 14:57:04 +0200 Subject: [PATCH 455/670] Add coverage for hit circle fade-out duration --- .../Editor/TestSceneOsuEditorHitAnimations.cs | 19 +++++++++++-------- .../Edit/DrawableOsuEditorRuleset.cs | 16 ++++++++-------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs index 94ee5d7e14..18de3a8414 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs @@ -13,6 +13,7 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -28,25 +29,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public void TestHitCircleAnimationDisable() { HitCircle hitCircle = null; + DrawableHitCircle drawableHitCircle = null; AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0)); toggleAnimations(true); seekSmoothlyTo(() => hitCircle.StartTime + 10); - AddAssert("hit circle piece has transforms", () => - { - var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); - return getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime); - }); + AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); + AddAssert("hit circle piece has transforms", + () => getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime)); AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1)); toggleAnimations(false); seekSmoothlyTo(() => hitCircle.StartTime + 10); - AddAssert("hit circle piece has no transforms", () => + AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); + AddAssert("hit circle piece has no transforms", + () => getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime)); + AddAssert("hit circle has longer fade-out applied", () => { - var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); - return getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime); + var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha)); + return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION; }); } diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index aeeae84d14..9143b154b9 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -20,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Edit { public class DrawableOsuEditorRuleset : DrawableOsuRuleset { + /// + /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. + /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. + /// + public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700; + public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) : base(ruleset, beatmap, mods) { @@ -46,12 +52,6 @@ namespace osu.Game.Rulesets.Osu.Edit d.ApplyCustomUpdateState += updateState; } - /// - /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. - /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. - /// - private const double editor_hit_object_fade_out_extension = 700; - private void updateState(DrawableHitObject hitObject, ArmedState state) { if (state == ArmedState.Idle || hitAnimations.Value) @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (hitObject is DrawableHitCircle circle) { circle.ApproachCircle - .FadeOutFromOne(editor_hit_object_fade_out_extension * 4) + .FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4) .Expire(); circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint); @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Edit hitObject.RemoveTransform(existing); using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + hitObject.FadeOut(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION).Expire(); break; } } From fe48ddfee3242cde208b64cb7a59967f0d670885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 15:26:31 +0200 Subject: [PATCH 456/670] Also cover slider animation disable --- .../Editor/TestSceneOsuEditorHitAnimations.cs | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs index 18de3a8414..7ffa2c1f94 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs @@ -36,16 +36,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor seekSmoothlyTo(() => hitCircle.StartTime + 10); AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); - AddAssert("hit circle piece has transforms", - () => getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime)); + assertFutureTransforms(() => drawableHitCircle.CirclePiece, true); AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1)); toggleAnimations(false); seekSmoothlyTo(() => hitCircle.StartTime + 10); AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); - AddAssert("hit circle piece has no transforms", - () => getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime)); + assertFutureTransforms(() => drawableHitCircle.CirclePiece, false); AddAssert("hit circle has longer fade-out applied", () => { var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha)); @@ -53,9 +51,47 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } + [Test] + public void TestSliderAnimationDisable() + { + Slider slider = null; + DrawableSlider drawableSlider = null; + DrawableSliderRepeat sliderRepeat = null; + + AddStep("retrieve first slider with repeats", () => slider = getSliderWithRepeats(0)); + toggleAnimations(true); + seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10); + + retrieveDrawables(); + assertFutureTransforms(() => sliderRepeat, true); + + AddStep("retrieve second slider with repeats", () => slider = getSliderWithRepeats(1)); + toggleAnimations(false); + seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10); + + retrieveDrawables(); + assertFutureTransforms(() => sliderRepeat.Arrow, false); + seekSmoothlyTo(() => slider.GetEndTime()); + AddAssert("slider has longer fade-out applied", () => + { + var alphaTransform = drawableSlider.Transforms.Last(t => t.TargetMember == nameof(Alpha)); + return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION; + }); + + void retrieveDrawables() => + AddStep("retrieve drawables", () => + { + drawableSlider = (DrawableSlider)getDrawableObjectFor(slider); + sliderRepeat = (DrawableSliderRepeat)getDrawableObjectFor(slider.NestedHitObjects.OfType().First()); + }); + } + private HitCircle getHitCircle(int index) => EditorBeatmap.HitObjects.OfType().ElementAt(index); + private Slider getSliderWithRepeats(int index) + => EditorBeatmap.HitObjects.OfType().Where(s => s.RepeatCount >= 1).ElementAt(index); + private DrawableHitObject getDrawableObjectFor(HitObject hitObject) => this.ChildrenOfType().Single(ho => ho.HitObject == hitObject); @@ -70,5 +106,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke())); AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime)); } + + private void assertFutureTransforms(Func getDrawable, bool hasFutureTransforms) + => AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms", + () => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms); } } From e94fbd83e2690c5c34c8ccd0cebdacaae636f4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 15:30:45 +0200 Subject: [PATCH 457/670] Ensure editor ruleset animation disable execution order --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index 9143b154b9..54fd95a618 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -69,8 +69,14 @@ namespace osu.Game.Rulesets.Osu.Edit if (hitObject is IHasMainCirclePiece mainPieceContainer) { // clear any explode animation logic. - mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); - mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + // this is scheduled after children to ensure that the clear happens after invocations of ApplyCustomUpdateState on the circle piece's nested skinnables. + ScheduleAfterChildren(() => + { + if (hitObject.HitObject == null) return; + + mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); + mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + }); } if (hitObject is DrawableSliderRepeat repeat) From afc89b39d9cb751ab00555c50a055cb607774603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 15:32:51 +0200 Subject: [PATCH 458/670] Use `StateUpdateTime` for transform clearing logic `MainCirclePiece` specifies a state transform starting at `StateUpdateTime`, which is earlier than the previously-used `HitStateUpdateTime`. Change the transform clearing logic to use the former to ensure that exactly all animation transforms are cleared. --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index 54fd95a618..0e61c02e2d 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -74,15 +74,15 @@ namespace osu.Game.Rulesets.Osu.Edit { if (hitObject.HitObject == null) return; - mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); - mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.StateUpdateTime, true); + mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.StateUpdateTime, true); }); } if (hitObject is DrawableSliderRepeat repeat) { - repeat.Arrow.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); - repeat.Arrow.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + repeat.Arrow.ApplyTransformsAt(hitObject.StateUpdateTime, true); + repeat.Arrow.ClearTransformsAfter(hitObject.StateUpdateTime, true); } // adjust the visuals of top-level object types to make them stay on screen for longer than usual. From 843c8bd7a44e7b244214818d6e6765e997fc1bf5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Jun 2021 20:33:50 +0300 Subject: [PATCH 459/670] Move spinner approach circle to its own `SkinnableDrawable` --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 +- .../Objects/Drawables/DrawableSpinner.cs | 12 ++-- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 2 +- .../Skinning/Legacy/LegacySpinner.cs | 31 --------- .../Legacy/LegacySpinnerApproachCircle.cs | 64 +++++++++++++++++++ .../Legacy/OsuLegacySkinTransformer.cs | 6 ++ 6 files changed, 76 insertions(+), 41 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index c635aab5f5..d712ffd92a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Mods // todo: hide background using (spinner.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt)) - spinner.HideApproachCircle(); + spinner.ApproachCircle.Hide(); using (spinner.BeginAbsoluteSequence(fadeStartTime)) spinner.FadeOut(fadeDuration); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 942cc52c50..4507b1520c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result; + public SkinnableDrawable ApproachCircle { get; private set; } + public SpinnerRotationTracker RotationTracker { get; private set; } private SpinnerSpmCalculator spmCalculator; @@ -42,8 +44,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; - internal readonly Bindable ApproachCircleVisibility = new Bindable(Visibility.Visible); - /// /// The amount of bonus score gained from spinning after the required number of spins, for display purposes. /// @@ -88,7 +88,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RelativeSizeAxes = Axes.Y, Children = new Drawable[] { - new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()), + ApproachCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerApproachCircle)), + new SkinnableSpinnerBody(ApproachCircle.CreateProxy(), _ => new DefaultSpinner()), RotationTracker = new SpinnerRotationTracker(this) } }, @@ -287,11 +288,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables updateBonusScore(); } - /// - /// Hides the spinner's approach circle if it has one. - /// - public void HideApproachCircle() => this.TransformBindableTo(ApproachCircleVisibility, Visibility.Hidden); - private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; private int wholeSpins; diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index fcb544fa5b..687fc1f966 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -10,7 +10,6 @@ namespace osu.Game.Rulesets.Osu Cursor, CursorTrail, SliderScorePoint, - ApproachCircle, ReverseArrow, HitCircleText, SliderHeadHitCircle, @@ -19,5 +18,6 @@ namespace osu.Game.Rulesets.Osu SliderBall, SliderBody, SpinnerBody, + SpinnerApproachCircle, } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 5b0c2d405b..f32caab8ac 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -32,7 +32,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected DrawableSpinner DrawableSpinner { get; private set; } - private Drawable approachCircle; private Sprite spin; private Sprite clear; @@ -61,7 +60,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy RelativeSizeAxes = Axes.Both, Children = new[] { - approachCircle = getSpinnerApproachCircle(source), spin = new Sprite { Anchor = Anchor.TopCentre, @@ -104,28 +102,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy }.With(s => s.Font = s.Font.With(fixedWidth: false)), } }); - - static Drawable getSpinnerApproachCircle(ISkinSource source) - { - var spinnerProvider = source.FindProvider(s => - s.GetTexture("spinner-circle") != null || - s.GetTexture("spinner-top") != null); - - if (spinnerProvider is DefaultLegacySkin) - return Empty(); - - return new Sprite - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-approachcircle"), - Scale = new Vector2(SPRITE_SCALE * 1.86f), - Y = SPINNER_Y_CENTRE, - }; - } } - private IBindable approachCircleVisibility; private IBindable gainedBonus; private IBindable spinsPerMinute; @@ -135,12 +113,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { base.LoadComplete(); - approachCircleVisibility = DrawableSpinner.ApproachCircleVisibility.GetBoundCopy(); - approachCircleVisibility.BindValueChanged(v => - { - approachCircle.Alpha = v.NewValue == Visibility.Hidden ? 0 : 1; - }, true); - gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy(); gainedBonus.BindValueChanged(bonus => { @@ -204,9 +176,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); } - using (BeginAbsoluteSequence(d.HitObject.StartTime)) - approachCircle.ScaleTo(SPRITE_SCALE * 1.86f).ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration); - double spinFadeOutLength = Math.Min(400, d.HitObject.Duration); using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true)) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs new file mode 100644 index 0000000000..92454cefa3 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Skinning; +using osuTK; +using static osu.Game.Rulesets.Osu.Skinning.Legacy.LegacySpinner; + +namespace osu.Game.Rulesets.Osu.Skinning.Legacy +{ + public class LegacySpinnerApproachCircle : CompositeDrawable + { + private DrawableSpinner drawableSpinner; + + [CanBeNull] + private Sprite sprite; + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableHitObject, ISkinSource source) + { + drawableSpinner = (DrawableSpinner)drawableHitObject; + + AutoSizeAxes = Axes.Both; + + var spinnerProvider = source.FindProvider(s => + s.GetTexture("spinner-circle") != null || + s.GetTexture("spinner-top") != null); + + if (spinnerProvider is DefaultLegacySkin) + return; + + InternalChild = sprite = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-approachcircle"), + Scale = new Vector2(SPRITE_SCALE * 1.86f), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); + } + + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) + { + if (!(drawableHitObject is DrawableSpinner spinner)) + return; + + using (BeginAbsoluteSequence(spinner.HitObject.StartTime)) + sprite?.ScaleTo(SPRITE_SCALE * 0.1f, spinner.HitObject.Duration); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 3267b48ebf..a8c42a3773 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -121,6 +121,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return new LegacyOldStyleSpinner(); return null; + + case OsuSkinComponents.SpinnerApproachCircle: + if (Source.GetTexture("spinner-approachcircle") != null) + return new LegacySpinnerApproachCircle(); + + return null; } } From d6b9436151a9b0a7f658f4f4c07f5230329de115 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Jun 2021 20:34:25 +0300 Subject: [PATCH 460/670] Proxy spinner approach circle before the spinner overlay components --- .../Skinning/IProxiesApproachCircle.cs | 12 +++++++ .../Skinning/Legacy/LegacySpinner.cs | 13 ++++--- .../Skinning/SkinnableSpinnerBody.cs | 34 +++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs create mode 100644 osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs new file mode 100644 index 0000000000..3cc0026adc --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public interface IProxiesApproachCircle + { + Container ApproachCircleTarget { get; } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index f32caab8ac..efbb27bf3f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -15,8 +15,10 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - public abstract class LegacySpinner : CompositeDrawable + public abstract class LegacySpinner : CompositeDrawable, IProxiesApproachCircle { + public const float SPRITE_SCALE = 0.625f; + /// /// All constants are in osu!stable's gamefield space, which is shifted 16px downwards. /// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space. @@ -26,12 +28,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f; - protected const float SPRITE_SCALE = 0.625f; - private const float spm_hide_offset = 50f; protected DrawableSpinner DrawableSpinner { get; private set; } + public Container ApproachCircleTarget { get; private set; } private Sprite spin; private Sprite clear; @@ -58,8 +59,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Depth = float.MinValue, RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { + ApproachCircleTarget = new Container + { + RelativeSizeAxes = Axes.Both, + }, spin = new Sprite { Anchor = Anchor.TopCentre, diff --git a/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs b/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs new file mode 100644 index 0000000000..763b9dd677 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs @@ -0,0 +1,34 @@ +// 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.Graphics; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + /// + /// A skinnable drawable of the component, with the approach circle exposed for modification. + /// + public class SkinnableSpinnerBody : SkinnableDrawable + { + private readonly Drawable approachCircleProxy; + + public SkinnableSpinnerBody(Drawable approachCircleProxy, Func defaultImplementation = null) + : base(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), defaultImplementation) + { + this.approachCircleProxy = approachCircleProxy; + } + + protected override void SkinChanged(ISkinSource skin) + { + if (Drawable is IProxiesApproachCircle oldProxiesApproachCircle) + oldProxiesApproachCircle.ApproachCircleTarget.Remove(approachCircleProxy); + + base.SkinChanged(skin); + + if (Drawable is IProxiesApproachCircle newProxiesApproachCircle) + newProxiesApproachCircle.ApproachCircleTarget.Add(approachCircleProxy); + } + } +} From 76db87f9cb273ca92e7cf61fc7faef898d74712a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Jun 2021 08:00:36 +0200 Subject: [PATCH 461/670] Try-catch around localisation store registration Some platforms (android, older windows versions) will throw exceptions at runtime when an unsupported `CultureInfo` is attempted to be instantiated, leading to nasty crashes. Add a preventative try-catch registration to prevent the crash, and log the errors for visibility. --- osu.Game/OsuGame.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0c4d035728..7455df361c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -55,6 +55,7 @@ using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Performance; using osu.Game.Skinning.Editor; +using osu.Framework.Extensions; namespace osu.Game { @@ -585,7 +586,15 @@ namespace osu.Game foreach (var language in Enum.GetValues(typeof(Language)).OfType()) { var cultureCode = language.ToCultureCode(); - Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode)); + + try + { + Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode)); + } + catch (Exception ex) + { + Logger.Error(ex, $"Could not load localisations for language \"{cultureCode}\""); + } } // The next time this is updated is in UpdateAfterChildren, which occurs too late and results From b47774b55a4d146e4d5a24a8daeafbde2f551cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Jun 2021 08:07:37 +0200 Subject: [PATCH 462/670] Remove Tagalog language for now Rationale given in inline comment. --- osu.Game/Localisation/Language.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index 65541feedf..3c66f31c58 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.cs @@ -86,8 +86,10 @@ namespace osu.Game.Localisation [Description(@"ไทย")] th, - [Description(@"Tagalog")] - tl, + // Tagalog has no associated localisations yet, and is not supported on Xamarin platforms or Windows versions <10. + // Can be revisited if localisations ever arrive. + //[Description(@"Tagalog")] + //tl, [Description(@"Türkçe")] tr, From afcc3e14f436d0e413bf5e6dfa0cdca844f6956d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 19 Jun 2021 16:16:29 +0900 Subject: [PATCH 463/670] m --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7455df361c..d7cdb8b396 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -55,7 +55,6 @@ using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Performance; using osu.Game.Skinning.Editor; -using osu.Framework.Extensions; namespace osu.Game { From 805e6cca75ef9d6637c10497959aadabdb163a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Jun 2021 11:07:13 +0200 Subject: [PATCH 464/670] Add direct references to Realm from Xamarin projects Fixes crashes on launch due to missing `realm-wrapper` transitive dependency. --- osu.Android.props | 4 ++++ osu.iOS.props | 1 + 2 files changed, 5 insertions(+) diff --git a/osu.Android.props b/osu.Android.props index 3a1e6ba9a3..1dc99bb60a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -54,4 +54,8 @@ + + + + diff --git a/osu.iOS.props b/osu.iOS.props index 01c9b27cc7..3689ce51f2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -99,5 +99,6 @@ + From fa87aa6be51ec0558ea0a94522751c52dd3330c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Jun 2021 11:10:37 +0200 Subject: [PATCH 465/670] Add autogenerated FodyWeavers files to Xamarin projects --- osu.Android/FodyWeavers.xml | 3 +++ osu.Android/FodyWeavers.xsd | 34 ++++++++++++++++++++++++++++++++++ osu.iOS/FodyWeavers.xml | 3 +++ osu.iOS/FodyWeavers.xsd | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 osu.Android/FodyWeavers.xml create mode 100644 osu.Android/FodyWeavers.xsd create mode 100644 osu.iOS/FodyWeavers.xml create mode 100644 osu.iOS/FodyWeavers.xsd diff --git a/osu.Android/FodyWeavers.xml b/osu.Android/FodyWeavers.xml new file mode 100644 index 0000000000..cc07b89533 --- /dev/null +++ b/osu.Android/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/osu.Android/FodyWeavers.xsd b/osu.Android/FodyWeavers.xsd new file mode 100644 index 0000000000..447878c551 --- /dev/null +++ b/osu.Android/FodyWeavers.xsd @@ -0,0 +1,34 @@ + + + + + + + + + + + Disables anonymized usage information from being sent on build. Read more about what data is being collected and why here: https://github.com/realm/realm-dotnet/blob/master/Realm/Realm.Fody/Common/Analytics.cs + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/osu.iOS/FodyWeavers.xml b/osu.iOS/FodyWeavers.xml new file mode 100644 index 0000000000..cc07b89533 --- /dev/null +++ b/osu.iOS/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/osu.iOS/FodyWeavers.xsd b/osu.iOS/FodyWeavers.xsd new file mode 100644 index 0000000000..447878c551 --- /dev/null +++ b/osu.iOS/FodyWeavers.xsd @@ -0,0 +1,34 @@ + + + + + + + + + + + Disables anonymized usage information from being sent on build. Read more about what data is being collected and why here: https://github.com/realm/realm-dotnet/blob/master/Realm/Realm.Fody/Common/Analytics.cs + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file From 27da3dc75a408b44ddf4dc1a38f4f0a9e55b8a57 Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Sat, 19 Jun 2021 20:54:24 +0800 Subject: [PATCH 466/670] added supporter-only-filter content --- .../BeatmapListingFilterControl.cs | 10 +- osu.Game/Overlays/BeatmapListingOverlay.cs | 114 +++++++++++++++++- 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 1935a250b7..3436a1b3b2 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -24,6 +24,7 @@ namespace osu.Game.Overlays.BeatmapListing { /// /// Fired when a search finishes. Contains only new items in the case of pagination. + /// Null when non-supporter user used supporter-only filters /// public Action> SearchFinished; @@ -212,7 +213,14 @@ namespace osu.Game.Overlays.BeatmapListing lastResponse = response; getSetsRequest = null; - SearchFinished?.Invoke(sets); + if (!api.LocalUser.Value.IsSupporter && (searchControl.Ranks.Any() || searchControl.Played.Value != SearchPlayed.Any)) + { + SearchFinished?.Invoke(null); + } + else + { + SearchFinished?.Invoke(sets); + } }; api.Queue(getSetsRequest); diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 5e65cd9488..63b9d3d34a 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -15,7 +15,9 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.Containers; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Resources.Localisation.Web; @@ -33,6 +35,7 @@ namespace osu.Game.Overlays private Container panelTarget; private FillFlowContainer foundContent; private NotFoundDrawable notFoundContent; + private SupporterRequiredDrawable supporterRequiredContent; private BeatmapListingFilterControl filterControl; public BeatmapListingOverlay() @@ -76,6 +79,7 @@ namespace osu.Game.Overlays { foundContent = new FillFlowContainer(), notFoundContent = new NotFoundDrawable(), + supporterRequiredContent = new SupporterRequiredDrawable(), } } }, @@ -117,6 +121,13 @@ namespace osu.Game.Overlays private void onSearchFinished(List beatmaps) { + // non-supporter user used supporter-only filters + if (beatmaps == null) + { + LoadComponentAsync(supporterRequiredContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + return; + } + var newPanels = beatmaps.Select(b => new GridBeatmapPanel(b) { Anchor = Anchor.TopCentre, @@ -170,7 +181,7 @@ namespace osu.Game.Overlays { var transform = lastContent.FadeOut(100, Easing.OutQuint); - if (lastContent == notFoundContent) + if (lastContent == notFoundContent || lastContent == supporterRequiredContent) { // not found display may be used multiple times, so don't expire/dispose it. transform.Schedule(() => panelTarget.Remove(lastContent)); @@ -240,6 +251,107 @@ namespace osu.Game.Overlays } } + public class SupporterRequiredDrawable : CompositeDrawable + { + public SupporterRequiredDrawable() + { + RelativeSizeAxes = Axes.X; + Height = 250; + Alpha = 0; + Margin = new MarginPadding { Top = 15 }; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + AddInternal(new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Texture = textures.Get(@"Online/supporter-required"), + }, + createSupportRequiredText(), + } + }); + } + + private Drawable createSupportRequiredText() + { + LinkFlowContainer linkFlowContainer; + string[] text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault( + BeatmapsStrings.ListingSearchFiltersRank.ToString(), + "{1}" + ).ToString().Split("{1}"); + + // var titleContainer = new Container + // { + // RelativeSizeAxes = Axes.X, + // Margin = new MarginPadding { Vertical = 5 }, + // Children = new Drawable[] + // { + // linkFlowContainer = new LinkFlowContainer + // { + // Anchor = Anchor.Centre, + // Origin = Anchor.Centre, + // } + // } + // }; + + linkFlowContainer = new LinkFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding + { + Bottom = 10, + } + }; + + linkFlowContainer.AddText( + text[0], + t => + { + t.Font = OsuFont.GetFont(size: 16); + t.Colour = Colour4.White; + } + ); + + linkFlowContainer.AddLink( + BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), + "https://osu.ppy.sh/store/products/supporter-tag", + t => + { + t.Font = OsuFont.GetFont(size: 16); + t.Colour = Colour4.AliceBlue; + } + ); + + linkFlowContainer.AddText( + text[1], + t => + { + t.Font = OsuFont.GetFont(size: 16); + t.Colour = Colour4.White; + } + ); + + return linkFlowContainer; + } + } + private const double time_between_fetches = 500; private double lastFetchDisplayedTime; From c04b09520da8115080bec4dfc4310e9f549493e3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Jun 2021 20:06:28 +0300 Subject: [PATCH 467/670] Replace spinner approach circle proxying logic with hooking up to `OnSkinChange` in mod --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 15 ++++++-- .../Objects/Drawables/DrawableSpinner.cs | 5 ++- .../Skinning/IHasSpinnerApproachCircle.cs | 18 ++++++++++ .../Skinning/IProxiesApproachCircle.cs | 12 ------- .../Skinning/Legacy/LegacySpinner.cs | 13 ++++--- .../Legacy/LegacySpinnerApproachCircle.cs | 9 +++-- .../Skinning/SkinnableSpinnerBody.cs | 34 ------------------- 7 files changed, 45 insertions(+), 61 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs delete mode 100644 osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs delete mode 100644 osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index d712ffd92a..071c3dc3f1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Skinning; namespace osu.Game.Rulesets.Osu.Mods { @@ -110,8 +111,8 @@ namespace osu.Game.Rulesets.Osu.Mods // hide elements we don't care about. // todo: hide background - using (spinner.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt)) - spinner.ApproachCircle.Hide(); + spinner.Body.OnSkinChanged += () => hideSpinnerApproachCircle(spinner); + hideSpinnerApproachCircle(spinner); using (spinner.BeginAbsoluteSequence(fadeStartTime)) spinner.FadeOut(fadeDuration); @@ -163,5 +164,15 @@ namespace osu.Game.Rulesets.Osu.Mods } } } + + private static void hideSpinnerApproachCircle(DrawableSpinner spinner) + { + var approachCircle = (spinner.Body.Drawable as IHasSpinnerApproachCircle)?.ApproachCircle; + if (approachCircle == null) + return; + + using (spinner.BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt)) + approachCircle.Hide(); + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 4507b1520c..ec87d3bfdf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result; - public SkinnableDrawable ApproachCircle { get; private set; } + public SkinnableDrawable Body { get; private set; } public SpinnerRotationTracker RotationTracker { get; private set; } @@ -88,8 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RelativeSizeAxes = Axes.Y, Children = new Drawable[] { - ApproachCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerApproachCircle)), - new SkinnableSpinnerBody(ApproachCircle.CreateProxy(), _ => new DefaultSpinner()), + Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()), RotationTracker = new SpinnerRotationTracker(this) } }, diff --git a/osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs new file mode 100644 index 0000000000..dcfc15913c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs @@ -0,0 +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 osu.Framework.Graphics; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + /// + /// A common interface between implementations of the component that provide approach circles for the spinner. + /// + public interface IHasSpinnerApproachCircle + { + /// + /// The spinner approach circle. + /// + Drawable ApproachCircle { get; } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs deleted file mode 100644 index 3cc0026adc..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Containers; - -namespace osu.Game.Rulesets.Osu.Skinning -{ - public interface IProxiesApproachCircle - { - Container ApproachCircleTarget { get; } - } -} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index efbb27bf3f..5471de22d4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - public abstract class LegacySpinner : CompositeDrawable, IProxiesApproachCircle + public abstract class LegacySpinner : CompositeDrawable, IHasSpinnerApproachCircle { public const float SPRITE_SCALE = 0.625f; @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected DrawableSpinner DrawableSpinner { get; private set; } - public Container ApproachCircleTarget { get; private set; } + public Drawable ApproachCircle { get; private set; } private Sprite spin; private Sprite clear; @@ -59,11 +59,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Depth = float.MinValue, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { - ApproachCircleTarget = new Container + ApproachCircle = new LegacySpinnerApproachCircle { - RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_Y_CENTRE, }, spin = new Sprite { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs index 92454cefa3..d5e510cd69 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; -using static osu.Game.Rulesets.Osu.Skinning.Legacy.LegacySpinner; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { @@ -19,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private DrawableSpinner drawableSpinner; [CanBeNull] - private Sprite sprite; + private Sprite approachCircle; [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject, ISkinSource source) @@ -35,12 +34,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (spinnerProvider is DefaultLegacySkin) return; - InternalChild = sprite = new Sprite + InternalChild = approachCircle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-approachcircle"), - Scale = new Vector2(SPRITE_SCALE * 1.86f), + Scale = new Vector2(1.86f), }; } @@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return; using (BeginAbsoluteSequence(spinner.HitObject.StartTime)) - sprite?.ScaleTo(SPRITE_SCALE * 0.1f, spinner.HitObject.Duration); + approachCircle?.ScaleTo(0.1f, spinner.HitObject.Duration); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs b/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs deleted file mode 100644 index 763b9dd677..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Graphics; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Osu.Skinning -{ - /// - /// A skinnable drawable of the component, with the approach circle exposed for modification. - /// - public class SkinnableSpinnerBody : SkinnableDrawable - { - private readonly Drawable approachCircleProxy; - - public SkinnableSpinnerBody(Drawable approachCircleProxy, Func defaultImplementation = null) - : base(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), defaultImplementation) - { - this.approachCircleProxy = approachCircleProxy; - } - - protected override void SkinChanged(ISkinSource skin) - { - if (Drawable is IProxiesApproachCircle oldProxiesApproachCircle) - oldProxiesApproachCircle.ApproachCircleTarget.Remove(approachCircleProxy); - - base.SkinChanged(skin); - - if (Drawable is IProxiesApproachCircle newProxiesApproachCircle) - newProxiesApproachCircle.ApproachCircleTarget.Add(approachCircleProxy); - } - } -} From c3217fd8b1f4d9169f416577b3ca07e4ada8a76c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Jun 2021 20:10:32 +0300 Subject: [PATCH 468/670] Remove leftover approach circle skin component --- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 1 - .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 6 ------ 2 files changed, 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 687fc1f966..46e501758b 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -18,6 +18,5 @@ namespace osu.Game.Rulesets.Osu SliderBall, SliderBody, SpinnerBody, - SpinnerApproachCircle, } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index a8c42a3773..3267b48ebf 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -121,12 +121,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return new LegacyOldStyleSpinner(); return null; - - case OsuSkinComponents.SpinnerApproachCircle: - if (Source.GetTexture("spinner-approachcircle") != null) - return new LegacySpinnerApproachCircle(); - - return null; } } From 46b9fd9ac8a3778201586016d577db9d775524c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Jun 2021 09:36:18 +0200 Subject: [PATCH 469/670] Remove and ignore FodyWeavers schema files --- .gitignore | 3 +++ osu.Android/FodyWeavers.xsd | 34 ---------------------------------- osu.Game/FodyWeavers.xsd | 34 ---------------------------------- osu.iOS/FodyWeavers.xsd | 34 ---------------------------------- 4 files changed, 3 insertions(+), 102 deletions(-) delete mode 100644 osu.Android/FodyWeavers.xsd delete mode 100644 osu.Game/FodyWeavers.xsd delete mode 100644 osu.iOS/FodyWeavers.xsd diff --git a/.gitignore b/.gitignore index d122d25054..de6a3ac848 100644 --- a/.gitignore +++ b/.gitignore @@ -336,3 +336,6 @@ inspectcode /BenchmarkDotNet.Artifacts *.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd diff --git a/osu.Android/FodyWeavers.xsd b/osu.Android/FodyWeavers.xsd deleted file mode 100644 index 447878c551..0000000000 --- a/osu.Android/FodyWeavers.xsd +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - Disables anonymized usage information from being sent on build. Read more about what data is being collected and why here: https://github.com/realm/realm-dotnet/blob/master/Realm/Realm.Fody/Common/Analytics.cs - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/osu.Game/FodyWeavers.xsd b/osu.Game/FodyWeavers.xsd deleted file mode 100644 index 447878c551..0000000000 --- a/osu.Game/FodyWeavers.xsd +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - Disables anonymized usage information from being sent on build. Read more about what data is being collected and why here: https://github.com/realm/realm-dotnet/blob/master/Realm/Realm.Fody/Common/Analytics.cs - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/osu.iOS/FodyWeavers.xsd b/osu.iOS/FodyWeavers.xsd deleted file mode 100644 index 447878c551..0000000000 --- a/osu.iOS/FodyWeavers.xsd +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - Disables anonymized usage information from being sent on build. Read more about what data is being collected and why here: https://github.com/realm/realm-dotnet/blob/master/Realm/Realm.Fody/Common/Analytics.cs - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file From 1f383532f2a7bee5caa6bc496e887c8dce0979dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Jun 2021 09:37:23 +0200 Subject: [PATCH 470/670] Move FodyWeavers configuration to solution root --- osu.Android/FodyWeavers.xml => FodyWeavers.xml | 0 osu.Game/FodyWeavers.xml | 3 --- osu.iOS/FodyWeavers.xml | 3 --- 3 files changed, 6 deletions(-) rename osu.Android/FodyWeavers.xml => FodyWeavers.xml (100%) delete mode 100644 osu.Game/FodyWeavers.xml delete mode 100644 osu.iOS/FodyWeavers.xml diff --git a/osu.Android/FodyWeavers.xml b/FodyWeavers.xml similarity index 100% rename from osu.Android/FodyWeavers.xml rename to FodyWeavers.xml diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml deleted file mode 100644 index cc07b89533..0000000000 --- a/osu.Game/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/osu.iOS/FodyWeavers.xml b/osu.iOS/FodyWeavers.xml deleted file mode 100644 index cc07b89533..0000000000 --- a/osu.iOS/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file From 32bd3107e1929cd8b9f1bb5f636ced0b8175da1f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Jun 2021 17:07:41 +0900 Subject: [PATCH 471/670] Remove high performance GC setting --- osu.Game/Performance/HighPerformanceSession.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Performance/HighPerformanceSession.cs b/osu.Game/Performance/HighPerformanceSession.cs index 96e67669c5..661c1046f1 100644 --- a/osu.Game/Performance/HighPerformanceSession.cs +++ b/osu.Game/Performance/HighPerformanceSession.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.Runtime; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -11,7 +10,6 @@ namespace osu.Game.Performance public class HighPerformanceSession : Component { private readonly IBindable localUserPlaying = new Bindable(); - private GCLatencyMode originalGCMode; [BackgroundDependencyLoader] private void load(OsuGame game) @@ -34,14 +32,10 @@ namespace osu.Game.Performance protected virtual void EnableHighPerformanceSession() { - originalGCMode = GCSettings.LatencyMode; - GCSettings.LatencyMode = GCLatencyMode.LowLatency; } protected virtual void DisableHighPerformanceSession() { - if (GCSettings.LatencyMode == GCLatencyMode.LowLatency) - GCSettings.LatencyMode = originalGCMode; } } } From 42fdfbb9a1d2504da309c96ca23cdee4829819b2 Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Sun, 20 Jun 2021 17:17:07 +0800 Subject: [PATCH 472/670] added visual tests --- .../Online/TestSceneBeatmapListingOverlay.cs | 74 +++++++++++++++++++ osu.Game/Overlays/BeatmapListingOverlay.cs | 32 +++----- 2 files changed, 83 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 156d6b744e..86008ce173 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -14,6 +14,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { @@ -58,6 +59,79 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); } + [Test] + public void TestSupporterOnlyFiltersPlaceholder() { + + AddStep("toggle non-supporter", () => + { + // non-supporter user + ((DummyAPIAccess)API).LocalUser.Value = new User + { + Username = API.LocalUser.Value.Username, + Id = API.LocalUser.Value.Id + 1, + IsSupporter = false, + }; + }); + AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); + + AddStep("toggle Random Rank Achieved filter", () => { + overlay.ChildrenOfType().Single().Ranks.Clear(); + Scoring.ScoreRank r = (Scoring.ScoreRank)(TestContext.CurrentContext.Random.NextShort() % 8); + overlay.ChildrenOfType().Single().Ranks.Add(r); + // overlay.ChildrenOfType().Single().Ranks. + }); + + AddUntilStep("supporter-placeholder show", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + + AddStep("Clear Rank Achieved filter", () => { + overlay.ChildrenOfType().Single().Ranks.Clear(); + }); + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("toggle Random Played filter", () => { + SearchPlayed r = (SearchPlayed)(TestContext.CurrentContext.Random.NextShort() % 2 + 1); + overlay.ChildrenOfType().Single().Played.Value = r; + }); + + AddUntilStep("supporter-placeholder show", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + + AddStep("Clear Played filter", () => { + overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any; + }); + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("toggle supporter", () => + { + // supporter user + ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true; + }); + + AddStep("toggle Random Rank Achieved filter", () => { + overlay.ChildrenOfType().Single().Ranks.Clear(); + Scoring.ScoreRank r = (Scoring.ScoreRank)(TestContext.CurrentContext.Random.NextShort() % 8); + overlay.ChildrenOfType().Single().Ranks.Add(r); + }); + + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("Clear Rank Achieved filter", () => { + overlay.ChildrenOfType().Single().Ranks.Clear(); + }); + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("toggle Random Played filter", () => { + SearchPlayed r = (SearchPlayed)(TestContext.CurrentContext.Random.NextShort() % 2 + 1); + overlay.ChildrenOfType().Single().Played.Value = r; + }); + + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("Clear Played filter", () => { + overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any; + }); + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + } + private void fetchFor(params BeatmapSetInfo[] beatmaps) { setsForResponse.Clear(); diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 63b9d3d34a..3dec6de03a 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -181,11 +181,16 @@ namespace osu.Game.Overlays { var transform = lastContent.FadeOut(100, Easing.OutQuint); - if (lastContent == notFoundContent || lastContent == supporterRequiredContent) + if (lastContent == notFoundContent) { // not found display may be used multiple times, so don't expire/dispose it. transform.Schedule(() => panelTarget.Remove(lastContent)); } + else if (lastContent == supporterRequiredContent) + { + // supporter required display may be used multiple times, so don't expire/dispose it. + transform.Schedule(() => panelTarget.Remove(supporterRequiredContent)); + } else { // Consider the case when the new content is smaller than the last content. @@ -256,9 +261,8 @@ namespace osu.Game.Overlays public SupporterRequiredDrawable() { RelativeSizeAxes = Axes.X; - Height = 250; + Height = 225; Alpha = 0; - Margin = new MarginPadding { Top = 15 }; } [BackgroundDependencyLoader] @@ -271,7 +275,6 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), Children = new Drawable[] { new Sprite @@ -290,24 +293,7 @@ namespace osu.Game.Overlays private Drawable createSupportRequiredText() { LinkFlowContainer linkFlowContainer; - string[] text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault( - BeatmapsStrings.ListingSearchFiltersRank.ToString(), - "{1}" - ).ToString().Split("{1}"); - - // var titleContainer = new Container - // { - // RelativeSizeAxes = Axes.X, - // Margin = new MarginPadding { Vertical = 5 }, - // Children = new Drawable[] - // { - // linkFlowContainer = new LinkFlowContainer - // { - // Anchor = Anchor.Centre, - // Origin = Anchor.Centre, - // } - // } - // }; + string[] text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(BeatmapsStrings.ListingSearchFiltersRank.ToString(), "{1}").ToString().Split("{1}"); linkFlowContainer = new LinkFlowContainer { @@ -335,7 +321,7 @@ namespace osu.Game.Overlays t => { t.Font = OsuFont.GetFont(size: 16); - t.Colour = Colour4.AliceBlue; + t.Colour = Colour4.FromHex("#A6C8D9"); } ); From e7aeba8d039cd8b3dfcaad98ed440d1cb4e9f620 Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Sun, 20 Jun 2021 18:28:43 +0800 Subject: [PATCH 473/670] added more visual tests --- .../Online/TestSceneBeatmapListingOverlay.cs | 153 +++++++++++------- .../BeatmapListingFilterControl.cs | 1 + 2 files changed, 99 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 86008ce173..eebaea545a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -40,6 +40,16 @@ namespace osu.Game.Tests.Visual.Online return true; }; + + AddStep("initialize dummy", () => + { + // non-supporter user + ((DummyAPIAccess)API).LocalUser.Value = new User + { + Username = "TestBot", + Id = API.LocalUser.Value.Id + 1, + }; + }); } [Test] @@ -60,78 +70,95 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestSupporterOnlyFiltersPlaceholder() { + public void TestSupporterOnlyFiltersPlaceholderNoBeatmaps() + { + AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); - AddStep("toggle non-supporter", () => - { - // non-supporter user - ((DummyAPIAccess)API).LocalUser.Value = new User - { - Username = API.LocalUser.Value.Username, - Id = API.LocalUser.Value.Id + 1, - IsSupporter = false, - }; - }); - AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); + // test non-supporter on Rank Achieved filter + toggleRandomRankFilter(); + AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddStep("toggle Random Rank Achieved filter", () => { - overlay.ChildrenOfType().Single().Ranks.Clear(); - Scoring.ScoreRank r = (Scoring.ScoreRank)(TestContext.CurrentContext.Random.NextShort() % 8); - overlay.ChildrenOfType().Single().Ranks.Add(r); - // overlay.ChildrenOfType().Single().Ranks. - }); - - AddUntilStep("supporter-placeholder show", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - - AddStep("Clear Rank Achieved filter", () => { - overlay.ChildrenOfType().Single().Ranks.Clear(); - }); + AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - AddStep("toggle Random Played filter", () => { - SearchPlayed r = (SearchPlayed)(TestContext.CurrentContext.Random.NextShort() % 2 + 1); - overlay.ChildrenOfType().Single().Played.Value = r; - }); + // test non-supporter on Played filter + toggleRandomSupporterOnlyPlayedFilter(); + AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("supporter-placeholder show", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - - AddStep("Clear Played filter", () => { - overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any; - }); + AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - AddStep("toggle supporter", () => - { - // supporter user - ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true; - }); - - AddStep("toggle Random Rank Achieved filter", () => { - overlay.ChildrenOfType().Single().Ranks.Clear(); - Scoring.ScoreRank r = (Scoring.ScoreRank)(TestContext.CurrentContext.Random.NextShort() % 8); - overlay.ChildrenOfType().Single().Ranks.Add(r); - }); + AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); + // test supporter on Rank Achieved filter + toggleRandomRankFilter(); AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - AddStep("Clear Rank Achieved filter", () => { - overlay.ChildrenOfType().Single().Ranks.Clear(); - }); + AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - AddStep("toggle Random Played filter", () => { - SearchPlayed r = (SearchPlayed)(TestContext.CurrentContext.Random.NextShort() % 2 + 1); - overlay.ChildrenOfType().Single().Played.Value = r; - }); - + // test supporter on Played filter + toggleRandomSupporterOnlyPlayedFilter(); AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - AddStep("Clear Played filter", () => { - overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any; - }); + AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); } + [Test] + public void TestSupporterOnlyFiltersPlaceholderOneBeatmap() + { + AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); + AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); + + // test non-supporter on Rank Achieved filter + toggleRandomRankFilter(); + AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + // test non-supporter on Played filter + toggleRandomSupporterOnlyPlayedFilter(); + AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); + + // test supporter on Rank Achieved filter + toggleRandomRankFilter(); + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + // test supporter on Played filter + toggleRandomSupporterOnlyPlayedFilter(); + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + } + + private void fetchFor(params BeatmapSetInfo[] beatmaps) { setsForResponse.Clear(); @@ -141,6 +168,22 @@ namespace osu.Game.Tests.Visual.Online overlay.ChildrenOfType().Single().Query.TriggerChange(); } + private void toggleRandomRankFilter() + { + short r = TestContext.CurrentContext.Random.NextShort(); + AddStep("toggle Random Rank Achieved filter", () => + { + overlay.ChildrenOfType().Single().Ranks.Clear(); + overlay.ChildrenOfType().Single().Ranks.Add((Scoring.ScoreRank)(r % 8)); + }); + } + + private void toggleRandomSupporterOnlyPlayedFilter() + { + short r = TestContext.CurrentContext.Random.NextShort(); + AddStep("toggle Random Played filter", () => overlay.ChildrenOfType().Single().Played.Value = (SearchPlayed)(r % 2 + 1)); + } + private class TestAPIBeatmapSet : APIBeatmapSet { private readonly BeatmapSetInfo beatmapSet; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 3436a1b3b2..7b7f742b73 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -213,6 +213,7 @@ namespace osu.Game.Overlays.BeatmapListing lastResponse = response; getSetsRequest = null; + // check if an non-supporter user used supporter-only filters if (!api.LocalUser.Value.IsSupporter && (searchControl.Ranks.Any() || searchControl.Played.Value != SearchPlayed.Any)) { SearchFinished?.Invoke(null); From 996503eb2d350aeae8252e9504541c82300da234 Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Sun, 20 Jun 2021 21:23:54 +0800 Subject: [PATCH 474/670] fixed filter text display, added visual tests --- .../Online/TestSceneBeatmapListingOverlay.cs | 106 ++++++++++++------ .../BeatmapListingFilterControl.cs | 8 +- osu.Game/Overlays/BeatmapListingOverlay.cs | 45 ++++---- 3 files changed, 103 insertions(+), 56 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index eebaea545a..9128f72d6e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -72,45 +72,56 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestSupporterOnlyFiltersPlaceholderNoBeatmaps() { + AddStep("fetch for 0 beatmaps", () => fetchFor()); AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); // test non-supporter on Rank Achieved filter toggleRandomRankFilter(); - AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + expectedPlaceholderShown(true, false); AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + expectedPlaceholderShown(false, true); // test non-supporter on Played filter toggleRandomSupporterOnlyPlayedFilter(); - AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + expectedPlaceholderShown(true, false); AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + expectedPlaceholderShown(false, true); + + // test non-supporter on both Rank Achieved and Played filter + toggleRandomRankFilter(); + toggleRandomSupporterOnlyPlayedFilter(); + expectedPlaceholderShown(true, false); + + AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); + AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); + expectedPlaceholderShown(false, true); AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); // test supporter on Rank Achieved filter toggleRandomRankFilter(); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + expectedPlaceholderShown(false, true); AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + expectedPlaceholderShown(false, true); // test supporter on Played filter toggleRandomSupporterOnlyPlayedFilter(); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + expectedPlaceholderShown(false, true); AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + expectedPlaceholderShown(false, true); + + // test supporter on both Rank Achieved and Played filter + toggleRandomRankFilter(); + toggleRandomSupporterOnlyPlayedFilter(); + expectedPlaceholderShown(false, true); + + AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); + AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); + expectedPlaceholderShown(false, true); } [Test] @@ -121,41 +132,51 @@ namespace osu.Game.Tests.Visual.Online // test non-supporter on Rank Achieved filter toggleRandomRankFilter(); - AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + expectedPlaceholderShown(true, false); AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + expectedPlaceholderShown(false, false); // test non-supporter on Played filter toggleRandomSupporterOnlyPlayedFilter(); - AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + expectedPlaceholderShown(true, false); AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + expectedPlaceholderShown(false, false); + + // test non-supporter on both Rank Achieved and Played filter + toggleRandomRankFilter(); + toggleRandomSupporterOnlyPlayedFilter(); + expectedPlaceholderShown(true, false); + + AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); + AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); + expectedPlaceholderShown(false, false); AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); // test supporter on Rank Achieved filter toggleRandomRankFilter(); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + expectedPlaceholderShown(false, false); AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + expectedPlaceholderShown(false, false); // test supporter on Played filter toggleRandomSupporterOnlyPlayedFilter(); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + expectedPlaceholderShown(false, false); AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + expectedPlaceholderShown(false, false); + + // test supporter on both Rank Achieved and Played filter + toggleRandomRankFilter(); + toggleRandomSupporterOnlyPlayedFilter(); + expectedPlaceholderShown(false, false); + + AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); + AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); + expectedPlaceholderShown(false, false); } @@ -184,6 +205,27 @@ namespace osu.Game.Tests.Visual.Online AddStep("toggle Random Played filter", () => overlay.ChildrenOfType().Single().Played.Value = (SearchPlayed)(r % 2 + 1)); } + private void expectedPlaceholderShown(bool supporterRequiredShown, bool notFoundShown) + { + if (supporterRequiredShown) + { + AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + } + else + { + AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + } + + if (notFoundShown) + { + AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + } + else + { + AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); + } + } + private class TestAPIBeatmapSet : APIBeatmapSet { private readonly BeatmapSetInfo beatmapSet; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 7b7f742b73..6e83dc0bf4 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -24,9 +24,9 @@ namespace osu.Game.Overlays.BeatmapListing { /// /// Fired when a search finishes. Contains only new items in the case of pagination. - /// Null when non-supporter user used supporter-only filters + /// Fired with BeatmapListingSearchControl when non-supporter user used supporter-only filters. /// - public Action> SearchFinished; + public Action, BeatmapListingSearchControl> SearchFinished; /// /// Fired when search criteria change. @@ -216,11 +216,11 @@ namespace osu.Game.Overlays.BeatmapListing // check if an non-supporter user used supporter-only filters if (!api.LocalUser.Value.IsSupporter && (searchControl.Ranks.Any() || searchControl.Played.Value != SearchPlayed.Any)) { - SearchFinished?.Invoke(null); + SearchFinished?.Invoke(sets, searchControl); } else { - SearchFinished?.Invoke(sets); + SearchFinished?.Invoke(sets, null); } }; diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 3dec6de03a..90f1e6932d 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -119,11 +119,17 @@ namespace osu.Game.Overlays private Task panelLoadDelegate; - private void onSearchFinished(List beatmaps) + private void onSearchFinished(List beatmaps, BeatmapListingSearchControl searchControl) { // non-supporter user used supporter-only filters - if (beatmaps == null) + if (searchControl != null) { + // compose filter string + List filtersStrs = new List(); + if (searchControl.Ranks.Any()) filtersStrs.Add(BeatmapsStrings.ListingSearchFiltersRank.ToString()); + if (searchControl.Played.Value != SearchPlayed.Any) filtersStrs.Add(BeatmapsStrings.ListingSearchFiltersPlayed.ToString()); + supporterRequiredContent.UpdateSupportRequiredText(string.Join(" and ", filtersStrs)); + LoadComponentAsync(supporterRequiredContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); return; } @@ -258,11 +264,24 @@ namespace osu.Game.Overlays public class SupporterRequiredDrawable : CompositeDrawable { + private LinkFlowContainer linkFlowContainer; + public SupporterRequiredDrawable() { RelativeSizeAxes = Axes.X; Height = 225; Alpha = 0; + + linkFlowContainer = linkFlowContainer = new LinkFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding + { + Bottom = 10, + } + }; } [BackgroundDependencyLoader] @@ -285,27 +304,15 @@ namespace osu.Game.Overlays FillMode = FillMode.Fit, Texture = textures.Get(@"Online/supporter-required"), }, - createSupportRequiredText(), + linkFlowContainer, } }); } - private Drawable createSupportRequiredText() - { - LinkFlowContainer linkFlowContainer; - string[] text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(BeatmapsStrings.ListingSearchFiltersRank.ToString(), "{1}").ToString().Split("{1}"); - - linkFlowContainer = new LinkFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding - { - Bottom = 10, - } - }; + public void UpdateSupportRequiredText(string filtersStr) { + string[] text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(filtersStr, "{1}").ToString().Split("{1}"); + linkFlowContainer.Clear(); linkFlowContainer.AddText( text[0], t => @@ -333,8 +340,6 @@ namespace osu.Game.Overlays t.Colour = Colour4.White; } ); - - return linkFlowContainer; } } From d0a8b748238dfa03b9174db624c34bac0259ad2a Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Sun, 20 Jun 2021 21:28:57 +0800 Subject: [PATCH 475/670] fixed filter text order --- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 90f1e6932d..ceb033380c 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -126,8 +126,8 @@ namespace osu.Game.Overlays { // compose filter string List filtersStrs = new List(); - if (searchControl.Ranks.Any()) filtersStrs.Add(BeatmapsStrings.ListingSearchFiltersRank.ToString()); if (searchControl.Played.Value != SearchPlayed.Any) filtersStrs.Add(BeatmapsStrings.ListingSearchFiltersPlayed.ToString()); + if (searchControl.Ranks.Any()) filtersStrs.Add(BeatmapsStrings.ListingSearchFiltersRank.ToString()); supporterRequiredContent.UpdateSupportRequiredText(string.Join(" and ", filtersStrs)); LoadComponentAsync(supporterRequiredContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); From 72155a7c525818431fef2c76565eb3995a1f769a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 03:37:49 +0300 Subject: [PATCH 476/670] Replace if pattern-matching check with switch cases instead --- .../Skinning/Legacy/LegacySpinnerApproachCircle.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs index d5e510cd69..d0ce8fbb47 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs @@ -53,11 +53,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSpinner spinner)) - return; + switch (drawableHitObject) + { + case DrawableSpinner spinner: + using (BeginAbsoluteSequence(spinner.HitObject.StartTime)) + approachCircle?.ScaleTo(0.1f, spinner.HitObject.Duration); - using (BeginAbsoluteSequence(spinner.HitObject.StartTime)) - approachCircle?.ScaleTo(0.1f, spinner.HitObject.Duration); + break; + } } } } From 01478d780da7c2ca1da8b1d74a9fc1503b3435d5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 03:43:11 +0300 Subject: [PATCH 477/670] Generalize `IHasSpinnerApproachCircle` from being spinner-specifc --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 +- .../{IHasSpinnerApproachCircle.cs => IHasApproachCircle.cs} | 6 +++--- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename osu.Game.Rulesets.Osu/Skinning/{IHasSpinnerApproachCircle.cs => IHasApproachCircle.cs} (59%) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 071c3dc3f1..16b38cd0b1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.Mods private static void hideSpinnerApproachCircle(DrawableSpinner spinner) { - var approachCircle = (spinner.Body.Drawable as IHasSpinnerApproachCircle)?.ApproachCircle; + var approachCircle = (spinner.Body.Drawable as IHasApproachCircle)?.ApproachCircle; if (approachCircle == null) return; diff --git a/osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs similarity index 59% rename from osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs rename to osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs index dcfc15913c..88aa715ad9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs @@ -6,12 +6,12 @@ using osu.Framework.Graphics; namespace osu.Game.Rulesets.Osu.Skinning { /// - /// A common interface between implementations of the component that provide approach circles for the spinner. + /// A common interface between skin component implementations which provide an approach circle. /// - public interface IHasSpinnerApproachCircle + public interface IHasApproachCircle { /// - /// The spinner approach circle. + /// The approach circle drawable. /// Drawable ApproachCircle { get; } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 5471de22d4..37379f4646 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - public abstract class LegacySpinner : CompositeDrawable, IHasSpinnerApproachCircle + public abstract class LegacySpinner : CompositeDrawable, IHasApproachCircle { public const float SPRITE_SCALE = 0.625f; From 5cfd0e32236a5a63eaf4132e6fe9dea22569bf2c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 04:16:58 +0300 Subject: [PATCH 478/670] Remove implicit `LegacySkin` check and refactor anything using it --- .../Skinning/RulesetSkinProvidingContainer.cs | 31 +++++++++++++------ osu.Game/Tests/Visual/TestPlayer.cs | 2 +- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index f11acd981a..bbaeee98d8 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Skinning Ruleset = ruleset; Beatmap = beatmap; - InternalChild = new BeatmapSkinProvidingContainer(GetRulesetTransformedSkin(beatmapSkin)) + InternalChild = new BeatmapSkinProvidingContainer(beatmapSkin is LegacySkin ? GetLegacyRulesetTransformedSkin(beatmapSkin) : beatmapSkin) { Child = Content = new Container { @@ -54,23 +54,34 @@ namespace osu.Game.Skinning { SkinSources.Clear(); - SkinSources.Add(GetRulesetTransformedSkin(skinManager.CurrentSkin.Value)); + // TODO: we also want to insert a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. - // TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. - if (skinManager.CurrentSkin.Value is LegacySkin && skinManager.CurrentSkin.Value != skinManager.DefaultLegacySkin) - SkinSources.Add(GetRulesetTransformedSkin(skinManager.DefaultLegacySkin)); + switch (skinManager.CurrentSkin.Value) + { + case LegacySkin currentLegacySkin: + SkinSources.Add(GetLegacyRulesetTransformedSkin(currentLegacySkin)); + + if (currentLegacySkin != skinManager.DefaultLegacySkin) + SkinSources.Add(GetLegacyRulesetTransformedSkin(skinManager.DefaultLegacySkin)); + + break; + + default: + SkinSources.Add(skinManager.CurrentSkin.Value); + break; + } } - protected ISkin GetRulesetTransformedSkin(ISkin skin) + protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin) { - if (!(skin is LegacySkin)) - return skin; + if (legacySkin == null) + return null; - var rulesetTransformed = Ruleset.CreateLegacySkinProvider(skin, Beatmap); + var rulesetTransformed = Ruleset.CreateLegacySkinProvider(legacySkin, Beatmap); if (rulesetTransformed != null) return rulesetTransformed; - return skin; + return legacySkin; } } } diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index e1431b0658..2be5d8ac9f 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual base.UpdateSkins(); if (skin != null) - SkinSources.Insert(0, GetRulesetTransformedSkin(skin)); + SkinSources.Insert(0, skin is LegacySkin ? GetLegacyRulesetTransformedSkin(skin) : skin); } } } From e4705abee2bb7b2017838440cd187154e0feefbf Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 20 Jun 2021 20:07:46 -0700 Subject: [PATCH 479/670] Update custom rulesets directory link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3054f19e79..2213b42121 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ If your platform is not listed above, there is still a chance you can manually b osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates). -You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852). +You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096). ## Developing osu! From 11b78ad849d333f8cd0ad79267e7ed4506603daf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 04:27:09 +0300 Subject: [PATCH 480/670] Make `TestPlayer` skin assigning logic not flaky --- .../Beatmaps/LegacyBeatmapSkinColourTest.cs | 2 +- osu.Game/Tests/Visual/PlayerTestScene.cs | 2 +- osu.Game/Tests/Visual/TestPlayer.cs | 20 ++++++++++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index 1feb3eebbf..347b611579 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps ExposedPlayer player = CreateTestPlayer(); - player.Skin = new TestSkin(userHasCustomColours); + player.SetSkin(new TestSkin(userHasCustomColours)); LoadScreen(player); diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index e42a043eec..f5fad895e2 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual } Player = CreatePlayer(ruleset); - Player.Skin = GetPlayerSkin(); + Player.SetSkin(GetPlayerSkin()); LoadScreen(Player); } diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 2be5d8ac9f..19fd7068b9 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -22,8 +24,6 @@ namespace osu.Game.Tests.Visual /// public class TestPlayer : Player { - public ISkin Skin { get; set; } - protected override bool PauseOnFocusLost { get; } public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; @@ -81,8 +81,22 @@ namespace osu.Game.Tests.Visual ScoreProcessor.NewJudgement += r => Results.Add(r); } + public ISkin Skin { get; private set; } + + private TestSkinProvidingContainer rulesetSkinProvider; + + internal void SetSkin(ISkin skin) + { + Debug.Assert(rulesetSkinProvider == null); + + if (Skin != null) + throw new InvalidOperationException("A skin has already been set."); + + Skin = skin; + } + protected override RulesetSkinProvidingContainer CreateRulesetSkinProvider(Ruleset ruleset, IBeatmap beatmap, ISkin beatmapSkin) - => new TestSkinProvidingContainer(Skin, ruleset, beatmap, beatmapSkin); + => rulesetSkinProvider = new TestSkinProvidingContainer(Skin, ruleset, beatmap, beatmapSkin); private class TestSkinProvidingContainer : RulesetSkinProvidingContainer { From 68e28f4903937d9c2ddd821b1147df9ac9af192f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 07:35:07 +0300 Subject: [PATCH 481/670] Implement `IHasApproachCircle` in `DrawableHitCircle` as well --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 5 ++++- osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index ca2e6578db..46fc8f99b2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -12,6 +12,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; @@ -19,7 +20,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece + public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece, IHasApproachCircle { public OsuAction? HitAction => HitArea.HitAction; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; @@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public HitReceptor HitArea { get; private set; } public SkinnableDrawable CirclePiece { get; private set; } + Drawable IHasApproachCircle.ApproachCircle => ApproachCircle; + private Container scaleContainer; private InputManager inputManager; diff --git a/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs index 88aa715ad9..7fbc5b144b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; namespace osu.Game.Rulesets.Osu.Skinning { /// - /// A common interface between skin component implementations which provide an approach circle. + /// A common interface between implementations which provide an approach circle. /// public interface IHasApproachCircle { From 8b2110c048d28a79244581e0d10fbf33ba1a578f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 20 Jun 2021 21:36:08 -0700 Subject: [PATCH 482/670] Add failing discussion links test --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index ecb37706b0..2c2c4dc24e 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -28,6 +28,8 @@ namespace osu.Game.Tests.Chat [TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")] [TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")] [TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/abc", "https://dev.ppy.sh/beatmapsets/abc")] + [TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions", "https://dev.ppy.sh/beatmapsets/discussions")] + [TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions/123", "https://dev.ppy.sh/beatmapsets/discussions/123")] public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link) { MessageFormatter.WebsiteRootUrl = "dev.ppy.sh"; From 6fda5e569ad5b4489aacd23c529570f870efd90e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 20 Jun 2021 21:37:00 -0700 Subject: [PATCH 483/670] Fix beatmap discussion links wrongly leading to beatmap page --- osu.Game/Online/Chat/MessageFormatter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index df14d7eb1c..faee08742b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -154,6 +154,10 @@ namespace osu.Game.Online.Chat case "beatmapsets": case "d": { + if (mainArg == "discussions") + // handle discussion links externally for now + return new LinkDetails(LinkAction.External, url); + if (args.Length > 4 && int.TryParse(args[4], out var id)) // https://osu.ppy.sh/beatmapsets/1154158#osu/2768184 return new LinkDetails(LinkAction.OpenBeatmap, id.ToString()); From 42edbe4fb91741f22a076e3f7cc474ab2ddb6ef3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 08:40:38 +0300 Subject: [PATCH 484/670] Move `ApproachCircle` implementation into per-style --- .../Skinning/Legacy/LegacyNewStyleSpinner.cs | 31 ++++++++++++++----- .../Skinning/Legacy/LegacyOldStyleSpinner.cs | 12 ++++++- .../Skinning/Legacy/LegacySpinner.cs | 14 +++------ 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index 22fb3aab86..09b0d83bb4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -31,12 +31,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Container scaleContainer; + public override Drawable ApproachCircle { get; protected set; } + [BackgroundDependencyLoader] private void load(ISkinSource source) { AddInternal(scaleContainer = new Container { - Scale = new Vector2(SPRITE_SCALE), Anchor = Anchor.TopCentre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, @@ -48,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-glow"), + Scale = new Vector2(SPRITE_SCALE), Blending = BlendingParameters.Additive, Colour = glowColour, }, @@ -55,28 +57,43 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-bottom") + Texture = source.GetTexture("spinner-bottom"), + Scale = new Vector2(SPRITE_SCALE), }, discTop = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-top") + Texture = source.GetTexture("spinner-top"), + Scale = new Vector2(SPRITE_SCALE), }, fixedMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-middle") + Texture = source.GetTexture("spinner-middle"), + Scale = new Vector2(SPRITE_SCALE), }, spinningMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-middle2") - } + Texture = source.GetTexture("spinner-middle2"), + Scale = new Vector2(SPRITE_SCALE), + }, } }); + + if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin)) + { + scaleContainer.Add(ApproachCircle = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-approachcircle"), + Scale = new Vector2(SPRITE_SCALE * 1.86f), + }); + } } protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -126,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy glow.Alpha = DrawableSpinner.Progress; - scaleContainer.Scale = new Vector2(SPRITE_SCALE * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f)); + scaleContainer.Scale = new Vector2(0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs index d80e061662..8f162806de 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs @@ -29,12 +29,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private const float final_metre_height = 692 * SPRITE_SCALE; + public override Drawable ApproachCircle { get; protected set; } + [BackgroundDependencyLoader] private void load(ISkinSource source) { spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true; - AddRangeInternal(new Drawable[] + AddRangeInternal(new[] { new Sprite { @@ -68,6 +70,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.TopLeft, Scale = new Vector2(SPRITE_SCALE) } + }, + ApproachCircle = new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-approachcircle"), + Scale = new Vector2(SPRITE_SCALE * 1.86f), + Y = SPINNER_Y_CENTRE, } }); } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 37379f4646..3bbb523cfe 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected DrawableSpinner DrawableSpinner { get; private set; } - public Drawable ApproachCircle { get; private set; } + public abstract Drawable ApproachCircle { get; protected set; } private Sprite spin; private Sprite clear; @@ -59,15 +59,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Depth = float.MinValue, RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { - ApproachCircle = new LegacySpinnerApproachCircle - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - Scale = new Vector2(SPRITE_SCALE), - Y = SPINNER_Y_CENTRE, - }, spin = new Sprite { Anchor = Anchor.TopCentre, @@ -184,6 +177,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); } + using (BeginAbsoluteSequence(d.HitObject.StartTime)) + ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration); + double spinFadeOutLength = Math.Min(400, d.HitObject.Duration); using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true)) From 036b745425441007a7aa9c377aff4697622b0295 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 08:41:10 +0300 Subject: [PATCH 485/670] Remove no longer needed `LegacySpinnerApproachCircle` --- .../Legacy/LegacySpinnerApproachCircle.cs | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs deleted file mode 100644 index d0ce8fbb47..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Skinning; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Skinning.Legacy -{ - public class LegacySpinnerApproachCircle : CompositeDrawable - { - private DrawableSpinner drawableSpinner; - - [CanBeNull] - private Sprite approachCircle; - - [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableHitObject, ISkinSource source) - { - drawableSpinner = (DrawableSpinner)drawableHitObject; - - AutoSizeAxes = Axes.Both; - - var spinnerProvider = source.FindProvider(s => - s.GetTexture("spinner-circle") != null || - s.GetTexture("spinner-top") != null); - - if (spinnerProvider is DefaultLegacySkin) - return; - - InternalChild = approachCircle = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-approachcircle"), - Scale = new Vector2(1.86f), - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); - } - - private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) - { - switch (drawableHitObject) - { - case DrawableSpinner spinner: - using (BeginAbsoluteSequence(spinner.HitObject.StartTime)) - approachCircle?.ScaleTo(0.1f, spinner.HitObject.Duration); - - break; - } - } - } -} From 0707642b76d94b81f3ef061bc6f207f7edf85764 Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Mon, 21 Jun 2021 14:25:20 +0800 Subject: [PATCH 486/670] fixed SupporterRequiredDrawable --- osu.Game/Overlays/BeatmapListingOverlay.cs | 89 +++++++++------------- 1 file changed, 34 insertions(+), 55 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index ceb033380c..5ddad1c9fc 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -121,25 +122,20 @@ namespace osu.Game.Overlays private void onSearchFinished(List beatmaps, BeatmapListingSearchControl searchControl) { - // non-supporter user used supporter-only filters - if (searchControl != null) - { - // compose filter string - List filtersStrs = new List(); - if (searchControl.Played.Value != SearchPlayed.Any) filtersStrs.Add(BeatmapsStrings.ListingSearchFiltersPlayed.ToString()); - if (searchControl.Ranks.Any()) filtersStrs.Add(BeatmapsStrings.ListingSearchFiltersRank.ToString()); - supporterRequiredContent.UpdateSupportRequiredText(string.Join(" and ", filtersStrs)); - - LoadComponentAsync(supporterRequiredContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); - return; - } - var newPanels = beatmaps.Select(b => new GridBeatmapPanel(b) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }); + // non-supporter user used supporter-only filters + if (searchControl != null) + { + supporterRequiredContent.UpdateText(searchControl.Played.Value != SearchPlayed.Any, searchControl.Ranks.Any()); + LoadComponentAsync(supporterRequiredContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + return; + } + if (filterControl.CurrentPage == 0) { //No matches case @@ -262,26 +258,16 @@ namespace osu.Game.Overlays } } + // using string literals as there's no proper processing for LocalizeStrings yet public class SupporterRequiredDrawable : CompositeDrawable { - private LinkFlowContainer linkFlowContainer; + private OsuSpriteText supporterRequiredText; public SupporterRequiredDrawable() { RelativeSizeAxes = Axes.X; Height = 225; Alpha = 0; - - linkFlowContainer = linkFlowContainer = new LinkFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding - { - Bottom = 10, - } - }; } [BackgroundDependencyLoader] @@ -304,42 +290,35 @@ namespace osu.Game.Overlays FillMode = FillMode.Fit, Texture = textures.Get(@"Online/supporter-required"), }, - linkFlowContainer, + supporterRequiredText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Bottom = 10 }, + }, + createSupporterTagLink(), } }); } - public void UpdateSupportRequiredText(string filtersStr) { - string[] text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(filtersStr, "{1}").ToString().Split("{1}"); + public void UpdateText(bool playedFilter, bool rankFilter) { + List filters = new List(); + if (playedFilter) filters.Add(BeatmapsStrings.ListingSearchFiltersPlayed.ToString()); + if (rankFilter) filters.Add(BeatmapsStrings.ListingSearchFiltersRank.ToString()); + supporterRequiredText.Text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(); + } - linkFlowContainer.Clear(); - linkFlowContainer.AddText( - text[0], - t => - { - t.Font = OsuFont.GetFont(size: 16); - t.Colour = Colour4.White; - } - ); + public Drawable createSupporterTagLink() { + LinkFlowContainer supporterTagLink = new LinkFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Bottom = 10 }, + }; - linkFlowContainer.AddLink( - BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), - "https://osu.ppy.sh/store/products/supporter-tag", - t => - { - t.Font = OsuFont.GetFont(size: 16); - t.Colour = Colour4.FromHex("#A6C8D9"); - } - ); - - linkFlowContainer.AddText( - text[1], - t => - { - t.Font = OsuFont.GetFont(size: 16); - t.Colour = Colour4.White; - } - ); + supporterTagLink.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), Online.Chat.LinkAction.External, "https://osu.ppy.sh/store/products/supporter-tag"); + return supporterTagLink; } } From 33674563041102d02722fd98366eab6d3c48aa92 Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Mon, 21 Jun 2021 14:30:54 +0800 Subject: [PATCH 487/670] fixed SupporterRequiredDrawable style --- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 5ddad1c9fc..42a7253276 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -294,6 +294,8 @@ namespace osu.Game.Overlays { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 16), + Colour = Colour4.White, Margin = new MarginPadding { Bottom = 10 }, }, createSupporterTagLink(), From b42aedeb8171352bb56ed5334b0a347f43865335 Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Mon, 21 Jun 2021 14:43:54 +0800 Subject: [PATCH 488/670] fixed code style --- osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 9128f72d6e..2146ea333a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -179,7 +179,6 @@ namespace osu.Game.Tests.Visual.Online expectedPlaceholderShown(false, false); } - private void fetchFor(params BeatmapSetInfo[] beatmaps) { setsForResponse.Clear(); From 22cc1e14527c14bb2aa16052cdd5bb3ce27072dc Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Mon, 21 Jun 2021 15:31:47 +0800 Subject: [PATCH 489/670] fixed code style base on code analysis --- osu.Game/Overlays/BeatmapListingOverlay.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 42a7253276..407b737db5 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; -using osu.Framework.Localisation; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -280,7 +279,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Horizontal, - Children = new Drawable[] + Children = new[] { new Sprite { @@ -303,14 +302,16 @@ namespace osu.Game.Overlays }); } - public void UpdateText(bool playedFilter, bool rankFilter) { + public void UpdateText(bool playedFilter, bool rankFilter) + { List filters = new List(); if (playedFilter) filters.Add(BeatmapsStrings.ListingSearchFiltersPlayed.ToString()); if (rankFilter) filters.Add(BeatmapsStrings.ListingSearchFiltersRank.ToString()); supporterRequiredText.Text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(); } - public Drawable createSupporterTagLink() { + private Drawable createSupporterTagLink() + { LinkFlowContainer supporterTagLink = new LinkFlowContainer { Anchor = Anchor.Centre, From ba15f7c19b1c13b59a8eec859a0312efec07aba9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 10:47:38 +0300 Subject: [PATCH 490/670] Move `ApproachCircle` out of the scale container and revert relevant changes --- .../Skinning/Legacy/LegacyNewStyleSpinner.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index 09b0d83bb4..82a2598443 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { AddInternal(scaleContainer = new Container { + Scale = new Vector2(SPRITE_SCALE), Anchor = Anchor.TopCentre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, @@ -49,7 +50,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-glow"), - Scale = new Vector2(SPRITE_SCALE), Blending = BlendingParameters.Additive, Colour = glowColour, }, @@ -58,40 +58,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-bottom"), - Scale = new Vector2(SPRITE_SCALE), }, discTop = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-top"), - Scale = new Vector2(SPRITE_SCALE), }, fixedMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-middle"), - Scale = new Vector2(SPRITE_SCALE), }, spinningMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-middle2"), - Scale = new Vector2(SPRITE_SCALE), }, } }); if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin)) { - scaleContainer.Add(ApproachCircle = new Sprite + AddInternal(ApproachCircle = new Sprite { - Anchor = Anchor.Centre, + Anchor = Anchor.TopCentre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-approachcircle"), Scale = new Vector2(SPRITE_SCALE * 1.86f), + Y = SPINNER_Y_CENTRE, }); } } @@ -143,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy glow.Alpha = DrawableSpinner.Progress; - scaleContainer.Scale = new Vector2(0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f); + scaleContainer.Scale = new Vector2(SPRITE_SCALE * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f)); } } } From b162da5ee0265726f99b8c847b7b31512cae0e88 Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Mon, 21 Jun 2021 15:47:47 +0800 Subject: [PATCH 491/670] minor code change --- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 407b737db5..578e70e630 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -320,7 +320,7 @@ namespace osu.Game.Overlays Margin = new MarginPadding { Bottom = 10 }, }; - supporterTagLink.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), Online.Chat.LinkAction.External, "https://osu.ppy.sh/store/products/supporter-tag"); + supporterTagLink.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), "https://osu.ppy.sh/store/products/supporter-tag"); return supporterTagLink; } } From 999bf27eae3540067220c61ac64c6c6c7001ab9d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 12:07:00 +0300 Subject: [PATCH 492/670] Remove unnecessary abstraction of `ApproachCircle` property --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs | 2 -- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs | 2 -- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index 82a2598443..ae8d6a61f8 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -31,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Container scaleContainer; - public override Drawable ApproachCircle { get; protected set; } - [BackgroundDependencyLoader] private void load(ISkinSource source) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs index 8f162806de..cbe721d21d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs @@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private const float final_metre_height = 692 * SPRITE_SCALE; - public override Drawable ApproachCircle { get; protected set; } - [BackgroundDependencyLoader] private void load(ISkinSource source) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 3bbb523cfe..317649785e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected DrawableSpinner DrawableSpinner { get; private set; } - public abstract Drawable ApproachCircle { get; protected set; } + public Drawable ApproachCircle { get; protected set; } private Sprite spin; private Sprite clear; From 3eb088f89a2fd3cc3b6490574a6a6963715ecc3f Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:30:04 +0200 Subject: [PATCH 493/670] Add low difficulty overlaps check --- .../Edit/Checks/CheckLowDiffOverlaps.cs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs new file mode 100644 index 0000000000..488bdfd972 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs @@ -0,0 +1,107 @@ +// 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.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Edit.Checks +{ + public class CheckLowDiffOverlaps : ICheck + { + // For the lowest difficulties, the osu! Ranking Criteria encourages overlapping ~180 BPM 1/2, but discourages ~180 BPM 1/1. + private const double should_overlap_threshold = 150; // 200 BPM 1/2 + private const double should_probably_overlap_threshold = 175; // 170 BPM 1/2 + private const double should_not_overlap_threshold = 250; // 120 BPM 1/2 = 240 BPM 1/1 + + // Objects need to overlap this much before being treated as an overlap, else it may just be the borders slightly touching. + private const double overlap_leniency = 5; + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Missing or unexpected overlaps"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateShouldOverlap(this), + new IssueTemplateShouldProbablyOverlap(this), + new IssueTemplateShouldNotOverlap(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + // TODO: This should also apply to *lowest difficulty* Normals - they are skipped for now. + if (context.InterpretedDifficulty > DifficultyRating.Easy) + yield break; + + var hitObjects = context.Beatmap.HitObjects; + + for (int i = 0; i < hitObjects.Count - 1; ++i) + { + if (!(hitObjects[i] is OsuHitObject hitObject) || hitObject is Spinner) + continue; + + if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner) + continue; + + var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime(); + if (deltaTime >= hitObject.TimeFadeIn + hitObject.TimePreempt) + // The objects are not visible at the same time (without mods), hence skipping. + continue; + + var distanceSq = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthSquared; + var diameter = (hitObject.Radius - overlap_leniency) * 2; + var diameterSq = diameter * diameter; + + bool areOverlapping = distanceSq < diameterSq; + + // Slider ends do not need to be overlapped because of slider leniency. + if (!areOverlapping && !(hitObject is Slider)) + { + if (deltaTime < should_overlap_threshold) + yield return new IssueTemplateShouldOverlap(this).Create(deltaTime, hitObject, nextHitObject); + else if (deltaTime < should_probably_overlap_threshold) + yield return new IssueTemplateShouldProbablyOverlap(this).Create(deltaTime, hitObject, nextHitObject); + } + + if (areOverlapping && deltaTime > should_not_overlap_threshold) + yield return new IssueTemplateShouldNotOverlap(this).Create(deltaTime, hitObject, nextHitObject); + } + } + + public abstract class IssueTemplateOverlap : IssueTemplate + { + protected IssueTemplateOverlap(ICheck check, IssueType issueType, string unformattedMessage) + : base(check, issueType, unformattedMessage) + { + } + + public Issue Create(double deltaTime, params HitObject[] hitObjects) => new Issue(hitObjects, this, deltaTime); + } + + public class IssueTemplateShouldOverlap : IssueTemplateOverlap + { + public IssueTemplateShouldOverlap(ICheck check) + : base(check, IssueType.Problem, "These are {0} ms apart and so should be overlapping.") + { + } + } + + public class IssueTemplateShouldProbablyOverlap : IssueTemplateOverlap + { + public IssueTemplateShouldProbablyOverlap(ICheck check) + : base(check, IssueType.Warning, "These are {0} ms apart and so should probably be overlapping.") + { + } + } + + public class IssueTemplateShouldNotOverlap : IssueTemplateOverlap + { + public IssueTemplateShouldNotOverlap(ICheck check) + : base(check, IssueType.Problem, "These are {0} ms apart and so should NOT be overlapping.") + { + } + } + } +} From fcb918d0e11868c7bfd3954939b2143165bf4e45 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:30:23 +0200 Subject: [PATCH 494/670] Add time distance equality check --- .../Edit/Checks/CheckTimeDistanceEquality.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs new file mode 100644 index 0000000000..db48878dd3 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs @@ -0,0 +1,157 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Edit.Checks +{ + public class CheckTimeDistanceEquality : ICheck + { + private const double pattern_lifetime = 600; // Two objects this many ms apart or more are skipped. (200 BPM 2/1) + private const double stack_leniency = 12; // Two objects this distance apart or less are skipped. + + private const double observation_lifetime = 4000; // How long an observation is relevant for comparison. (120 BPM 8/1) + private const double similar_time_leniency = 16; // How different two delta times can be to still be compared. (240 BPM 1/16) + + private const double distance_leniency_absolute_warning = 10; // How many pixels are subtracted from the difference between current and expected distance. + private const double distance_leniency_percent_warning = 0.15; // How much of the current distance that the difference can make out. + private const double distance_leniency_absolute_problem = 20; + private const double distance_leniency_percent_problem = 0.3; + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Object too close or far away from previous"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateIrregularSpacingProblem(this), + new IssueTemplateIrregularSpacingWarning(this) + }; + + /// + /// Represents an observation of the time and distance between two objects. + /// + private readonly struct ObservedTimeDistance + { + public readonly double ObservationTime; + public readonly double DeltaTime; + public readonly double Distance; + + public ObservedTimeDistance(double observationTime, double deltaTime, double distance) + { + ObservationTime = observationTime; + DeltaTime = deltaTime; + Distance = distance; + } + } + + public IEnumerable Run(BeatmapVerifierContext context) + { + if (context.InterpretedDifficulty > DifficultyRating.Normal) + yield break; + + var prevObservedTimeDistances = new List(); + var hitObjects = context.Beatmap.HitObjects; + + for (int i = 0; i < hitObjects.Count - 1; ++i) + { + if (!(hitObjects[i] is OsuHitObject hitObject) || hitObject is Spinner) + continue; + + if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner) + continue; + + var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime(); + + // Ignore objects that are far enough apart in time to not be considered the same pattern. + if (deltaTime > pattern_lifetime) + continue; + + // Relying on FastInvSqrt is probably good enough here. We'll be taking the difference between distances later, hence square not being sufficient. + var distance = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthFast; + + // Ignore stacks and half-stacks, as these are close enough to where they can't be confused for being time-distanced. + if (distance < stack_leniency) + continue; + + var observedTimeDistance = new ObservedTimeDistance(nextHitObject.StartTime, deltaTime, distance); + var expectedDistance = getExpectedDistance(prevObservedTimeDistances, observedTimeDistance); + + if (expectedDistance == 0) + { + // There was nothing relevant to compare to. + prevObservedTimeDistances.Add(observedTimeDistance); + continue; + } + + if ((Math.Abs(expectedDistance - distance) - distance_leniency_absolute_problem) / distance > distance_leniency_percent_problem) + yield return new IssueTemplateIrregularSpacingProblem(this).Create(expectedDistance, distance, hitObject, nextHitObject); + else if ((Math.Abs(expectedDistance - distance) - distance_leniency_absolute_warning) / distance > distance_leniency_percent_warning) + yield return new IssueTemplateIrregularSpacingWarning(this).Create(expectedDistance, distance, hitObject, nextHitObject); + else + { + // We use `else` here to prevent issues from cascading; an object spaced too far could cause regular spacing to be considered "too short" otherwise. + prevObservedTimeDistances.Add(observedTimeDistance); + } + } + } + + private double getExpectedDistance(IEnumerable prevObservedTimeDistances, ObservedTimeDistance observedTimeDistance) + { + var observations = prevObservedTimeDistances.Count(); + + int count = 0; + double sum = 0; + + // Looping this in reverse allows us to break before going through all elements, as we're only interested in the most recent ones. + for (int i = observations - 1; i >= 0; --i) + { + var prevObservedTimeDistance = prevObservedTimeDistances.ElementAt(i); + + // Only consider observations within the last few seconds - this allows the map to build spacing up/down over time, but prevents it from being too sudden. + if (observedTimeDistance.ObservationTime - prevObservedTimeDistance.ObservationTime > observation_lifetime) + break; + + // Only consider observations which have a similar time difference - this leniency allows handling of multi-BPM maps which speed up/down slowly. + if (Math.Abs(observedTimeDistance.DeltaTime - prevObservedTimeDistance.DeltaTime) > similar_time_leniency) + break; + + count += 1; + sum += prevObservedTimeDistance.Distance / Math.Max(prevObservedTimeDistance.DeltaTime, 1); + } + + return sum / Math.Max(count, 1) * observedTimeDistance.DeltaTime; + } + + public abstract class IssueTemplateIrregularSpacing : IssueTemplate + { + protected IssueTemplateIrregularSpacing(ICheck check, IssueType issueType) + : base(check, issueType, "Expected {0:0} px spacing like previous objects, currently {1:0}.") + { + } + + public Issue Create(double expected, double actual, params HitObject[] hitObjects) => new Issue(hitObjects, this, expected, actual); + } + + public class IssueTemplateIrregularSpacingProblem : IssueTemplateIrregularSpacing + { + public IssueTemplateIrregularSpacingProblem(ICheck check) + : base(check, IssueType.Problem) + { + } + } + + public class IssueTemplateIrregularSpacingWarning : IssueTemplateIrregularSpacing + { + public IssueTemplateIrregularSpacingWarning(ICheck check) + : base(check, IssueType.Warning) + { + } + } + } +} From 2f3f4f3e4b8b660b05d6652a9b1261f09901bcea Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:30:45 +0200 Subject: [PATCH 495/670] Add new checks to verifier --- osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs index 04e881fbf3..896e904f3f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs @@ -13,7 +13,12 @@ namespace osu.Game.Rulesets.Osu.Edit { private readonly List checks = new List { - new CheckOffscreenObjects() + // Compose + new CheckOffscreenObjects(), + + // Spread + new CheckTimeDistanceEquality(), + new CheckLowDiffOverlaps() }; public IEnumerable Run(BeatmapVerifierContext context) From e11139eadfe1a6d0e09e247abc02915fede9f21f Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:33:50 +0200 Subject: [PATCH 496/670] Add low difficulty overlap tests Moq is introduced to mock sliders' end time/position. This is already used similarly in `osu.Game.Tests`. --- .../Editor/Checks/CheckLowDiffOverlapsTest.cs | 260 ++++++++++++++++++ .../osu.Game.Rulesets.Osu.Tests.csproj | 1 + 2 files changed, 261 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckLowDiffOverlapsTest.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckLowDiffOverlapsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckLowDiffOverlapsTest.cs new file mode 100644 index 0000000000..fd17d11d10 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckLowDiffOverlapsTest.cs @@ -0,0 +1,260 @@ +// 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 Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Edit.Checks; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks +{ + [TestFixture] + public class CheckLowDiffOverlapsTest + { + private CheckLowDiffOverlaps check; + + [SetUp] + public void Setup() + { + check = new CheckLowDiffOverlaps(); + } + + [Test] + public void TestNoOverlapFarApart() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(200, 0) } + } + }); + } + + [Test] + public void TestNoOverlapClose() + { + assertShouldProbablyOverlap(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 167, Position = new Vector2(200, 0) } + } + }); + } + + [Test] + public void TestNoOverlapTooClose() + { + assertShouldOverlap(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 100, Position = new Vector2(200, 0) } + } + }); + } + + [Test] + public void TestNoOverlapTooCloseExpert() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 100, Position = new Vector2(200, 0) } + } + }, DifficultyRating.Expert); + } + + [Test] + public void TestOverlapClose() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 167, Position = new Vector2(20, 0) } + } + }); + } + + [Test] + public void TestOverlapFarApart() + { + assertShouldNotOverlap(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(20, 0) } + } + }); + } + + [Test] + public void TestAlmostOverlapFarApart() + { + assertOk(new Beatmap + { + HitObjects = new List + { + // Default circle diameter is 128 px, but part of that is the fade/border of the circle. + // We want this to only be a problem when it actually looks like an overlap. + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(125, 0) } + } + }); + } + + [Test] + public void TestAlmostNotOverlapFarApart() + { + assertShouldNotOverlap(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(110, 0) } + } + }); + } + + [Test] + public void TestOverlapFarApartExpert() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(20, 0) } + } + }, DifficultyRating.Expert); + } + + [Test] + public void TestOverlapTooFarApart() + { + // Far apart enough to where the objects are not visible at the same time, and so overlapping is fine. + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 2000, Position = new Vector2(20, 0) } + } + }); + } + + [Test] + public void TestSliderTailOverlapFarApart() + { + assertShouldNotOverlap(new Beatmap + { + HitObjects = new List + { + getSliderMock(startTime: 0, endTime: 500, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object, + new HitCircle { StartTime = 1000, Position = new Vector2(120, 0) } + } + }); + } + + [Test] + public void TestSliderTailOverlapClose() + { + assertOk(new Beatmap + { + HitObjects = new List + { + getSliderMock(startTime: 0, endTime: 900, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object, + new HitCircle { StartTime = 1000, Position = new Vector2(120, 0) } + } + }); + } + + [Test] + public void TestSliderTailNoOverlapFarApart() + { + assertOk(new Beatmap + { + HitObjects = new List + { + getSliderMock(startTime: 0, endTime: 500, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object, + new HitCircle { StartTime = 1000, Position = new Vector2(300, 0) } + } + }); + } + + [Test] + public void TestSliderTailNoOverlapClose() + { + // If these were circles they would need to overlap, but overlapping with slider tails is not required. + assertOk(new Beatmap + { + HitObjects = new List + { + getSliderMock(startTime: 0, endTime: 900, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object, + new HitCircle { StartTime = 1000, Position = new Vector2(300, 0) } + } + }); + } + + private Mock getSliderMock(double startTime, double endTime, Vector2 startPosition, Vector2 endPosition) + { + var mockSlider = new Mock(); + mockSlider.SetupGet(s => s.StartTime).Returns(startTime); + mockSlider.SetupGet(s => s.Position).Returns(startPosition); + mockSlider.SetupGet(s => s.EndPosition).Returns(endPosition); + mockSlider.As().Setup(d => d.EndTime).Returns(endTime); + + return mockSlider; + } + + private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating = DifficultyRating.Easy) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating); + Assert.That(check.Run(context), Is.Empty); + } + + private void assertShouldProbablyOverlap(IBeatmap beatmap, int count = 1) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldProbablyOverlap)); + } + + private void assertShouldOverlap(IBeatmap beatmap, int count = 1) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldOverlap)); + } + + private void assertShouldNotOverlap(IBeatmap beatmap, int count = 1) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldNotOverlap)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index ebe642803b..1efd19f49d 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -3,6 +3,7 @@ + From 629c98e6a0d64e4a0dfb97ca1f876b5252e2d5dd Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:34:11 +0200 Subject: [PATCH 497/670] Add time distance equality tests --- .../Checks/CheckTimeDistanceEqualityTest.cs | 324 ++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTimeDistanceEqualityTest.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTimeDistanceEqualityTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTimeDistanceEqualityTest.cs new file mode 100644 index 0000000000..49a6fd12fa --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTimeDistanceEqualityTest.cs @@ -0,0 +1,324 @@ +// 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 Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Edit.Checks; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks +{ + [TestFixture] + public class CheckTimeDistanceEqualityTest + { + private CheckTimeDistanceEquality check; + + [SetUp] + public void Setup() + { + check = new CheckTimeDistanceEquality(); + } + + [Test] + public void TestCirclesEquidistant() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(100, 0) }, + new HitCircle { StartTime = 1500, Position = new Vector2(150, 0) } + } + }); + } + + [Test] + public void TestCirclesOneSlightlyOff() + { + assertWarning(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(80, 0) }, // Distance a quite low compared to previous. + new HitCircle { StartTime = 1500, Position = new Vector2(130, 0) } + } + }); + } + + [Test] + public void TestCirclesOneOff() + { + assertProblem(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Twice the regular spacing. + new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) } + } + }); + } + + [Test] + public void TestCirclesTwoOff() + { + assertProblem(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Twice the regular spacing. + new HitCircle { StartTime = 1500, Position = new Vector2(250, 0) } // Also twice the regular spacing. + } + }, count: 2); + } + + [Test] + public void TestCirclesStacked() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(50, 0) }, // Stacked, is fine. + new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) } + } + }); + } + + [Test] + public void TestCirclesStacking() + { + assertWarning(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(50, 0), StackHeight = 1 }, + new HitCircle { StartTime = 1500, Position = new Vector2(50, 0), StackHeight = 2 }, + new HitCircle { StartTime = 2000, Position = new Vector2(50, 0), StackHeight = 3 }, + new HitCircle { StartTime = 2500, Position = new Vector2(50, 0), StackHeight = 4 }, // Ends up far from (50; 0), causing irregular spacing. + new HitCircle { StartTime = 3000, Position = new Vector2(100, 0) } + } + }); + } + + [Test] + public void TestCirclesHalfStack() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(55, 0) }, // Basically stacked, so is fine. + new HitCircle { StartTime = 1500, Position = new Vector2(105, 0) } + } + }); + } + + [Test] + public void TestCirclesPartialOverlap() + { + assertProblem(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(65, 0) }, // Really low distance compared to previous. + new HitCircle { StartTime = 1500, Position = new Vector2(115, 0) } + } + }); + } + + [Test] + public void TestCirclesSlightlyDifferent() + { + assertOk(new Beatmap + { + HitObjects = new List + { + // Does not need to be perfect, as long as the distance is approximately correct it's sight-readable. + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(52, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(97, 0) }, + new HitCircle { StartTime = 1500, Position = new Vector2(165, 0) } + } + }); + } + + [Test] + public void TestCirclesSlowlyChanging() + { + const float multiplier = 1.2f; + + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(50 + 50 * multiplier, 0) }, + // This gap would be a warning if it weren't for the previous pushing the average spacing up. + new HitCircle { StartTime = 1500, Position = new Vector2(50 + 50 * multiplier + 50 * multiplier * multiplier, 0) } + } + }); + } + + [Test] + public void TestCirclesQuicklyChanging() + { + const float multiplier = 1.6f; + + var beatmap = new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(50 + 50 * multiplier, 0) }, // Warning + new HitCircle { StartTime = 1500, Position = new Vector2(50 + 50 * multiplier + 50 * multiplier * multiplier, 0) } // Problem + } + }; + + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.First().Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingWarning); + Assert.That(issues.Last().Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingProblem); + } + + [Test] + public void TestCirclesTooFarApart() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 4000, Position = new Vector2(200, 0) }, // 2 seconds apart from previous, so can start from wherever. + new HitCircle { StartTime = 4500, Position = new Vector2(250, 0) } + } + }); + } + + [Test] + public void TestCirclesOneOffExpert() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Jumps are allowed in higher difficulties. + new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) } + } + }, DifficultyRating.Expert); + } + + [Test] + public void TestSpinner() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new Spinner { StartTime = 500, EndTime = 1000 }, // Distance to and from the spinner should be ignored. If it isn't this should give a problem. + new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }, + new HitCircle { StartTime = 2000, Position = new Vector2(150, 0) } + } + }); + } + + [Test] + public void TestSliders() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + getSliderMock(startTime: 1000, endTime: 1500, startPosition: new Vector2(100, 0), endPosition: new Vector2(150, 0)).Object, + getSliderMock(startTime: 2000, endTime: 2500, startPosition: new Vector2(200, 0), endPosition: new Vector2(250, 0)).Object, + new HitCircle { StartTime = 2500, Position = new Vector2(300, 0) } + } + }); + } + + [Test] + public void TestSlidersOneOff() + { + assertProblem(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + getSliderMock(startTime: 1000, endTime: 1500, startPosition: new Vector2(100, 0), endPosition: new Vector2(150, 0)).Object, + getSliderMock(startTime: 2000, endTime: 2500, startPosition: new Vector2(250, 0), endPosition: new Vector2(300, 0)).Object, // Twice the spacing. + new HitCircle { StartTime = 2500, Position = new Vector2(300, 0) } + } + }); + } + + private Mock getSliderMock(double startTime, double endTime, Vector2 startPosition, Vector2 endPosition) + { + var mockSlider = new Mock(); + mockSlider.SetupGet(s => s.StartTime).Returns(startTime); + mockSlider.SetupGet(s => s.Position).Returns(startPosition); + mockSlider.SetupGet(s => s.EndPosition).Returns(endPosition); + mockSlider.As().Setup(d => d.EndTime).Returns(endTime); + + return mockSlider; + } + + private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating = DifficultyRating.Easy) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating); + Assert.That(check.Run(context), Is.Empty); + } + + private void assertWarning(IBeatmap beatmap, int count = 1) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingWarning)); + } + + private void assertProblem(IBeatmap beatmap, int count = 1) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingProblem)); + } + } +} From 36e459e97e7b516f2c95a481c1561488226289a5 Mon Sep 17 00:00:00 2001 From: aitani9 <55509723+aitani9@users.noreply.github.com> Date: Mon, 21 Jun 2021 13:42:15 -0700 Subject: [PATCH 498/670] Use margin instead of padding --- osu.Game/Overlays/OverlayStreamItem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs index 7f8559e7de..cd1391a3d8 100644 --- a/osu.Game/Overlays/OverlayStreamItem.cs +++ b/osu.Game/Overlays/OverlayStreamItem.cs @@ -39,9 +39,9 @@ namespace osu.Game.Overlays protected OverlayStreamItem(T value) : base(value) { - Height = 60; - Width = 100; - Padding = new MarginPadding(5); + Height = 50; + Width = 90; + Margin = new MarginPadding(5); } [BackgroundDependencyLoader] From ebe0d43790a7d793dd6db2cb2ae8e1ce57d6d234 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 02:51:00 +0300 Subject: [PATCH 499/670] Add ability to disallow falling back to parent skins --- osu.Game/Skinning/SkinProvidingContainer.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 315571e79b..4435d924c2 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -38,6 +38,11 @@ namespace osu.Game.Skinning [CanBeNull] private ISkinSource fallbackSource; + /// + /// Whether falling back to parent s is allowed in this container. + /// + protected virtual bool AllowFallingBackToParent => true; + protected virtual bool AllowDrawableLookup(ISkinComponent component) => true; protected virtual bool AllowTextureLookup(string componentName) => true; @@ -180,9 +185,12 @@ namespace osu.Game.Skinning { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - fallbackSource = dependencies.Get(); - if (fallbackSource != null) - fallbackSource.SourceChanged += OnSourceChanged; + if (AllowFallingBackToParent) + { + fallbackSource = dependencies.Get(); + if (fallbackSource != null) + fallbackSource.SourceChanged += OnSourceChanged; + } dependencies.CacheAs(this); From d53a43cf3c65dbc1ede229e1dffa56f32f6a87da Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 02:52:37 +0300 Subject: [PATCH 500/670] Isolate `RulesetSkinProvidingContainer` from falling back to parent skin sources For simplicity of lookup order, and which sources are used for the lookup. --- .../Skinning/RulesetSkinProvidingContainer.cs | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index bbaeee98d8..54d366b2f4 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -7,18 +7,26 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.UI; namespace osu.Game.Skinning { /// - /// A type of that provides access to the beatmap skin and user skin, - /// with each legacy skin source transformed with the ruleset's legacy skin transformer. + /// A type of specialized for and other gameplay-related components. + /// Providing access to the skin sources and the beatmap skin each surrounded with the ruleset legacy skin transformer. + /// While also limiting lookups from falling back to any parent s out of this container. /// public class RulesetSkinProvidingContainer : SkinProvidingContainer { protected readonly Ruleset Ruleset; protected readonly IBeatmap Beatmap; + /// + /// This container already re-exposes all skin sources in a ruleset-usable form. + /// Therefore disallow falling back to any parent any further. + /// + protected override bool AllowFallingBackToParent => false; + protected override Container Content { get; } public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin) @@ -42,12 +50,7 @@ namespace osu.Game.Skinning private void load() { UpdateSkins(); - } - - protected override void OnSourceChanged() - { - UpdateSkins(); - base.OnSourceChanged(); + skinManager.SourceChanged += UpdateSkins; } protected virtual void UpdateSkins() @@ -83,5 +86,13 @@ namespace osu.Game.Skinning return legacySkin; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skinManager != null) + skinManager.SourceChanged -= UpdateSkins; + } } } From 97dbc7f20ecc19ee0d00516a51618212b28bc404 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 02:54:34 +0300 Subject: [PATCH 501/670] Add back `SkinManager.DefaultSkin` to the ruleset skin lookup sources --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 54d366b2f4..8113597dee 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -73,6 +73,8 @@ namespace osu.Game.Skinning SkinSources.Add(skinManager.CurrentSkin.Value); break; } + + SkinSources.Add(skinManager.DefaultSkin); } protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin) From 9e5bb146d3aaa2936c07f2299a58ce36d56d82a4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 03:06:39 +0300 Subject: [PATCH 502/670] Add xmldoc to `SkinManager` The `` part comes from `BeatmapManager`, which I believe works correctly here as well, as this does handle the "storage and retrieval" of skins. --- osu.Game/Skinning/SkinManager.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 7acc52809f..1f10177a9e 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -30,6 +30,13 @@ using osu.Game.IO.Archives; namespace osu.Game.Skinning { + /// + /// Handles the storage and retrieval of s. + /// + /// + /// This is also exposed and cached as on a game-wide level for general components across the game. + /// Lookups from gameplay components are instead covered by , and are never hit here. + /// [ExcludeFromDynamicCompile] public class SkinManager : ArchiveModelManager, ISkinSource, IStorageResourceProvider { From 627c857da8d0293625e9600c2e5176c333c0f0ee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 03:44:32 +0300 Subject: [PATCH 503/670] Propagate `SourceChanged` events from `SkinManager` down in the ruleset skin container --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 8113597dee..4b3c3881c2 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -50,7 +50,13 @@ namespace osu.Game.Skinning private void load() { UpdateSkins(); - skinManager.SourceChanged += UpdateSkins; + skinManager.SourceChanged += OnSourceChanged; + } + + protected override void OnSourceChanged() + { + UpdateSkins(); + base.OnSourceChanged(); } protected virtual void UpdateSkins() From caa90bccc6c92ecc399900ad4774f308002c1260 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 03:45:43 +0300 Subject: [PATCH 504/670] Fix default skin potentially added twice in `RulesetSkinProvidingContainer` --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 4b3c3881c2..88cf70fa18 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -80,7 +80,8 @@ namespace osu.Game.Skinning break; } - SkinSources.Add(skinManager.DefaultSkin); + if (skinManager.CurrentSkin.Value != skinManager.DefaultSkin) + SkinSources.Add(skinManager.DefaultSkin); } protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin) From ec040ff3fc80e3f0374245564f16279caf547b3f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 05:05:41 +0300 Subject: [PATCH 505/670] Fix leak due to not properly unbinding `SourceChanged` event on disposal --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 88cf70fa18..b07d3f5199 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -101,7 +101,7 @@ namespace osu.Game.Skinning base.Dispose(isDisposing); if (skinManager != null) - skinManager.SourceChanged -= UpdateSkins; + skinManager.SourceChanged -= OnSourceChanged; } } } From 00b4cf1829cd00af1a07c567a25fa9dbda7e0e5a Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 21 Jun 2021 20:20:43 -0700 Subject: [PATCH 506/670] Handle sub screen `OnExiting` logic on main screen --- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index e418d36d40..ceee002c6e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -240,13 +240,15 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnExiting(IScreen next) { + if (screenStack.CurrentScreen?.OnExiting(next) == true) + return true; + RoomManager.PartRoom(); waves.Hide(); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); - screenStack.CurrentScreen?.OnExiting(next); base.OnExiting(next); return false; } From 9bcd1e69224d4a7943ba8203765c59b50c5b6ed3 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 21 Jun 2021 20:22:18 -0700 Subject: [PATCH 507/670] Move confirm dialog logic to `OnExiting` --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index f9b3549f3c..d7025c2550 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -305,6 +305,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return true; } + return base.OnBackButton(); + } + + public override bool OnExiting(IScreen next) + { + if (client.Room == null) + { + // room has not been created yet; exit immediately. + return base.OnExiting(next); + } + if (!exitConfirmed && dialogOverlay != null) { dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => @@ -316,7 +327,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return true; } - return base.OnBackButton(); + return base.OnExiting(next); } private ModSettingChangeTracker modSettingChangeTracker; From db860980622bfbc29db9c66f70373dd4e24132da Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 21 Jun 2021 20:23:11 -0700 Subject: [PATCH 508/670] Fix dialog not closing after spamming OS window close --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index d7025c2550..4b8c4422ec 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -318,11 +318,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!exitConfirmed && dialogOverlay != null) { - dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => + if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog) + confirmDialog.PerformOkAction(); + else { - exitConfirmed = true; - this.Exit(); - })); + dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => + { + exitConfirmed = true; + this.Exit(); + })); + } return true; } From 2cdbada87e2bfe8e8bafca51c37242b379243b54 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 21 Jun 2021 20:24:22 -0700 Subject: [PATCH 509/670] Fix screen breadcrumb control updating on click --- .../Graphics/UserInterface/ScreenBreadcrumbControl.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs index e85525b2f8..d7bd7d7e01 100644 --- a/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs @@ -3,6 +3,7 @@ using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; namespace osu.Game.Graphics.UserInterface @@ -19,8 +20,13 @@ namespace osu.Game.Graphics.UserInterface if (stack.CurrentScreen != null) onPushed(null, stack.CurrentScreen); + } - Current.ValueChanged += current => current.NewValue.MakeCurrent(); + protected override void SelectTab(TabItem tab) + { + // override base method to prevent current item from being changed on click. + // depend on screen push/exit to change current item instead. + tab.Value.MakeCurrent(); } private void onPushed(IScreen lastScreen, IScreen newScreen) From f89c154e1810b54b14f48522c1c5c2a6b5bcb546 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Tue, 22 Jun 2021 12:24:25 +0700 Subject: [PATCH 510/670] change `GetFontSizeByLevel` to return actual font size --- .../Containers/Markdown/OsuMarkdownHeading.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs index 40eb4cad15..a07216c32e 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs @@ -28,27 +28,25 @@ namespace osu.Game.Graphics.Containers.Markdown // Reference for this font size // https://github.com/ppy/osu-web/blob/376cac43a051b9c85ce95e2c446099be187b3e45/resources/assets/less/bem/osu-md.less#L9 // https://github.com/ppy/osu-web/blob/376cac43a051b9c85ce95e2c446099be187b3e45/resources/assets/less/variables.less#L161 - const float base_font_size = 14; - switch (level) { case 1: - return 30 / base_font_size; + return 30; case 2: - return 26 / base_font_size; + return 26; case 3: - return 20 / base_font_size; + return 20; case 4: - return 18 / base_font_size; + return 18; case 5: - return 16 / base_font_size; + return 16; default: - return 1; + return 14; } } From 5c3129f1a2e83a58e2c19a6018fc02df9544530c Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Tue, 22 Jun 2021 12:24:51 +0700 Subject: [PATCH 511/670] add font size in `HeadingTextFlowContainer` --- .../Graphics/Containers/Markdown/OsuMarkdownHeading.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs index a07216c32e..a3a86df678 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownHeading.cs @@ -20,7 +20,8 @@ namespace osu.Game.Graphics.Containers.Markdown public override MarkdownTextFlowContainer CreateTextFlow() => new HeadingTextFlowContainer { - Weight = GetFontWeightByLevel(level), + FontSize = GetFontSizeByLevel(level), + FontWeight = GetFontWeightByLevel(level), }; protected override float GetFontSizeByLevel(int level) @@ -65,9 +66,11 @@ namespace osu.Game.Graphics.Containers.Markdown private class HeadingTextFlowContainer : OsuMarkdownTextFlowContainer { - public FontWeight Weight { get; set; } + public float FontSize; + public FontWeight FontWeight; - protected override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(weight: Weight)); + protected override SpriteText CreateSpriteText() + => base.CreateSpriteText().With(t => t.Font = t.Font.With(size: FontSize, weight: FontWeight)); } } } From 0d17fb425973e8935220d06a2f40ff3223b23b86 Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Tue, 22 Jun 2021 13:53:21 +0800 Subject: [PATCH 512/670] fixed code --- .../Online/TestSceneBeatmapListingOverlay.cs | 194 +++++++++--------- .../BeatmapListingFilterControl.cs | 41 +++- osu.Game/Overlays/BeatmapListingOverlay.cs | 70 +++---- 3 files changed, 170 insertions(+), 135 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 2146ea333a..cd382c2bb2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -24,6 +24,8 @@ namespace osu.Game.Tests.Visual.Online private BeatmapListingOverlay overlay; + private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType().Single(); + [BackgroundDependencyLoader] private void load() { @@ -70,113 +72,123 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestSupporterOnlyFiltersPlaceholderNoBeatmaps() + public void TestNonSupportUseSupporterOnlyFiltersPlaceholderNoBeatmaps() { AddStep("fetch for 0 beatmaps", () => fetchFor()); AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); // test non-supporter on Rank Achieved filter - toggleRandomRankFilter(); - expectedPlaceholderShown(true, false); + toggleRankFilter(Scoring.ScoreRank.XH); + supporterRequiredPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - expectedPlaceholderShown(false, true); + AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + notFoundPlaceholderShown(); // test non-supporter on Played filter - toggleRandomSupporterOnlyPlayedFilter(); - expectedPlaceholderShown(true, false); + toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + supporterRequiredPlaceholderShown(); - AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - expectedPlaceholderShown(false, true); + AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + notFoundPlaceholderShown(); // test non-supporter on both Rank Achieved and Played filter - toggleRandomRankFilter(); - toggleRandomSupporterOnlyPlayedFilter(); - expectedPlaceholderShown(true, false); + toggleRankFilter(Scoring.ScoreRank.XH); + toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + supporterRequiredPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - expectedPlaceholderShown(false, true); - - AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); - - // test supporter on Rank Achieved filter - toggleRandomRankFilter(); - expectedPlaceholderShown(false, true); - - AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - expectedPlaceholderShown(false, true); - - // test supporter on Played filter - toggleRandomSupporterOnlyPlayedFilter(); - expectedPlaceholderShown(false, true); - - AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - expectedPlaceholderShown(false, true); - - // test supporter on both Rank Achieved and Played filter - toggleRandomRankFilter(); - toggleRandomSupporterOnlyPlayedFilter(); - expectedPlaceholderShown(false, true); - - AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - expectedPlaceholderShown(false, true); + AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + notFoundPlaceholderShown(); } [Test] - public void TestSupporterOnlyFiltersPlaceholderOneBeatmap() + public void TestSupportUseSupporterOnlyFiltersPlaceholderNoBeatmaps() + { + AddStep("fetch for 0 beatmaps", () => fetchFor()); + AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); + + // test supporter on Rank Achieved filter + toggleRankFilter(Scoring.ScoreRank.XH); + notFoundPlaceholderShown(); + + AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + notFoundPlaceholderShown(); + + // test supporter on Played filter + toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + notFoundPlaceholderShown(); + + AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + notFoundPlaceholderShown(); + + // test supporter on both Rank Achieved and Played filter + toggleRankFilter(Scoring.ScoreRank.XH); + toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + notFoundPlaceholderShown(); + + AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + notFoundPlaceholderShown(); + } + + [Test] + public void TestNonSupporterUseSupporterOnlyFiltersPlaceholderOneBeatmap() { AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); // test non-supporter on Rank Achieved filter - toggleRandomRankFilter(); - expectedPlaceholderShown(true, false); + toggleRankFilter(Scoring.ScoreRank.XH); + supporterRequiredPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - expectedPlaceholderShown(false, false); + AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + noPlaceholderShown(); // test non-supporter on Played filter - toggleRandomSupporterOnlyPlayedFilter(); - expectedPlaceholderShown(true, false); + toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + supporterRequiredPlaceholderShown(); - AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - expectedPlaceholderShown(false, false); + AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + noPlaceholderShown(); // test non-supporter on both Rank Achieved and Played filter - toggleRandomRankFilter(); - toggleRandomSupporterOnlyPlayedFilter(); - expectedPlaceholderShown(true, false); + toggleRankFilter(Scoring.ScoreRank.XH); + toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + supporterRequiredPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - expectedPlaceholderShown(false, false); + AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + noPlaceholderShown(); + } + [Test] + public void TestSupporterUseSupporterOnlyFiltersPlaceholderOneBeatmap() + { + AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); // test supporter on Rank Achieved filter - toggleRandomRankFilter(); - expectedPlaceholderShown(false, false); + toggleRankFilter(Scoring.ScoreRank.XH); + noPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - expectedPlaceholderShown(false, false); + AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + noPlaceholderShown(); // test supporter on Played filter - toggleRandomSupporterOnlyPlayedFilter(); - expectedPlaceholderShown(false, false); + toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + noPlaceholderShown(); - AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - expectedPlaceholderShown(false, false); + AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + noPlaceholderShown(); // test supporter on both Rank Achieved and Played filter - toggleRandomRankFilter(); - toggleRandomSupporterOnlyPlayedFilter(); - expectedPlaceholderShown(false, false); + toggleRankFilter(Scoring.ScoreRank.XH); + toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + noPlaceholderShown(); - AddStep("Set Played filter to Any", () => overlay.ChildrenOfType().Single().Played.Value = SearchPlayed.Any); - AddStep("Clear Rank Achieved filter", () => overlay.ChildrenOfType().Single().Ranks.Clear()); - expectedPlaceholderShown(false, false); + AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + noPlaceholderShown(); } private void fetchFor(params BeatmapSetInfo[] beatmaps) @@ -185,44 +197,36 @@ namespace osu.Game.Tests.Visual.Online setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b))); // trigger arbitrary change for fetching. - overlay.ChildrenOfType().Single().Query.TriggerChange(); + searchControl.Query.TriggerChange(); } - private void toggleRandomRankFilter() + private void toggleRankFilter(Scoring.ScoreRank rank) { - short r = TestContext.CurrentContext.Random.NextShort(); - AddStep("toggle Random Rank Achieved filter", () => + AddStep("toggle Rank Achieved filter", () => { - overlay.ChildrenOfType().Single().Ranks.Clear(); - overlay.ChildrenOfType().Single().Ranks.Add((Scoring.ScoreRank)(r % 8)); + searchControl.Ranks.Clear(); + searchControl.Ranks.Add(rank); }); } - private void toggleRandomSupporterOnlyPlayedFilter() + private void toggleSupporterOnlyPlayedFilter(SearchPlayed played) { - short r = TestContext.CurrentContext.Random.NextShort(); - AddStep("toggle Random Played filter", () => overlay.ChildrenOfType().Single().Played.Value = (SearchPlayed)(r % 2 + 1)); + AddStep("toggle Played filter", () => searchControl.Played.Value = played); } - private void expectedPlaceholderShown(bool supporterRequiredShown, bool notFoundShown) + private void supporterRequiredPlaceholderShown() { - if (supporterRequiredShown) - { - AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - } - else - { - AddUntilStep("supporter-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - } + AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + } - if (notFoundShown) - { - AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - } - else - { - AddUntilStep("not-found-placeholder hidden", () => !overlay.ChildrenOfType().Any()); - } + private void notFoundPlaceholderShown() + { + AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + } + + private void noPlaceholderShown() + { + AddUntilStep("no placeholder shown", () => !overlay.ChildrenOfType().Any() && !overlay.ChildrenOfType().Any()); } private class TestAPIBeatmapSet : APIBeatmapSet diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 6e83dc0bf4..f49d913bb2 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -10,11 +10,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; +using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -26,7 +28,7 @@ namespace osu.Game.Overlays.BeatmapListing /// Fired when a search finishes. Contains only new items in the case of pagination. /// Fired with BeatmapListingSearchControl when non-supporter user used supporter-only filters. /// - public Action, BeatmapListingSearchControl> SearchFinished; + public Action SearchFinished; /// /// Fired when search criteria change. @@ -216,11 +218,19 @@ namespace osu.Game.Overlays.BeatmapListing // check if an non-supporter user used supporter-only filters if (!api.LocalUser.Value.IsSupporter && (searchControl.Ranks.Any() || searchControl.Played.Value != SearchPlayed.Any)) { - SearchFinished?.Invoke(sets, searchControl); + List filters = new List(); + + if (searchControl.Played.Value != SearchPlayed.Any) + filters.Add(BeatmapsStrings.ListingSearchFiltersPlayed); + + if (searchControl.Ranks.Any()) + filters.Add(BeatmapsStrings.ListingSearchFiltersRank); + + SearchFinished?.Invoke(SearchResult.SupporterOnlyFilter(filters)); } else { - SearchFinished?.Invoke(sets, null); + SearchFinished?.Invoke(SearchResult.ResultsReturned(sets)); } }; @@ -246,5 +256,30 @@ namespace osu.Game.Overlays.BeatmapListing base.Dispose(isDisposing); } + + public enum SearchResultType + { + ResultsReturned, + SupporterOnlyFilter + } + + public struct SearchResult + { + public SearchResultType Type { get; private set; } + public List Results { get; private set; } + public List Filters { get; private set; } + + public static SearchResult ResultsReturned(List results) => new SearchResult + { + Type = SearchResultType.ResultsReturned, + Results = results + }; + + public static SearchResult SupporterOnlyFilter(List filters) => new SearchResult + { + Type = SearchResultType.SupporterOnlyFilter, + Filters = filters + }; + } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 578e70e630..63800e6585 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Localisation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -119,28 +120,28 @@ namespace osu.Game.Overlays private Task panelLoadDelegate; - private void onSearchFinished(List beatmaps, BeatmapListingSearchControl searchControl) + private void onSearchFinished(BeatmapListingFilterControl.SearchResult searchResult) { - var newPanels = beatmaps.Select(b => new GridBeatmapPanel(b) + // non-supporter user used supporter-only filters + if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilter) + { + supporterRequiredContent.UpdateText(searchResult.Filters); + addContentToPlaceholder(supporterRequiredContent); + return; + } + + var newPanels = searchResult.Results.Select(b => new GridBeatmapPanel(b) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }); - // non-supporter user used supporter-only filters - if (searchControl != null) - { - supporterRequiredContent.UpdateText(searchControl.Played.Value != SearchPlayed.Any, searchControl.Ranks.Any()); - LoadComponentAsync(supporterRequiredContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); - return; - } - if (filterControl.CurrentPage == 0) { //No matches case if (!newPanels.Any()) { - LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + addContentToPlaceholder(notFoundContent); return; } @@ -182,16 +183,11 @@ namespace osu.Game.Overlays { var transform = lastContent.FadeOut(100, Easing.OutQuint); - if (lastContent == notFoundContent) + if (lastContent == notFoundContent || lastContent == supporterRequiredContent) { - // not found display may be used multiple times, so don't expire/dispose it. + // the placeholder may be used multiple times, so don't expire/dispose it. transform.Schedule(() => panelTarget.Remove(lastContent)); } - else if (lastContent == supporterRequiredContent) - { - // supporter required display may be used multiple times, so don't expire/dispose it. - transform.Schedule(() => panelTarget.Remove(supporterRequiredContent)); - } else { // Consider the case when the new content is smaller than the last content. @@ -260,7 +256,7 @@ namespace osu.Game.Overlays // using string literals as there's no proper processing for LocalizeStrings yet public class SupporterRequiredDrawable : CompositeDrawable { - private OsuSpriteText supporterRequiredText; + private OsuSpriteText filtersText; public SupporterRequiredDrawable() { @@ -289,30 +285,20 @@ namespace osu.Game.Overlays FillMode = FillMode.Fit, Texture = textures.Get(@"Online/supporter-required"), }, - supporterRequiredText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 16), - Colour = Colour4.White, - Margin = new MarginPadding { Bottom = 10 }, - }, - createSupporterTagLink(), + createSupporterText(), } }); } - public void UpdateText(bool playedFilter, bool rankFilter) + public void UpdateText(List filters) { - List filters = new List(); - if (playedFilter) filters.Add(BeatmapsStrings.ListingSearchFiltersPlayed.ToString()); - if (rankFilter) filters.Add(BeatmapsStrings.ListingSearchFiltersRank.ToString()); - supporterRequiredText.Text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(); + // use string literals for now + filtersText.Text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(); } - private Drawable createSupporterTagLink() + private Drawable createSupporterText() { - LinkFlowContainer supporterTagLink = new LinkFlowContainer + LinkFlowContainer supporterRequiredText = new LinkFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -320,8 +306,18 @@ namespace osu.Game.Overlays Margin = new MarginPadding { Bottom = 10 }, }; - supporterTagLink.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), "https://osu.ppy.sh/store/products/supporter-tag"); - return supporterTagLink; + filtersText = (OsuSpriteText)supporterRequiredText.AddText( + "_", + t => + { + t.Font = OsuFont.GetFont(size: 16); + t.Colour = Colour4.White; + } + ).First(); + + supporterRequiredText.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), @"/store/products/supporter-tag"); + + return supporterRequiredText; } } From e9339d6100b43c72308862582215c00ff3a2950d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 15:16:19 +0900 Subject: [PATCH 513/670] Move some inline comments on `const`s to xmldoc instead --- .../Edit/Checks/CheckLowDiffOverlaps.cs | 4 ++- .../Edit/Checks/CheckTimeDistanceEquality.cs | 34 +++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs index 488bdfd972..1dd859b5b8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs @@ -17,7 +17,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks private const double should_probably_overlap_threshold = 175; // 170 BPM 1/2 private const double should_not_overlap_threshold = 250; // 120 BPM 1/2 = 240 BPM 1/1 - // Objects need to overlap this much before being treated as an overlap, else it may just be the borders slightly touching. + /// + /// Objects need to overlap this much before being treated as an overlap, else it may just be the borders slightly touching. + /// private const double overlap_leniency = 5; public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Missing or unexpected overlaps"); diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs index db48878dd3..6420d9558e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs @@ -14,14 +14,36 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks { public class CheckTimeDistanceEquality : ICheck { - private const double pattern_lifetime = 600; // Two objects this many ms apart or more are skipped. (200 BPM 2/1) - private const double stack_leniency = 12; // Two objects this distance apart or less are skipped. + /// + /// Two objects this many ms apart or more are skipped. (200 BPM 2/1) + /// + private const double pattern_lifetime = 600; - private const double observation_lifetime = 4000; // How long an observation is relevant for comparison. (120 BPM 8/1) - private const double similar_time_leniency = 16; // How different two delta times can be to still be compared. (240 BPM 1/16) + /// + /// Two objects this distance apart or less are skipped. + /// + private const double stack_leniency = 12; + + /// + /// How long an observation is relevant for comparison. (120 BPM 8/1) + /// + private const double observation_lifetime = 4000; + + /// + /// How different two delta times can be to still be compared. (240 BPM 1/16) + /// + private const double similar_time_leniency = 16; + + /// + /// How many pixels are subtracted from the difference between current and expected distance. + /// + private const double distance_leniency_absolute_warning = 10; + + /// + /// How much of the current distance that the difference can make out. + /// + private const double distance_leniency_percent_warning = 0.15; - private const double distance_leniency_absolute_warning = 10; // How many pixels are subtracted from the difference between current and expected distance. - private const double distance_leniency_percent_warning = 0.15; // How much of the current distance that the difference can make out. private const double distance_leniency_absolute_problem = 20; private const double distance_leniency_percent_problem = 0.3; From b54e82eb993d2c41d0f0b98b094c8188414cdf05 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 22 Jun 2021 12:34:34 +0900 Subject: [PATCH 514/670] Remove unused argument from `CatchPlayfield` --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 3 +-- osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 0e1ef90737..644facdabc 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI // only check the X position; handle all vertical space. base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y)); - public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation) + public CatchPlayfield(BeatmapDifficulty difficulty) { var droppedObjectContainer = new Container { diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 9389fa803b..8b6a074426 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.UI protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield); - protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation); + protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer(); From 3745101f326b2d677f2c5ff124f01a43269c518e Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 10 Jun 2021 15:31:20 +0800 Subject: [PATCH 515/670] Extract seed setting control to IHasSeed --- osu.Game/Rulesets/Mods/IHasSeed.cs | 94 +++++++++++++++++++++++++++++ osu.Game/Rulesets/Mods/ModRandom.cs | 82 +------------------------ 2 files changed, 96 insertions(+), 80 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/IHasSeed.cs diff --git a/osu.Game/Rulesets/Mods/IHasSeed.cs b/osu.Game/Rulesets/Mods/IHasSeed.cs new file mode 100644 index 0000000000..b6852d960b --- /dev/null +++ b/osu.Game/Rulesets/Mods/IHasSeed.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Rulesets.Mods +{ + public interface IHasSeed + { + public Bindable Seed { get; } + } + + public class SeedSettingsControl : SettingsItem + { + protected override Drawable CreateControl() => new SeedControl + { + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 5 } + }; + + private sealed class SeedControl : CompositeDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current; + set + { + current.Current = value; + seedNumberBox.Text = value.Value.ToString(); + } + } + + private readonly OsuNumberBox seedNumberBox; + + public SeedControl() + { + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 2), + new Dimension(GridSizeMode.Relative, 0.25f) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + seedNumberBox = new OsuNumberBox + { + RelativeSizeAxes = Axes.X, + CommitOnFocusLost = true + } + } + } + } + }; + + seedNumberBox.Current.BindValueChanged(e => + { + int? value = null; + + if (int.TryParse(e.NewValue, out var intVal)) + value = intVal; + + current.Value = value; + }); + } + + protected override void Update() + { + if (current.Value == null) + seedNumberBox.Text = current.Current.Value.ToString(); + } + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index e0c3008ae8..c6040a48aa 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -8,11 +8,10 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods { - public abstract class ModRandom : Mod + public abstract class ModRandom : Mod, IHasSeed { public override string Name => "Random"; public override string Acronym => "RD"; @@ -20,88 +19,11 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.Dice; public override double ScoreMultiplier => 1; - [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(ModRandomSettingsControl))] + [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))] public Bindable Seed { get; } = new Bindable { Default = null, Value = null }; - - private class ModRandomSettingsControl : SettingsItem - { - protected override Drawable CreateControl() => new SeedControl - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 5 } - }; - - private sealed class SeedControl : CompositeDrawable, IHasCurrentValue - { - private readonly BindableWithCurrent current = new BindableWithCurrent(); - - public Bindable Current - { - get => current; - set - { - current.Current = value; - seedNumberBox.Text = value.Value.ToString(); - } - } - - private readonly SettingsNumberBox.NumberBox seedNumberBox; - - public SeedControl() - { - AutoSizeAxes = Axes.Y; - - InternalChildren = new[] - { - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 2), - new Dimension(GridSizeMode.Relative, 0.25f) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - seedNumberBox = new SettingsNumberBox.NumberBox - { - RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true - } - } - } - } - }; - - seedNumberBox.Current.BindValueChanged(e => - { - int? value = null; - - if (int.TryParse(e.NewValue, out var intVal)) - value = intVal; - - current.Value = value; - }); - } - - protected override void Update() - { - if (current.Value == null) - seedNumberBox.Text = current.Current.Value.ToString(); - } - } - } } } From fc224c53f4ee203ae3ad6b9f7d556c24b8e892f2 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 22 Jun 2021 14:49:37 +0800 Subject: [PATCH 516/670] Remove extra usings --- osu.Game/Rulesets/Mods/ModRandom.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index c6040a48aa..61297c162d 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -2,10 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; using osu.Game.Graphics; From 0ad189e357f93d023dbaec929713ad40e867df36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 16:19:55 +0900 Subject: [PATCH 517/670] Expose skin sources via `ISkinSource` and revert to consuming based on hierarchy --- .../TestSceneCursorTrail.cs | 4 +++ .../TestSceneSkinFallbacks.cs | 3 ++ .../Gameplay/TestSceneSkinnableDrawable.cs | 3 ++ .../Gameplay/TestSceneSkinnableSound.cs | 2 ++ osu.Game/Skinning/ISkinSource.cs | 6 ++++ .../Skinning/RulesetSkinProvidingContainer.cs | 35 ++++++++----------- osu.Game/Skinning/SkinManager.cs | 26 ++++++++++---- osu.Game/Skinning/SkinProvidingContainer.cs | 15 ++++++++ .../Beatmaps/LegacyBeatmapSkinColourTest.cs | 1 + 9 files changed, 67 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index 46274e779b..fe962d3cb8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -114,6 +116,8 @@ namespace osu.Game.Rulesets.Osu.Tests public ISkin FindProvider(Func lookupFunction) => null; + public IEnumerable AllSources => Enumerable.Empty(); + public event Action SourceChanged { add { } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 2b45818aa9..5ef27ab5ea 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -146,6 +147,8 @@ namespace osu.Game.Rulesets.Osu.Tests public ISkin FindProvider(Func lookupFunction) => null; + public IEnumerable AllSources => Enumerable.Empty(); + public event Action SourceChanged; private bool enabled = true; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index 77966e925a..3317d8f80a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using NUnit.Framework; @@ -330,6 +331,8 @@ namespace osu.Game.Tests.Visual.Gameplay public ISkin FindProvider(Func lookupFunction) => throw new NotImplementedException(); + public IEnumerable AllSources => Enumerable.Empty(); + public event Action SourceChanged { add { } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 55ee01e0d5..59edb527eb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -147,6 +148,7 @@ namespace osu.Game.Tests.Visual.Gameplay public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup); public ISkin FindProvider(Func lookupFunction) => source?.FindProvider(lookupFunction); + public IEnumerable AllSources => source.AllSources; public void TriggerSourceChanged() { diff --git a/osu.Game/Skinning/ISkinSource.cs b/osu.Game/Skinning/ISkinSource.cs index c7ebe91d64..ba3e2bf6ad 100644 --- a/osu.Game/Skinning/ISkinSource.cs +++ b/osu.Game/Skinning/ISkinSource.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using JetBrains.Annotations; namespace osu.Game.Skinning @@ -20,5 +21,10 @@ namespace osu.Game.Skinning /// The skin to be used for subsequent lookups, or null if none is available. [CanBeNull] ISkin FindProvider(Func lookupFunction); + + /// + /// Retrieve all sources available for lookup, with highest priority source first. + /// + IEnumerable AllSources { get; } } } diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index b07d3f5199..21a858977b 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -26,7 +26,6 @@ namespace osu.Game.Skinning /// Therefore disallow falling back to any parent any further. /// protected override bool AllowFallingBackToParent => false; - protected override Container Content { get; } public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin) @@ -44,13 +43,13 @@ namespace osu.Game.Skinning } [Resolved] - private SkinManager skinManager { get; set; } + private ISkinSource skinSource { get; set; } [BackgroundDependencyLoader] private void load() { UpdateSkins(); - skinManager.SourceChanged += OnSourceChanged; + skinSource.SourceChanged += OnSourceChanged; } protected override void OnSourceChanged() @@ -63,25 +62,19 @@ namespace osu.Game.Skinning { SkinSources.Clear(); - // TODO: we also want to insert a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements. - - switch (skinManager.CurrentSkin.Value) + foreach (var skin in skinSource.AllSources) { - case LegacySkin currentLegacySkin: - SkinSources.Add(GetLegacyRulesetTransformedSkin(currentLegacySkin)); + switch (skin) + { + case LegacySkin legacySkin: + SkinSources.Add(GetLegacyRulesetTransformedSkin(legacySkin)); + break; - if (currentLegacySkin != skinManager.DefaultLegacySkin) - SkinSources.Add(GetLegacyRulesetTransformedSkin(skinManager.DefaultLegacySkin)); - - break; - - default: - SkinSources.Add(skinManager.CurrentSkin.Value); - break; + default: + SkinSources.Add(skin); + break; + } } - - if (skinManager.CurrentSkin.Value != skinManager.DefaultSkin) - SkinSources.Add(skinManager.DefaultSkin); } protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin) @@ -100,8 +93,8 @@ namespace osu.Game.Skinning { base.Dispose(isDisposing); - if (skinManager != null) - skinManager.SourceChanged -= OnSourceChanged; + if (skinSource != null) + skinSource.SourceChanged -= OnSourceChanged; } } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 1f10177a9e..6bd4888eb5 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -248,17 +248,29 @@ namespace osu.Game.Skinning return null; } + public IEnumerable AllSources + { + get + { + yield return CurrentSkin.Value; + + if (CurrentSkin.Value is LegacySkin) + yield return DefaultLegacySkin; + + yield return DefaultSkin; + } + } + private T lookupWithFallback(Func lookupFunction) where T : class { - if (lookupFunction(CurrentSkin.Value) is T skinSourced) - return skinSourced; + foreach (var source in AllSources) + { + if (lookupFunction(source) is T skinSourced) + return skinSourced; + } - if (CurrentSkin.Value is LegacySkin && lookupFunction(DefaultLegacySkin) is T legacySourced) - return legacySourced; - - // Finally fall back to the (non-legacy) default. - return lookupFunction(DefaultSkin); + return null; } #region IResourceStorageProvider diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 4435d924c2..c83c299723 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -131,6 +131,21 @@ namespace osu.Game.Skinning return fallbackSource?.FindProvider(lookupFunction); } + public IEnumerable AllSources + { + get + { + foreach (var skin in SkinSources) + yield return skin; + + if (fallbackSource != null) + { + foreach (var skin in fallbackSource.AllSources) + yield return skin; + } + } + } + public Drawable GetDrawableComponent(ISkinComponent component) { foreach (var skin in SkinSources) diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index 347b611579..bb4768982a 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -146,6 +146,7 @@ namespace osu.Game.Tests.Beatmaps } public ISkin FindProvider(Func lookupFunction) => null; + public IEnumerable AllSources => Enumerable.Empty(); } } } From 14bdcef26b7da34ae0a1e1376b9c8e859ce42224 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 16:20:09 +0900 Subject: [PATCH 518/670] Add missing newline --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index 21a858977b..f1cc3df3de 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -26,6 +26,7 @@ namespace osu.Game.Skinning /// Therefore disallow falling back to any parent any further. /// protected override bool AllowFallingBackToParent => false; + protected override Container Content { get; } public RulesetSkinProvidingContainer(Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin) From ffac32a848b7755aa5e2f17f912b75946ab08014 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 16:40:48 +0900 Subject: [PATCH 519/670] Reword xmldoc --- osu.Game/Skinning/ISkinnableDrawable.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/ISkinnableDrawable.cs b/osu.Game/Skinning/ISkinnableDrawable.cs index 9625a9eb6d..60b40982e5 100644 --- a/osu.Game/Skinning/ISkinnableDrawable.cs +++ b/osu.Game/Skinning/ISkinnableDrawable.cs @@ -16,8 +16,9 @@ namespace osu.Game.Skinning bool IsEditable => true; /// - /// if this 's is automatically determined by proximity, - /// if the user has chosen a fixed anchor point. + /// In the context of the skin layout editor, whether this has a permanent anchor defined. + /// If , this 's is automatically determined by proximity, + /// If , a fixed anchor point has been defined. /// bool UsesFixedAnchor { get; set; } } From 4b3165084d72b4fefb2fe81ec9fb401da2c5fdc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 16:40:56 +0900 Subject: [PATCH 520/670] Move scoped functionality into local function --- .../Skinning/Editor/SkinSelectionHandler.cs | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index c2ad08f0dc..23f36ffe5b 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -281,23 +281,7 @@ namespace osu.Game.Skinning.Editor if (parent == null) return drawable.Anchor; - Vector2 screenPosition; - { - var quad = drawable.ScreenSpaceDrawQuad; - var origin = drawable.Origin; - - screenPosition = quad.TopLeft; - - if (origin.HasFlagFast(Anchor.x2)) - screenPosition.X += quad.Width; - else if (origin.HasFlagFast(Anchor.x1)) - screenPosition.X += quad.Width / 2f; - - if (origin.HasFlagFast(Anchor.y2)) - screenPosition.Y += quad.Height; - else if (origin.HasFlagFast(Anchor.y1)) - screenPosition.Y += quad.Height / 2f; - } + var screenPosition = getScreenPosition(); var absolutePosition = parent.ToLocalSpace(screenPosition); var factor = parent.RelativeToAbsoluteFactor; @@ -319,6 +303,26 @@ namespace osu.Game.Skinning.Editor result |= getAnchorFromPosition(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2); return result; + + Vector2 getScreenPosition() + { + var quad = drawable.ScreenSpaceDrawQuad; + var origin = drawable.Origin; + + var pos = quad.TopLeft; + + if (origin.HasFlagFast(Anchor.x2)) + pos.X += quad.Width; + else if (origin.HasFlagFast(Anchor.x1)) + pos.X += quad.Width / 2f; + + if (origin.HasFlagFast(Anchor.y2)) + pos.Y += quad.Height; + else if (origin.HasFlagFast(Anchor.y1)) + pos.Y += quad.Height / 2f; + + return pos; + } } private static void applyAnchor(Drawable drawable, Anchor anchor) From b12adc6073b7d9c493a97bc9b2e8c5172fc46d88 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 10:48:03 +0300 Subject: [PATCH 521/670] Remove all test skinning changes in favour of the `ISkinSource.AllSources` path --- .../TestSceneLegacyBeatmapSkin.cs | 7 +++- .../TestSceneSkinFallbacks.cs | 24 ++++++++++- osu.Game/Screens/Play/Player.cs | 4 +- .../Tests/Beatmaps/HitObjectSampleTest.cs | 42 +++++++++++++++++-- .../Beatmaps/LegacyBeatmapSkinColourTest.cs | 32 ++++++++++---- .../Tests/Visual/LegacySkinPlayerTestScene.cs | 17 +++++++- osu.Game/Tests/Visual/PlayerTestScene.cs | 8 ---- osu.Game/Tests/Visual/TestPlayer.cs | 42 ------------------- 8 files changed, 108 insertions(+), 68 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs index 0077ff9e3c..bc3daca16f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs @@ -101,10 +101,15 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("is custom hyper dash fruit colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashFruitColour == TestSkin.HYPER_DASH_FRUIT_COLOUR); } - protected override ExposedPlayer CreateTestPlayer() => new CatchExposedPlayer(); + protected override ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new CatchExposedPlayer(userHasCustomColours); private class CatchExposedPlayer : ExposedPlayer { + public CatchExposedPlayer(bool userHasCustomColours) + : base(userHasCustomColours) + { + } + public Color4 UsableHyperDashColour => GameplayClockContainer.ChildrenOfType() .First() diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 5ef27ab5ea..334d27e0a9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -22,6 +22,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osu.Game.Storyboards; +using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { @@ -99,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private AudioManager audio { get; set; } - protected override ISkin GetPlayerSkin() => testUserSkin; + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin); protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, audio, testBeatmapSkin); @@ -116,6 +117,27 @@ namespace osu.Game.Rulesets.Osu.Tests protected override ISkin GetSkin() => skin; } + public class SkinProvidingPlayer : TestPlayer + { + private readonly TestSource userSkin; + + public SkinProvidingPlayer(TestSource userSkin) + { + this.userSkin = userSkin; + } + + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.CacheAs(userSkin); + + return dependencies; + } + } + public class TestSource : ISkinSource { private readonly string identifier; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index efc5fcfbe5..58f60d14cf 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -228,7 +228,7 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(GameplayBeatmap); - var rulesetSkinProvider = CreateRulesetSkinProvider(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin); + var rulesetSkinProvider = new RulesetSkinProvidingContainer(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin); // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. @@ -309,8 +309,6 @@ namespace osu.Game.Screens.Play protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); - protected virtual RulesetSkinProvidingContainer CreateRulesetSkinProvider(Ruleset ruleset, IBeatmap beatmap, ISkin beatmapSkin) => new RulesetSkinProvidingContainer(ruleset, beatmap, beatmapSkin); - private Drawable createUnderlayComponents() => DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 7af0397726..7ee6c519b7 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -47,11 +47,15 @@ namespace osu.Game.Tests.Beatmaps private readonly TestResourceStore userSkinResourceStore = new TestResourceStore(); private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore(); + private SkinSourceDependencyContainer dependencies; private IBeatmap currentTestBeatmap; protected sealed override bool HasCustomSteps => true; protected override bool Autoplay => true; + protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent))); + protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap; protected sealed override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) @@ -59,8 +63,6 @@ namespace osu.Game.Tests.Beatmaps protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false); - protected override ISkin GetPlayerSkin() => Skin; - protected void CreateTestWithBeatmap(string filename) { CreateTest(() => @@ -107,7 +109,8 @@ namespace osu.Game.Tests.Beatmaps } }; - Skin = new LegacySkin(userSkinInfo, this); + // Need to refresh the cached skin source to refresh the skin resource store. + dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this)); }); } @@ -129,6 +132,39 @@ namespace osu.Game.Tests.Beatmaps #endregion + private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer + { + public ISkinSource SkinSource; + + private readonly IReadOnlyDependencyContainer fallback; + + public SkinSourceDependencyContainer(IReadOnlyDependencyContainer fallback) + { + this.fallback = fallback; + } + + public object Get(Type type) + { + if (type == typeof(ISkinSource)) + return SkinSource; + + return fallback.Get(type); + } + + public object Get(Type type, CacheInfo info) + { + if (type == typeof(ISkinSource)) + return SkinSource; + + return fallback.Get(type, info); + } + + public void Inject(T instance) where T : class + { + // Never used directly + } + } + private class TestResourceStore : IResourceStore { public readonly List PerformedLookups = new List(); diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index bb4768982a..86c75c297a 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osu.Game.Tests.Visual; using osuTK.Graphics; @@ -47,24 +49,36 @@ namespace osu.Game.Tests.Beatmaps protected virtual ExposedPlayer LoadBeatmap(bool userHasCustomColours) { + ExposedPlayer player; + Beatmap.Value = testBeatmap; - ExposedPlayer player = CreateTestPlayer(); - - player.SetSkin(new TestSkin(userHasCustomColours)); - - LoadScreen(player); + LoadScreen(player = CreateTestPlayer(userHasCustomColours)); return player; } - protected virtual ExposedPlayer CreateTestPlayer() => new ExposedPlayer(); + protected virtual ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new ExposedPlayer(userHasCustomColours); - protected class ExposedPlayer : TestPlayer + protected class ExposedPlayer : Player { - public ExposedPlayer() - : base(false, false) + protected readonly bool UserHasCustomColours; + + public ExposedPlayer(bool userHasCustomColours) + : base(new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { + UserHasCustomColours = userHasCustomColours; + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(new TestSkin(UserHasCustomColours)); + return dependencies; } public IReadOnlyList UsableComboColours => diff --git a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs index f4f351d46c..d74be70df8 100644 --- a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs +++ b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; +using osu.Game.Rulesets; using osu.Game.Skinning; namespace osu.Game.Tests.Visual @@ -15,12 +16,15 @@ namespace osu.Game.Tests.Visual { protected LegacySkin LegacySkin { get; private set; } - protected override ISkin GetPlayerSkin() => LegacySkin; + private ISkinSource legacySkinSource; + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(legacySkinSource); [BackgroundDependencyLoader] private void load(SkinManager skins) { LegacySkin = new DefaultLegacySkin(skins); + legacySkinSource = new SkinProvidingContainer(LegacySkin); } [SetUpSteps] @@ -47,5 +51,16 @@ namespace osu.Game.Tests.Visual AddUntilStep("wait for components to load", () => this.ChildrenOfType().All(t => t.ComponentsLoaded)); } + + public class SkinProvidingPlayer : TestPlayer + { + [Cached(typeof(ISkinSource))] + private readonly ISkinSource skinSource; + + public SkinProvidingPlayer(ISkinSource skinSource) + { + this.skinSource = skinSource; + } + } } } diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index f5fad895e2..088e997de9 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -10,7 +10,6 @@ using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Skinning; namespace osu.Game.Tests.Visual { @@ -79,8 +78,6 @@ namespace osu.Game.Tests.Visual } Player = CreatePlayer(ruleset); - Player.SetSkin(GetPlayerSkin()); - LoadScreen(Player); } @@ -96,11 +93,6 @@ namespace osu.Game.Tests.Visual [NotNull] protected abstract Ruleset CreatePlayerRuleset(); - /// - /// Creates an to be put inside the 's ruleset skin providing container. - /// - protected virtual ISkin GetPlayerSkin() => null; - protected sealed override Ruleset CreateRuleset() => CreatePlayerRuleset(); protected virtual TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false); diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 19fd7068b9..09da4db952 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -1,21 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; -using osu.Game.Skinning; namespace osu.Game.Tests.Visual { @@ -80,41 +74,5 @@ namespace osu.Game.Tests.Visual { ScoreProcessor.NewJudgement += r => Results.Add(r); } - - public ISkin Skin { get; private set; } - - private TestSkinProvidingContainer rulesetSkinProvider; - - internal void SetSkin(ISkin skin) - { - Debug.Assert(rulesetSkinProvider == null); - - if (Skin != null) - throw new InvalidOperationException("A skin has already been set."); - - Skin = skin; - } - - protected override RulesetSkinProvidingContainer CreateRulesetSkinProvider(Ruleset ruleset, IBeatmap beatmap, ISkin beatmapSkin) - => rulesetSkinProvider = new TestSkinProvidingContainer(Skin, ruleset, beatmap, beatmapSkin); - - private class TestSkinProvidingContainer : RulesetSkinProvidingContainer - { - private readonly ISkin skin; - - public TestSkinProvidingContainer(ISkin skin, Ruleset ruleset, IBeatmap beatmap, [CanBeNull] ISkin beatmapSkin) - : base(ruleset, beatmap, beatmapSkin) - { - this.skin = skin; - } - - protected override void UpdateSkins() - { - base.UpdateSkins(); - - if (skin != null) - SkinSources.Insert(0, skin is LegacySkin ? GetLegacyRulesetTransformedSkin(skin) : skin); - } - } } } From d0cdc07b1167fefd67bc1db900b9edf302104343 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 10:49:21 +0300 Subject: [PATCH 522/670] Reuse `AllSources` when looking up on `FindProvider` --- osu.Game/Skinning/SkinManager.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 6bd4888eb5..e220cad42a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -236,14 +236,11 @@ namespace osu.Game.Skinning public ISkin FindProvider(Func lookupFunction) { - if (lookupFunction(CurrentSkin.Value)) - return CurrentSkin.Value; - - if (CurrentSkin.Value is LegacySkin && lookupFunction(DefaultLegacySkin)) - return DefaultLegacySkin; - - if (lookupFunction(DefaultSkin)) - return DefaultSkin; + foreach (var source in AllSources) + { + if (lookupFunction(source)) + return source; + } return null; } From c1284940e1da334498ca471952b2f8a4c8663033 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 10:49:37 +0300 Subject: [PATCH 523/670] Fix potentially providing the same skin instance twice in `AllSources` --- osu.Game/Skinning/SkinManager.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index e220cad42a..3234cca0ac 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -251,10 +251,11 @@ namespace osu.Game.Skinning { yield return CurrentSkin.Value; - if (CurrentSkin.Value is LegacySkin) + if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultLegacySkin) yield return DefaultLegacySkin; - yield return DefaultSkin; + if (CurrentSkin.Value != DefaultSkin) + yield return DefaultSkin; } } From a9c783025262cf069e39c92f02e6760cc6c3d8cb Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 22 Jun 2021 10:05:03 +0900 Subject: [PATCH 524/670] Fix NRE when hit object blueprint is not implemented --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 3e97e15cca..3552305664 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -256,7 +256,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (drawable == null) return null; - return CreateHitObjectBlueprintFor(item).With(b => b.DrawableObject = drawable); + return CreateHitObjectBlueprintFor(item)?.With(b => b.DrawableObject = drawable); } public virtual HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => null; From fbe44dac34f255e4ac297fceedcd4c55ca8e1afb Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 22 Jun 2021 10:05:29 +0900 Subject: [PATCH 525/670] Add empty catch hit object composer --- .../Editor/TestSceneEditor.cs | 14 +++++++++++++ osu.Game.Rulesets.Catch/CatchRuleset.cs | 4 ++++ .../Edit/CatchHitObjectComposer.cs | 20 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs new file mode 100644 index 0000000000..161c685043 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + [TestFixture] + public class TestSceneEditor : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new CatchRuleset(); + } +} diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 23ce444560..cdd9a24f6a 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -22,7 +22,9 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using System; using osu.Framework.Extensions.EnumExtensions; +using osu.Game.Rulesets.Catch.Edit; using osu.Game.Rulesets.Catch.Skinning.Legacy; +using osu.Game.Rulesets.Edit; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch @@ -182,5 +184,7 @@ namespace osu.Game.Rulesets.Catch public int LegacyID => 2; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); + + public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); } } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs new file mode 100644 index 0000000000..392d7f004f --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -0,0 +1,20 @@ +// 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.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public class CatchHitObjectComposer : HitObjectComposer + { + public CatchHitObjectComposer(CatchRuleset ruleset) + : base(ruleset) + { + } + + protected override IReadOnlyList CompositionTools => System.Array.Empty(); + } +} From b8ccfe6ea7d4dfde1c743ae814090060148cf9cd Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 22 Jun 2021 10:39:32 +0900 Subject: [PATCH 526/670] Add basic selection blueprint movement logic --- .../BananaShowerSelectionBlueprint.cs | 15 +++++++ .../Blueprints/CatchSelectionBlueprint.cs | 38 +++++++++++++++++ .../Blueprints/FruitSelectionBlueprint.cs | 15 +++++++ .../Edit/CatchBlueprintContainer.cs | 35 ++++++++++++++++ .../Edit/CatchHitObjectComposer.cs | 12 ++++++ .../Edit/CatchSelectionHandler.cs | 41 +++++++++++++++++++ 6 files changed, 156 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs new file mode 100644 index 0000000000..9132b1a9e8 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Catch.Objects; + +namespace osu.Game.Rulesets.Catch.Edit.Blueprints +{ + public class BananaShowerSelectionBlueprint : CatchSelectionBlueprint + { + public BananaShowerSelectionBlueprint(BananaShower hitObject) + : base(hitObject) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs new file mode 100644 index 0000000000..3ca57590e2 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.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 osu.Framework.Allocation; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Edit.Blueprints +{ + public abstract class CatchSelectionBlueprint : HitObjectSelectionBlueprint + where THitObject : CatchHitObject + { + [Resolved] + private Playfield playfield { get; set; } + + protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer; + + public override Vector2 ScreenSpaceSelectionPoint + { + get + { + float x = HitObject.OriginalX; + float y = HitObjectContainer.PositionAtTime(HitObject.StartTime); + return HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight)); + } + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SelectionQuad.Contains(screenSpacePos); + + protected CatchSelectionBlueprint(THitObject hitObject) + : base(hitObject) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs new file mode 100644 index 0000000000..f104dbfb63 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Catch.Objects; + +namespace osu.Game.Rulesets.Catch.Edit.Blueprints +{ + public class FruitSelectionBlueprint : CatchSelectionBlueprint + { + public FruitSelectionBlueprint(Fruit hitObject) + : base(hitObject) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs new file mode 100644 index 0000000000..b189a43915 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Catch.Edit.Blueprints; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit.Compose.Components; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public class CatchBlueprintContainer : ComposeBlueprintContainer + { + public CatchBlueprintContainer(CatchHitObjectComposer composer) + : base(composer) + { + } + + protected override SelectionHandler CreateSelectionHandler() => new CatchSelectionHandler(); + + public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) + { + switch (hitObject) + { + case Fruit fruit: + return new FruitSelectionBlueprint(fruit); + + case BananaShower bananaShower: + return new BananaShowerSelectionBlueprint(bananaShower); + } + + return base.CreateHitObjectBlueprintFor(hitObject); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 392d7f004f..00a10b86f4 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Catch.Edit { @@ -16,5 +18,15 @@ namespace osu.Game.Rulesets.Catch.Edit } protected override IReadOnlyList CompositionTools => System.Array.Empty(); + + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + { + var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition); + // TODO: implement position snap + result.ScreenSpacePosition.X = screenSpacePosition.X; + return result; + } + + protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this); } } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs new file mode 100644 index 0000000000..1fe303dc39 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -0,0 +1,41 @@ +// 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.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public class CatchSelectionHandler : EditorSelectionHandler + { + [Resolved] + private Playfield playfield { get; set; } + + protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer; + + public override bool HandleMovement(MoveSelectionEvent moveEvent) + { + var blueprint = moveEvent.Blueprint; + Vector2 originalPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint); + Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta); + float deltaX = targetPosition.X - originalPosition.X; + + EditorBeatmap.PerformOnSelection(h => + { + if (!(h is CatchHitObject hitObject)) return; + + if (hitObject is BananaShower) return; + + // TODO: confine in bounds + hitObject.OriginalXBindable.Value += deltaX; + }); + + return true; + } + } +} From c28cd5dd75bfa68ff12762bed9985793f81b6e8c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 22 Jun 2021 11:39:04 +0900 Subject: [PATCH 527/670] Add basic juice stream selection blueprint --- .../JuiceStreamSelectionBlueprint.cs | 57 +++++++++++++++++++ .../Edit/CatchBlueprintContainer.cs | 3 + .../Edit/CatchSelectionHandler.cs | 4 ++ 3 files changed, 64 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs new file mode 100644 index 0000000000..0cbce144a3 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs @@ -0,0 +1,57 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Edit.Blueprints +{ + public class JuiceStreamSelectionBlueprint : CatchSelectionBlueprint + { + public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(computeBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight))); + + private float minNestedX; + private float maxNestedX; + + public JuiceStreamSelectionBlueprint(JuiceStream hitObject) + : base(hitObject) + { + } + + [BackgroundDependencyLoader] + private void load() + { + HitObject.DefaultsApplied += onDefaultsApplied; + calculateObjectBounds(); + } + + private void onDefaultsApplied(HitObject _) => calculateObjectBounds(); + + private void calculateObjectBounds() + { + minNestedX = HitObject.NestedHitObjects.OfType().Min(nested => nested.OriginalX) - HitObject.OriginalX; + maxNestedX = HitObject.NestedHitObjects.OfType().Max(nested => nested.OriginalX) - HitObject.OriginalX; + } + + private RectangleF computeBoundingBox() + { + float left = HitObject.OriginalX + minNestedX; + float right = HitObject.OriginalX + maxNestedX; + float top = HitObjectContainer.PositionAtTime(HitObject.EndTime); + float bottom = HitObjectContainer.PositionAtTime(HitObject.StartTime); + float objectRadius = CatchHitObject.OBJECT_RADIUS * HitObject.Scale; + return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + HitObject.DefaultsApplied -= onDefaultsApplied; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs index b189a43915..7f2782a474 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs @@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Catch.Edit case Fruit fruit: return new FruitSelectionBlueprint(fruit); + case JuiceStream juiceStream: + return new JuiceStreamSelectionBlueprint(juiceStream); + case BananaShower bananaShower: return new BananaShowerSelectionBlueprint(bananaShower); } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index 1fe303dc39..a0cedcb807 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.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.Linq; using osu.Framework.Allocation; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Objects; @@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Catch.Edit // TODO: confine in bounds hitObject.OriginalXBindable.Value += deltaX; + + foreach (var nested in hitObject.NestedHitObjects.OfType()) + nested.OriginalXBindable.Value += deltaX; }); return true; From 0078d7dc18af34d5216f7acf98b2fb55aa6d6256 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 22 Jun 2021 12:04:24 +0900 Subject: [PATCH 528/670] Add outline to selected fruit --- .../Blueprints/Components/FruitOutline.cs | 39 +++++++++++++++++++ .../Blueprints/FruitSelectionBlueprint.cs | 12 ++++++ 2 files changed, 51 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs new file mode 100644 index 0000000000..35c481d793 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs @@ -0,0 +1,39 @@ +// 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.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Skinning.Default; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components +{ + public class FruitOutline : CompositeDrawable + { + public FruitOutline() + { + Anchor = Anchor.BottomLeft; + Origin = Anchor.Centre; + Size = new Vector2(2 * CatchHitObject.OBJECT_RADIUS); + // TODO: use skinned component? + InternalChild = new BorderPiece(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour osuColour) + { + Colour = osuColour.Yellow; + } + + public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject) + { + X = hitObject.EffectiveX; + Y = hitObjectContainer.PositionAtTime(hitObject.StartTime); + Scale = new Vector2(hitObject.Scale); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs index f104dbfb63..9665aac2fb 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs @@ -1,15 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; namespace osu.Game.Rulesets.Catch.Edit.Blueprints { public class FruitSelectionBlueprint : CatchSelectionBlueprint { + private readonly FruitOutline outline; + public FruitSelectionBlueprint(Fruit hitObject) : base(hitObject) { + InternalChild = outline = new FruitOutline(); + } + + protected override void Update() + { + base.Update(); + + if (IsSelected) + outline.UpdateFrom(HitObjectContainer, HitObject); } } } From 4d7a8777954d0b821688a9b037773740dfc761b1 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 22 Jun 2021 12:10:16 +0900 Subject: [PATCH 529/670] Add basic fruit placement tool --- .../Blueprints/CatchPlacementBlueprint.cs | 27 ++++++++++ .../Blueprints/FruitPlacementBlueprint.cs | 50 +++++++++++++++++++ .../Edit/CatchHitObjectComposer.cs | 5 +- .../Edit/FruitCompositionTool.cs | 20 ++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs new file mode 100644 index 0000000000..69054e2c81 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs @@ -0,0 +1,27 @@ +// 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.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Catch.Edit.Blueprints +{ + public class CatchPlacementBlueprint : PlacementBlueprint + where THitObject : CatchHitObject, new() + { + protected new THitObject HitObject => (THitObject)base.HitObject; + + protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer; + + [Resolved] + private Playfield playfield { get; set; } + + public CatchPlacementBlueprint() + : base(new THitObject()) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs new file mode 100644 index 0000000000..0f28cf6786 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.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.Input.Events; +using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Edit.Blueprints +{ + public class FruitPlacementBlueprint : CatchPlacementBlueprint + { + private readonly FruitOutline outline; + + public FruitPlacementBlueprint() + { + InternalChild = outline = new FruitOutline(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + BeginPlacement(); + } + + protected override void Update() + { + base.Update(); + + outline.UpdateFrom(HitObjectContainer, HitObject); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button != MouseButton.Left) return base.OnMouseDown(e); + + EndPlacement(true); + return true; + } + + public override void UpdateTimeAndPosition(SnapResult result) + { + base.UpdateTimeAndPosition(result); + + HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 00a10b86f4..2099be8864 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -17,7 +17,10 @@ namespace osu.Game.Rulesets.Catch.Edit { } - protected override IReadOnlyList CompositionTools => System.Array.Empty(); + protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + { + new FruitCompositionTool(), + }; public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { diff --git a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs new file mode 100644 index 0000000000..da716bbe1d --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Catch.Edit.Blueprints; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public class FruitCompositionTool : HitObjectCompositionTool + { + public FruitCompositionTool() + : base(nameof(Fruit)) + { + } + + public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint(); + } +} From e8907b53a8678b52657ea75b46a2fcea017e0ecb Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 22 Jun 2021 12:30:09 +0900 Subject: [PATCH 530/670] Add basic banana shower placement tool --- .../Edit/BananaShowerCompositionTool.cs | 20 ++++++ .../BananaShowerPlacementBlueprint.cs | 72 +++++++++++++++++++ .../Blueprints/Components/TimeSpanOutline.cs | 61 ++++++++++++++++ .../Edit/CatchHitObjectComposer.cs | 1 + 4 files changed, 154 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs diff --git a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs new file mode 100644 index 0000000000..be18f223eb --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Catch.Edit.Blueprints; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public class BananaShowerCompositionTool : HitObjectCompositionTool + { + public BananaShowerCompositionTool() + : base(nameof(BananaShower)) + { + } + + public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint(); + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs new file mode 100644 index 0000000000..843cca3c7b --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -0,0 +1,72 @@ +// 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.Input.Events; +using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Edit; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Edit.Blueprints +{ + public class BananaShowerPlacementBlueprint : CatchPlacementBlueprint + { + private readonly TimeSpanOutline outline; + + public BananaShowerPlacementBlueprint() + { + InternalChild = outline = new TimeSpanOutline(); + } + + protected override void Update() + { + base.Update(); + + outline.UpdateFrom(HitObjectContainer, HitObject); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + switch (PlacementActive) + { + case PlacementState.Waiting: + if (e.Button != MouseButton.Left) break; + + BeginPlacement(true); + return true; + + case PlacementState.Active: + if (e.Button != MouseButton.Right) break; + + if (HitObject.Duration < 0) + { + HitObject.StartTime = HitObject.EndTime; + HitObject.Duration = -HitObject.Duration; + } + + EndPlacement(HitObject.Duration > 0); + return true; + } + + return base.OnMouseDown(e); + } + + public override void UpdateTimeAndPosition(SnapResult result) + { + base.UpdateTimeAndPosition(result); + + if (!(result.Time is double time)) return; + + switch (PlacementActive) + { + case PlacementState.Waiting: + HitObject.StartTime = time; + break; + + case PlacementState.Active: + HitObject.EndTime = time; + break; + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs new file mode 100644 index 0000000000..4e5164e965 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs @@ -0,0 +1,61 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components +{ + public class TimeSpanOutline : CompositeDrawable + { + private const float border_width = 4; + + private bool isEmpty = true; + + public TimeSpanOutline() + { + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + RelativeSizeAxes = Axes.X; + + Masking = true; + BorderThickness = border_width; + + // a box is needed to make edge visible + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Transparent + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour osuColour) + { + BorderColour = osuColour.Yellow; + } + + public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, BananaShower hitObject) + { + float startY = hitObjectContainer.PositionAtTime(hitObject.StartTime); + float endY = hitObjectContainer.PositionAtTime(hitObject.EndTime); + + Y = Math.Max(startY, endY); + float height = Math.Abs(startY - endY); + + bool wasEmpty = isEmpty; + isEmpty = height == 0; + if (wasEmpty != isEmpty) + this.FadeTo(isEmpty ? 0.5f : 1f, 150); + + Height = Math.Max(height, border_width); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 2099be8864..c11e5eb45b 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Catch.Edit protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { new FruitCompositionTool(), + new BananaShowerCompositionTool() }; public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) From 21331d3a13c80c4f8fc4c3b8aac4438fec1ea7ae Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 22 Jun 2021 12:47:24 +0900 Subject: [PATCH 531/670] Disable caught object stacking in editor --- .../Edit/CatchEditorPlayfield.cs | 27 +++++++++++++++++++ .../Edit/CatchHitObjectComposer.cs | 6 +++++ .../Edit/DrawableCatchEditorRuleset.cs | 21 +++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs create mode 100644 osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs new file mode 100644 index 0000000000..d383eb9ba6 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.UI; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public class CatchEditorPlayfield : CatchPlayfield + { + // TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen. + public CatchEditorPlayfield(BeatmapDifficulty difficulty) + : base(difficulty) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // TODO: honor "hit animation" setting? + CatcherArea.MovableCatcher.CatchFruitOnPlate = false; + + // TODO: disable hit lighting as well + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index c11e5eb45b..d9712bc8e9 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -17,6 +20,9 @@ namespace osu.Game.Rulesets.Catch.Edit { } + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) => + new DrawableCatchEditorRuleset(ruleset, beatmap, mods); + protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { new FruitCompositionTool(), diff --git a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs new file mode 100644 index 0000000000..0344709d45 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public class DrawableCatchEditorRuleset : DrawableCatchRuleset + { + public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + : base(ruleset, beatmap, mods) + { + } + + protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); + } +} From c4fde635c6f44c18687475a0f76401d9d66a35e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 17:41:20 +0900 Subject: [PATCH 532/670] Ensure duplicate mods cannot be defined --- osu.Game.Tests/Mods/ModUtilsTest.cs | 8 ++++++++ osu.Game/Utils/ModUtils.cs | 23 ++++++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 9f27289d7e..0fac4521d9 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -14,6 +14,14 @@ namespace osu.Game.Tests.Mods [TestFixture] public class ModUtilsTest { + [Test] + public void TestModIsNotCompatibleWithItself() + { + var mod = new Mock(); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object, mod.Object }, out var invalid), Is.False); + Assert.That(invalid, Is.EquivalentTo(new[] { mod.Object })); + } + [Test] public void TestModIsCompatibleByItself() { diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 98766cb844..7485950f47 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -51,14 +51,31 @@ namespace osu.Game.Utils /// Whether all s in the combination are compatible with each-other. public static bool CheckCompatibleSet(IEnumerable combination, [NotNullWhen(false)] out List? invalidMods) { - combination = FlattenMods(combination).ToArray(); + var mods = FlattenMods(combination).ToArray(); invalidMods = null; - foreach (var mod in combination) + // ensure there are no duplicate mod definitions. + for (int i = 0; i < mods.Length; i++) + { + var candidate = mods[i]; + + for (int j = i + 1; j < mods.Length; j++) + { + var m = mods[j]; + + if (candidate.Equals(m)) + { + invalidMods ??= new List(); + invalidMods.Add(m); + } + } + } + + foreach (var mod in mods) { foreach (var type in mod.IncompatibleMods) { - foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m))) + foreach (var invalid in mods.Where(m => type.IsInstanceOfType(m))) { if (invalid == mod) continue; From 6e0801b8529e5e3a832dd90ea56c1bc9e8638ee2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 17:41:27 +0900 Subject: [PATCH 533/670] Fix incorrect existing test case --- osu.Game.Tests/Mods/ModUtilsTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 0fac4521d9..4c126f0a3b 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -155,7 +155,7 @@ namespace osu.Game.Tests.Mods // multi mod. new object[] { - new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModHalfTime() }, + new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() }, new[] { typeof(MultiMod) } }, // valid pair. From b8126e3ca8440180aaa1cc1812a0763f27e599e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 17:59:24 +0900 Subject: [PATCH 534/670] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1dc99bb60a..3c4380e355 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3c52405f8e..f91620bd25 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 3689ce51f2..22c4340ba2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 31cbb36a647f6221506da9cce093a6f0ed850d4b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 12:02:21 +0300 Subject: [PATCH 535/670] Implement `FindProvider` and `AllSources` properly on all test `ISkinSource`s --- osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs | 4 ++-- osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs | 4 ++-- osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs | 4 ++-- osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs | 5 +++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index fe962d3cb8..06bdb562e4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -114,9 +114,9 @@ namespace osu.Game.Rulesets.Osu.Tests public IBindable GetConfig(TLookup lookup) => null; - public ISkin FindProvider(Func lookupFunction) => null; + public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null; - public IEnumerable AllSources => Enumerable.Empty(); + public IEnumerable AllSources => new[] { this }; public event Action SourceChanged { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 334d27e0a9..662cbaee68 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -167,9 +167,9 @@ namespace osu.Game.Rulesets.Osu.Tests public IBindable GetConfig(TLookup lookup) => null; - public ISkin FindProvider(Func lookupFunction) => null; + public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null; - public IEnumerable AllSources => Enumerable.Empty(); + public IEnumerable AllSources => new[] { this }; public event Action SourceChanged; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index 3317d8f80a..f29fbbf52b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -331,7 +331,7 @@ namespace osu.Game.Tests.Visual.Gameplay public ISkin FindProvider(Func lookupFunction) => throw new NotImplementedException(); - public IEnumerable AllSources => Enumerable.Empty(); + public IEnumerable AllSources => throw new NotImplementedException(); public event Action SourceChanged { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 59edb527eb..51f8b90c8f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -147,8 +147,8 @@ namespace osu.Game.Tests.Visual.Gameplay public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT); public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup); - public ISkin FindProvider(Func lookupFunction) => source?.FindProvider(lookupFunction); - public IEnumerable AllSources => source.AllSources; + public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source?.FindProvider(lookupFunction); + public IEnumerable AllSources => new[] { this }.Concat(source?.AllSources); public void TriggerSourceChanged() { diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index 86c75c297a..e0c2965fa0 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -159,8 +159,9 @@ namespace osu.Game.Tests.Beatmaps remove { } } - public ISkin FindProvider(Func lookupFunction) => null; - public IEnumerable AllSources => Enumerable.Empty(); + public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null; + + public IEnumerable AllSources => new[] { this }; } } } From ece63b9ba1ee6cc8683255dab0abf7d5cc439ee0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 12:03:55 +0300 Subject: [PATCH 536/670] Remove unused using directive --- osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index 06bdb562e4..211b0e8145 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; From 71e2815e7ec670693bf94ad8885e10b13015f338 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 12:05:17 +0300 Subject: [PATCH 537/670] Update and improve code documentation Co-authored-by: Dean Herbert --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 5 ++--- osu.Game/Skinning/SkinManager.cs | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index f1cc3df3de..c48aeca99a 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -13,8 +13,7 @@ namespace osu.Game.Skinning { /// /// A type of specialized for and other gameplay-related components. - /// Providing access to the skin sources and the beatmap skin each surrounded with the ruleset legacy skin transformer. - /// While also limiting lookups from falling back to any parent s out of this container. + /// Providing access to parent skin sources and the beatmap skin each surrounded with the ruleset legacy skin transformer. /// public class RulesetSkinProvidingContainer : SkinProvidingContainer { @@ -22,7 +21,7 @@ namespace osu.Game.Skinning protected readonly IBeatmap Beatmap; /// - /// This container already re-exposes all skin sources in a ruleset-usable form. + /// This container already re-exposes all parent sources in a ruleset-usable form. /// Therefore disallow falling back to any parent any further. /// protected override bool AllowFallingBackToParent => false; diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 3234cca0ac..4cde4cd2b8 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -34,8 +34,8 @@ namespace osu.Game.Skinning /// Handles the storage and retrieval of s. /// /// - /// This is also exposed and cached as on a game-wide level for general components across the game. - /// Lookups from gameplay components are instead covered by , and are never hit here. + /// This is also exposed and cached as to allow for any component to potentially have skinning support. + /// For gameplay components, see which adds extra legacy and toggle logic that may affect the lookup process. /// [ExcludeFromDynamicCompile] public class SkinManager : ArchiveModelManager, ISkinSource, IStorageResourceProvider From a4b66bec2e570af80da16610bc99b859fa4651bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 18:18:25 +0900 Subject: [PATCH 538/670] Ensure realm contexts are flushed when update thread changes native thread --- osu.Game/OsuGameBase.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3a08ef684f..fb083ea7d5 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -182,6 +182,13 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); + + Host.UpdateThreadChanging += () => + { + var blocking = realmFactory.BlockAllOperations(); + Schedule(() => blocking.Dispose()); + }; + AddInternal(realmFactory); dependencies.CacheAs(Storage); From 37f7486fb1f2be6c5c7a2cf355266ec811265f77 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Jun 2021 12:25:29 +0300 Subject: [PATCH 539/670] Fix potential null reference in LINQ method --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 51f8b90c8f..ccf13e1e8f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Gameplay public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup); public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source?.FindProvider(lookupFunction); - public IEnumerable AllSources => new[] { this }.Concat(source?.AllSources); + public IEnumerable AllSources => new[] { this }.Concat(source?.AllSources ?? Enumerable.Empty()); public void TriggerSourceChanged() { From b9a9174168f62115acb2bcc78f9054d6a248c27e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 18:26:42 +0900 Subject: [PATCH 540/670] Remove live realm bindings for now --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 37 +++++++++------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 432c52c2e9..d2e07c7d15 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -159,28 +159,6 @@ namespace osu.Game.Overlays.Toolbar }; } - private RealmKeyBinding realmKeyBinding; - - protected override void LoadComplete() - { - base.LoadComplete(); - - if (Hotkey == null) return; - - realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); - - if (realmKeyBinding != null) - { - realmKeyBinding.PropertyChanged += (sender, args) => - { - if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) - updateKeyBindingTooltip(); - }; - } - - updateKeyBindingTooltip(); - } - protected override bool OnMouseDown(MouseDownEvent e) => true; protected override bool OnClick(ClickEvent e) @@ -196,6 +174,7 @@ namespace osu.Game.Overlays.Toolbar HoverBackground.FadeIn(200); tooltipContainer.FadeIn(100); + return base.OnHover(e); } @@ -222,6 +201,20 @@ namespace osu.Game.Overlays.Toolbar private void updateKeyBindingTooltip() { + if (Hotkey == null) return; + + var realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); + + // TODO: temporarily disabled to avoid crashes when querying after ExecutionState is changed. + // if (realmKeyBinding != null) + // { + // realmKeyBinding.PropertyChanged += (sender, args) => + // { + // if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) + // updateKeyBindingTooltip(); + // }; + // } + if (realmKeyBinding != null) { var keyBindingString = realmKeyBinding.KeyCombination.ReadableString(); From f03c2bab481df4a343171b34e9eb1303095cee47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 22:45:13 +0900 Subject: [PATCH 541/670] Update event name in line with framework changes --- osu.Game/OsuGameBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index fb083ea7d5..43c81783fe 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -183,7 +183,7 @@ namespace osu.Game dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); - Host.UpdateThreadChanging += () => + Host.UpdateThreadPausing += () => { var blocking = realmFactory.BlockAllOperations(); Schedule(() => blocking.Dispose()); From 9cb9ef5c563316df57f7a49400359463ead3f49b Mon Sep 17 00:00:00 2001 From: aitani9 <55509723+aitani9@users.noreply.github.com> Date: Tue, 22 Jun 2021 13:31:41 -0700 Subject: [PATCH 542/670] Refactor the menu's max height to be a property --- osu.Game/Overlays/Settings/SettingsEnumDropdown.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index 9987a0c607..fa9973fb08 100644 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs @@ -14,13 +14,15 @@ namespace osu.Game.Overlays.Settings protected new class DropdownControl : OsuEnumDropdown { + protected virtual int MenuMaxHeight => 200; + public DropdownControl() { Margin = new MarginPadding { Top = 5 }; RelativeSizeAxes = Axes.X; } - protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200); + protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = MenuMaxHeight); } } } From 59928a7d91d230e32fec56d395d65c485457cc52 Mon Sep 17 00:00:00 2001 From: aitani9 <55509723+aitani9@users.noreply.github.com> Date: Tue, 22 Jun 2021 13:32:45 -0700 Subject: [PATCH 543/670] Decrease the max height of dropdown menus in mod settings --- osu.Game/Configuration/SettingSourceAttribute.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 3e50613093..cd0652f401 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; namespace osu.Game.Configuration @@ -149,7 +150,7 @@ namespace osu.Game.Configuration break; case IBindable bindable: - var dropdownType = typeof(SettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]); + var dropdownType = typeof(ModSettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]); var dropdown = (Drawable)Activator.CreateInstance(dropdownType); dropdownType.GetProperty(nameof(SettingsDropdown.LabelText))?.SetValue(dropdown, attr.Label); @@ -183,5 +184,16 @@ namespace osu.Game.Configuration => obj.GetSettingsSourceProperties() .OrderBy(attr => attr.Item1) .ToArray(); + + private class ModSettingsEnumDropdown : SettingsEnumDropdown + where T : struct, Enum + { + protected override OsuDropdown CreateDropdown() => new ModDropdownControl(); + + private class ModDropdownControl : DropdownControl + { + protected override int MenuMaxHeight => 100; + } + } } } From 1a7bfafc698f03a840d8e8381281f0aeb1b39df0 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 09:34:11 +0900 Subject: [PATCH 544/670] Add icon for composition tools --- osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs | 4 ++++ osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs index be18f223eb..31075db7d1 100644 --- a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs +++ b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; @@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Catch.Edit { } + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); + public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs index da716bbe1d..f776fe39c1 100644 --- a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs +++ b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; @@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Catch.Edit { } + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); + public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint(); } } From e96814bb866cb631bf5a8111df88f99c686b199e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 09:37:30 +0900 Subject: [PATCH 545/670] Remove comment about using skin for blueprint As the current game-wise direction is not using skin elements in blueprints. The design of the blueprint could be improved somehow, though. --- .../Edit/Blueprints/Components/FruitOutline.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs index 35c481d793..8769acc382 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs @@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components Anchor = Anchor.BottomLeft; Origin = Anchor.Centre; Size = new Vector2(2 * CatchHitObject.OBJECT_RADIUS); - // TODO: use skinned component? InternalChild = new BorderPiece(); } From eec44574739ed2348b620b141b8b61a94568920d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 09:40:07 +0900 Subject: [PATCH 546/670] Add `[CanBeNull]` to methods returning null by default --- osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 2 ++ .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 8a4d381535..185f029d14 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -94,6 +95,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Creates a for a specific item. /// /// The item to create the overlay for. + [CanBeNull] protected virtual SelectionBlueprint CreateBlueprintFor(T item) => null; protected virtual DragBox CreateDragBox(Action performSelect) => new DragBox(performSelect); diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 3552305664..79b38861ee 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Humanizer; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -259,6 +260,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return CreateHitObjectBlueprintFor(item)?.With(b => b.DrawableObject = drawable); } + [CanBeNull] public virtual HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => null; private void hitObjectAdded(HitObject obj) From a9b8736f70b19173731153284b5a29347870ee34 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 10:18:44 +0900 Subject: [PATCH 547/670] Order field and properties consistently --- .../Edit/Blueprints/CatchSelectionBlueprint.cs | 10 +++++----- osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs index 3ca57590e2..298f9474b0 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs @@ -13,11 +13,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints public abstract class CatchSelectionBlueprint : HitObjectSelectionBlueprint where THitObject : CatchHitObject { - [Resolved] - private Playfield playfield { get; set; } - - protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer; - public override Vector2 ScreenSpaceSelectionPoint { get @@ -30,6 +25,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SelectionQuad.Contains(screenSpacePos); + protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer; + + [Resolved] + private Playfield playfield { get; set; } + protected CatchSelectionBlueprint(THitObject hitObject) : base(hitObject) { diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index a0cedcb807..f3a4d72c87 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Edit { public class CatchSelectionHandler : EditorSelectionHandler { + protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer; + [Resolved] private Playfield playfield { get; set; } - protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer; - public override bool HandleMovement(MoveSelectionEvent moveEvent) { var blueprint = moveEvent.Blueprint; From 69c8865a043d51dc824b33997f59051b4b82cb72 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 10:19:09 +0900 Subject: [PATCH 548/670] Use more consistent method names --- .../Edit/Blueprints/JuiceStreamSelectionBlueprint.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs index 0cbce144a3..d6b8c35a09 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { public class JuiceStreamSelectionBlueprint : CatchSelectionBlueprint { - public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(computeBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight))); + public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight))); private float minNestedX; private float maxNestedX; @@ -26,18 +26,18 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints private void load() { HitObject.DefaultsApplied += onDefaultsApplied; - calculateObjectBounds(); + computeObjectBounds(); } - private void onDefaultsApplied(HitObject _) => calculateObjectBounds(); + private void onDefaultsApplied(HitObject _) => computeObjectBounds(); - private void calculateObjectBounds() + private void computeObjectBounds() { minNestedX = HitObject.NestedHitObjects.OfType().Min(nested => nested.OriginalX) - HitObject.OriginalX; maxNestedX = HitObject.NestedHitObjects.OfType().Max(nested => nested.OriginalX) - HitObject.OriginalX; } - private RectangleF computeBoundingBox() + private RectangleF getBoundingBox() { float left = HitObject.OriginalX + minNestedX; float right = HitObject.OriginalX + maxNestedX; From 5a5cb39c9f1d6d11f3eec86e03dbf85af8c694c7 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 10:19:39 +0900 Subject: [PATCH 549/670] Add some comments about logic --- .../Edit/Blueprints/BananaShowerPlacementBlueprint.cs | 1 + .../Edit/Blueprints/Components/TimeSpanOutline.cs | 5 ++--- osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 843cca3c7b..6dea8b0712 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints case PlacementState.Active: if (e.Button != MouseButton.Right) break; + // If the duration is negative, swap the start and the end time to make the duration positive. if (HitObject.Duration < 0) { HitObject.StartTime = HitObject.EndTime; diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs index 4e5164e965..f925783919 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs @@ -21,14 +21,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public TimeSpanOutline() { - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; + Anchor = Origin = Anchor.BottomLeft; RelativeSizeAxes = Axes.X; Masking = true; BorderThickness = border_width; - // a box is needed to make edge visible + // A box is needed to make the border visible. InternalChild = new Box { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index f3a4d72c87..c1a491d1ce 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.Catch.Edit // TODO: confine in bounds hitObject.OriginalXBindable.Value += deltaX; + // Move the nested hit objects to give an instant result before nested objects are recreated. foreach (var nested in hitObject.NestedHitObjects.OfType()) nested.OriginalXBindable.Value += deltaX; }); From 125e1434017835cb992cd9bcad8b6b580b818cd9 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 10:26:01 +0900 Subject: [PATCH 550/670] Fix banana shower placement outline initial opacity --- .../Edit/Blueprints/Components/TimeSpanOutline.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs index f925783919..65dfce0493 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components { private const float border_width = 4; + private const float opacity_when_empty = 0.5f; + private bool isEmpty = true; public TimeSpanOutline() @@ -26,6 +28,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components Masking = true; BorderThickness = border_width; + Alpha = opacity_when_empty; // A box is needed to make the border visible. InternalChild = new Box @@ -52,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components bool wasEmpty = isEmpty; isEmpty = height == 0; if (wasEmpty != isEmpty) - this.FadeTo(isEmpty ? 0.5f : 1f, 150); + this.FadeTo(isEmpty ? opacity_when_empty : 1f, 150); Height = Math.Max(height, border_width); } From 0b351c99228cad3d84274a15774415a0a1ddaf87 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 10:57:04 +0900 Subject: [PATCH 551/670] Fix "possible NRE" inspection --- .../Compose/Components/Timeline/TimelineBlueprintContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 6f04f36b83..a642768574 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -89,7 +90,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } else { - placementBlueprint = CreateBlueprintFor(obj.NewValue); + placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull(); placementBlueprint.Colour = Color4.MediumPurple; From 49000b9501d9a8818679aede641c4ed843683f88 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 22 Jun 2021 08:48:41 -0700 Subject: [PATCH 552/670] Add multiplayer leave navigation tests --- .../Multiplayer/TestSceneMultiplayer.cs | 49 +++++++++++++++++++ osu.Game/Tests/Visual/ScreenTestScene.cs | 8 ++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index c5a6723508..599dfb082b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -6,15 +6,20 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Resources; @@ -159,6 +164,50 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen()); } + [Test] + public void TestLeaveNavigation() + { + loadMultiplayer(); + + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + AllowedMods = { new OsuModHidden() } + } + } + }); + + AddStep("open mod overlay", () => this.ChildrenOfType().ElementAt(2).Click()); + + AddStep("invoke on back button", () => multiplayerScreen.OnBackButton()); + + AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); + + AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); + + testLeave("lounge tab item", () => this.ChildrenOfType.BreadcrumbTabItem>().First().Click()); + + testLeave("back button", () => multiplayerScreen.OnBackButton()); + + // mimics home button and OS window close + testLeave("forced exit", () => multiplayerScreen.Exit()); + + void testLeave(string actionName, Action action) + { + AddStep($"leave via {actionName}", action); + + AddAssert("dialog overlay is visible", () => DialogOverlay.State.Value == Visibility.Visible); + + AddStep("close dialog overlay", () => InputManager.Key(Key.Escape)); + } + } + private void createRoom(Func room) { AddStep("open room", () => diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 33cc00e748..b30be05ac4 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -1,9 +1,11 @@ // 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.Framework.Testing; +using osu.Game.Overlays; using osu.Game.Screens; namespace osu.Game.Tests.Visual @@ -19,12 +21,16 @@ namespace osu.Game.Tests.Visual protected override Container Content => content; + [Cached] + protected DialogOverlay DialogOverlay { get; private set; } + protected ScreenTestScene() { base.Content.AddRange(new Drawable[] { Stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }, - content = new Container { RelativeSizeAxes = Axes.Both } + content = new Container { RelativeSizeAxes = Axes.Both }, + DialogOverlay = new DialogOverlay() }); } From dc428da06c70797d92563caa3b11e0a89088d74b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 22 Jun 2021 19:30:52 -0700 Subject: [PATCH 553/670] Fix test regression --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 07162c3cd1..b6ae91844a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -55,7 +55,12 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestExitWithoutSave() { - AddStep("exit without save", () => Editor.Exit()); + AddStep("exit without save", () => + { + Editor.Exit(); + DialogOverlay.CurrentDialog.PerformOkAction(); + }); + AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen()); AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true); } From 6fd020d91d76c3017fed6e4f99d2228d5d6a391b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 23 Jun 2021 12:00:38 +0900 Subject: [PATCH 554/670] Remove unnecessary public accessibility All interface members are implicitly public. --- osu.Game/Rulesets/Mods/IHasSeed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/IHasSeed.cs b/osu.Game/Rulesets/Mods/IHasSeed.cs index b6852d960b..8e317b08be 100644 --- a/osu.Game/Rulesets/Mods/IHasSeed.cs +++ b/osu.Game/Rulesets/Mods/IHasSeed.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods { public interface IHasSeed { - public Bindable Seed { get; } + Bindable Seed { get; } } public class SeedSettingsControl : SettingsItem From 54084f8a9c7009766380f8cc12f87aca866030f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 13:40:05 +0900 Subject: [PATCH 555/670] Move `SeedSettingsControl` to own file --- osu.Game/Rulesets/Mods/IHasSeed.cs | 82 ----------------- osu.Game/Rulesets/Mods/SeedSettingsControl.cs | 92 +++++++++++++++++++ 2 files changed, 92 insertions(+), 82 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/SeedSettingsControl.cs diff --git a/osu.Game/Rulesets/Mods/IHasSeed.cs b/osu.Game/Rulesets/Mods/IHasSeed.cs index 8e317b08be..001a9d214c 100644 --- a/osu.Game/Rulesets/Mods/IHasSeed.cs +++ b/osu.Game/Rulesets/Mods/IHasSeed.cs @@ -2,11 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods { @@ -14,81 +9,4 @@ namespace osu.Game.Rulesets.Mods { Bindable Seed { get; } } - - public class SeedSettingsControl : SettingsItem - { - protected override Drawable CreateControl() => new SeedControl - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 5 } - }; - - private sealed class SeedControl : CompositeDrawable, IHasCurrentValue - { - private readonly BindableWithCurrent current = new BindableWithCurrent(); - - public Bindable Current - { - get => current; - set - { - current.Current = value; - seedNumberBox.Text = value.Value.ToString(); - } - } - - private readonly OsuNumberBox seedNumberBox; - - public SeedControl() - { - AutoSizeAxes = Axes.Y; - - InternalChildren = new[] - { - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 2), - new Dimension(GridSizeMode.Relative, 0.25f) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - seedNumberBox = new OsuNumberBox - { - RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true - } - } - } - } - }; - - seedNumberBox.Current.BindValueChanged(e => - { - int? value = null; - - if (int.TryParse(e.NewValue, out var intVal)) - value = intVal; - - current.Value = value; - }); - } - - protected override void Update() - { - if (current.Value == null) - seedNumberBox.Text = current.Current.Value.ToString(); - } - } - } } diff --git a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs new file mode 100644 index 0000000000..5c57717d93 --- /dev/null +++ b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// A settings control for use by mods which have a customisable seed value. + /// + public class SeedSettingsControl : SettingsItem + { + protected override Drawable CreateControl() => new SeedControl + { + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 5 } + }; + + private sealed class SeedControl : CompositeDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current; + set + { + current.Current = value; + seedNumberBox.Text = value.Value.ToString(); + } + } + + private readonly OsuNumberBox seedNumberBox; + + public SeedControl() + { + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 2), + new Dimension(GridSizeMode.Relative, 0.25f) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + seedNumberBox = new OsuNumberBox + { + RelativeSizeAxes = Axes.X, + CommitOnFocusLost = true + } + } + } + } + }; + + seedNumberBox.Current.BindValueChanged(e => + { + int? value = null; + + if (int.TryParse(e.NewValue, out var intVal)) + value = intVal; + + current.Value = value; + }); + } + + protected override void Update() + { + if (current.Value == null) + seedNumberBox.Text = current.Current.Value.ToString(); + } + } + } +} From bae42a89089559b7beecec677664fd57aa8705e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 14:07:37 +0900 Subject: [PATCH 556/670] Add inline comment explaining why the height is set lower --- osu.Game/Configuration/SettingSourceAttribute.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index cd0652f401..cee5883d9a 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -192,6 +192,7 @@ namespace osu.Game.Configuration private class ModDropdownControl : DropdownControl { + // Set low enough to workaround nested scroll issues (see https://github.com/ppy/osu-framework/issues/4536). protected override int MenuMaxHeight => 100; } } From e1b2c63e0941a08606f95bb08d28712c73576582 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 14:08:24 +0900 Subject: [PATCH 557/670] Add `IApplicableToBeatmapProcessor` mod interface --- osu.Game/Beatmaps/WorkingBeatmap.cs | 3 +++ .../Mods/IApplicableToBeatmapProcessor.cs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 712a3dd33a..662d24cc83 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -133,6 +133,9 @@ namespace osu.Game.Beatmaps IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted); + foreach (var mod in mods.OfType()) + mod.ApplyToBeatmapProcessor(processor); + processor?.PreProcess(); // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs new file mode 100644 index 0000000000..e23a5d8d99 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs @@ -0,0 +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 osu.Game.Beatmaps; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// Interface for a that applies changes to a . + /// + public interface IApplicableToBeatmapProcessor : IApplicableMod + { + /// + /// Applies this to a . + /// + void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor); + } +} From a0fd7f72ac8968bfa6d7a430ab571bdf3e558582 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 14:11:25 +0900 Subject: [PATCH 558/670] Use IApplicableToBeatmapProcessor in CatchModHardRock --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs | 8 ++++---- osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index fac5d03833..3a5322ce82 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -8,7 +8,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.MathUtils; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Beatmaps @@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { public const int RNG_SEED = 1337; + public bool HardRockOffsets { get; set; } + public CatchBeatmapProcessor(IBeatmap beatmap) : base(beatmap) { @@ -43,11 +44,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } } - public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods) + public void ApplyPositionOffsets(IBeatmap beatmap) { var rng = new FastRandom(RNG_SEED); - bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock); float? lastPosition = null; double lastStartTime = 0; @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps switch (obj) { case Fruit fruit: - if (shouldApplyHardRockOffset) + if (HardRockOffsets) applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng); break; diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs index 0dde6aa06e..68b6ce96a3 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs @@ -7,10 +7,14 @@ using osu.Game.Rulesets.Catch.Beatmaps; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModHardRock : ModHardRock, IApplicableToBeatmap + public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor { public override double ScoreMultiplier => 1.12; - public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this); + public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor) + { + var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor; + catchProcessor.HardRockOffsets = true; + } } } From ad60b9d5a08000018b5a21b1a6656ce43884fe40 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 23 Jun 2021 14:20:57 +0900 Subject: [PATCH 559/670] Allow catch difficulty adjust to enable hard rock offsets --- .../Mods/CatchModDifficultyAdjust.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 5f1736450a..0eec47ed09 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -5,11 +5,12 @@ using System.Linq; using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModDifficultyAdjust : ModDifficultyAdjust + public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor { [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension @@ -31,6 +32,9 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; + [SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")] + public BindableBool HardRockOffsets { get; } = new BindableBool(); + protected override void ApplyLimits(bool extended) { base.ApplyLimits(extended); @@ -70,5 +74,11 @@ namespace osu.Game.Rulesets.Catch.Mods ApplySetting(CircleSize, cs => difficulty.CircleSize = cs); ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar); } + + public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor) + { + var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor; + catchProcessor.HardRockOffsets = HardRockOffsets.Value; + } } } From c9a203cc11f12532054493629617e21daaba4b21 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 23 Jun 2021 15:03:34 +0900 Subject: [PATCH 560/670] Move collections db in-place --- osu.Game/Collections/CollectionManager.cs | 41 ++++++++++++++++------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index b53cc659f7..b2f36c75f4 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -257,27 +257,42 @@ namespace osu.Game.Collections { Interlocked.Increment(ref lastSave); + // This is NOT thread-safe!! try { - // This is NOT thread-safe!! + var tempPath = Path.GetTempFileName(); - using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write))) + using (var ms = new MemoryStream()) { - sw.Write(database_version); - - var collectionsCopy = Collections.ToArray(); - sw.Write(collectionsCopy.Length); - - foreach (var c in collectionsCopy) + using (var sw = new SerializationWriter(ms, true)) { - sw.Write(c.Name.Value); + sw.Write(database_version); - var beatmapsCopy = c.Beatmaps.ToArray(); - sw.Write(beatmapsCopy.Length); + var collectionsCopy = Collections.ToArray(); + sw.Write(collectionsCopy.Length); - foreach (var b in beatmapsCopy) - sw.Write(b.MD5Hash); + foreach (var c in collectionsCopy) + { + sw.Write(c.Name.Value); + + var beatmapsCopy = c.Beatmaps.ToArray(); + sw.Write(beatmapsCopy.Length); + + foreach (var b in beatmapsCopy) + sw.Write(b.MD5Hash); + } } + + using (var fs = File.OpenWrite(tempPath)) + ms.WriteTo(fs); + + var storagePath = storage.GetFullPath(database_name); + var storageBackupPath = storage.GetFullPath($"{database_name}.bak"); + if (File.Exists(storageBackupPath)) + File.Delete(storageBackupPath); + if (File.Exists(storagePath)) + File.Move(storagePath, storageBackupPath); + File.Move(tempPath, storagePath); } if (saveFailures < 10) From e4d17bd75773c52b61ec26c610483aec2ba4b34d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 15:06:28 +0900 Subject: [PATCH 561/670] Remove commented code This will be reverted in a future change, no need to keep it around. --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index d2e07c7d15..4a33f9e296 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -205,16 +205,6 @@ namespace osu.Game.Overlays.Toolbar var realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); - // TODO: temporarily disabled to avoid crashes when querying after ExecutionState is changed. - // if (realmKeyBinding != null) - // { - // realmKeyBinding.PropertyChanged += (sender, args) => - // { - // if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) - // updateKeyBindingTooltip(); - // }; - // } - if (realmKeyBinding != null) { var keyBindingString = realmKeyBinding.KeyCombination.ReadableString(); From 5828606c1332152b71c942ae2b06d7d98dc445ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 23 Jun 2021 15:09:42 +0900 Subject: [PATCH 562/670] Prefer using the backup file if it exists --- osu.Game/Collections/CollectionManager.cs | 30 ++++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index b2f36c75f4..04ea7eaebf 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -35,6 +35,7 @@ namespace osu.Game.Collections private const int database_version = 30000000; private const string database_name = "collection.db"; + private const string database_backup_name = "collection.db.bak"; public readonly BindableList Collections = new BindableList(); @@ -56,11 +57,14 @@ namespace osu.Game.Collections { Collections.CollectionChanged += collectionsChanged; - if (storage.Exists(database_name)) + // If a backup file exists, it means the previous write operation didn't complete successfully. Always prefer the backup file in such a case. + string filename = storage.Exists(database_backup_name) ? database_backup_name : database_name; + + if (storage.Exists(filename)) { List beatmapCollections; - using (var stream = storage.GetStream(database_name)) + using (var stream = storage.GetStream(filename)) beatmapCollections = readCollections(stream); // intentionally fire-and-forget async. @@ -286,13 +290,21 @@ namespace osu.Game.Collections using (var fs = File.OpenWrite(tempPath)) ms.WriteTo(fs); - var storagePath = storage.GetFullPath(database_name); - var storageBackupPath = storage.GetFullPath($"{database_name}.bak"); - if (File.Exists(storageBackupPath)) - File.Delete(storageBackupPath); - if (File.Exists(storagePath)) - File.Move(storagePath, storageBackupPath); - File.Move(tempPath, storagePath); + var databasePath = storage.GetFullPath(database_name); + var databaseBackupPath = storage.GetFullPath(database_backup_name); + + // Back up the existing database, clearing any existing backup. + if (File.Exists(databaseBackupPath)) + File.Delete(databaseBackupPath); + if (File.Exists(databasePath)) + File.Move(databasePath, databaseBackupPath); + + // Move the new database in-place of the existing one. + File.Move(tempPath, databasePath); + + // If everything succeeded up to this point, remove the backup file. + if (File.Exists(databaseBackupPath)) + File.Delete(databaseBackupPath); } if (saveFailures < 10) From 0510282dcbe1a3ad581d01ed57d1bfcfea431c18 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 23 Jun 2021 15:10:03 +0900 Subject: [PATCH 563/670] Add missing ctor --- osu.Game/IO/Legacy/SerializationWriter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/IO/Legacy/SerializationWriter.cs b/osu.Game/IO/Legacy/SerializationWriter.cs index bb8014fe54..9ebeaf616e 100644 --- a/osu.Game/IO/Legacy/SerializationWriter.cs +++ b/osu.Game/IO/Legacy/SerializationWriter.cs @@ -18,8 +18,8 @@ namespace osu.Game.IO.Legacy /// handle null strings and simplify use with ISerializable. public class SerializationWriter : BinaryWriter { - public SerializationWriter(Stream s) - : base(s, Encoding.UTF8) + public SerializationWriter(Stream s, bool leaveOpen = false) + : base(s, Encoding.UTF8, leaveOpen) { } From e7981eae9bbca5e40afbe9103628cfe17f9af9ba Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 23 Jun 2021 16:14:05 +0900 Subject: [PATCH 564/670] Safeguard load procedure a bit more --- osu.Game/Collections/CollectionManager.cs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 04ea7eaebf..e6e8bedcf4 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -55,16 +55,26 @@ namespace osu.Game.Collections [BackgroundDependencyLoader] private void load() { + if (storage.Exists(database_backup_name)) + { + // If a backup file exists, it means the previous write operation didn't run to completion. + // Always prefer the backup file in such a case as it's the most recent copy that is guaranteed to not be malformed. + // + // The database is saved 100ms after any change, and again when the game is closed, so there shouldn't be a large diff between the two files in the worst case. + if (storage.Exists(database_name)) + storage.Delete(database_name); + File.Copy(storage.GetFullPath(database_backup_name), storage.GetFullPath(database_name)); + } + + // Only accept changes after/if the above succeeds to prevent simultaneously accessing either file. + // Todo: This really should not be delayed like this, but is unlikely to cause issues because CollectionManager loads very early in execution. Collections.CollectionChanged += collectionsChanged; - // If a backup file exists, it means the previous write operation didn't complete successfully. Always prefer the backup file in such a case. - string filename = storage.Exists(database_backup_name) ? database_backup_name : database_name; - - if (storage.Exists(filename)) + if (storage.Exists(database_name)) { List beatmapCollections; - using (var stream = storage.GetStream(filename)) + using (var stream = storage.GetStream(database_name)) beatmapCollections = readCollections(stream); // intentionally fire-and-forget async. From 2f657b972ddba9fefb4ada925f36067c3b275e99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 16:29:18 +0900 Subject: [PATCH 565/670] Remove local configuration of rider data sources --- .idea/.idea.osu.Desktop/.idea/dataSources.xml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 .idea/.idea.osu.Desktop/.idea/dataSources.xml diff --git a/.idea/.idea.osu.Desktop/.idea/dataSources.xml b/.idea/.idea.osu.Desktop/.idea/dataSources.xml deleted file mode 100644 index 10f8c1c84d..0000000000 --- a/.idea/.idea.osu.Desktop/.idea/dataSources.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - sqlite.xerial - true - org.sqlite.JDBC - jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db - - - - - - \ No newline at end of file From 28f4c56cd6531b64f6631076fa911d62a2867b43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 16:30:17 +0900 Subject: [PATCH 566/670] Fix minor typo in comment --- osu.Game/OsuGameBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3a08ef684f..6195d8e1ea 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -258,7 +258,7 @@ namespace osu.Game dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true)); - // this should likely be moved to ArchiveModelManager when another case appers where it is necessary + // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to // allow lookups to be done on the child (ScoreManager in this case) to perform the cascading delete. List getBeatmapScores(BeatmapSetInfo set) From 7767e2e77fbc2919b3ec9cce8fcf1f99590dc1cf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 23 Jun 2021 17:34:30 +0900 Subject: [PATCH 567/670] Add to tooltip --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 0eec47ed09..98ddcaa21d 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -49,12 +49,14 @@ namespace osu.Game.Rulesets.Catch.Mods { string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}"; string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}"; + string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : $"Spicy patterns"; return string.Join(", ", new[] { circleSize, base.SettingDescription, - approachRate + approachRate, + spicyPatterns, }.Where(s => !string.IsNullOrEmpty(s))); } } From ed0552a9e8cce45220498581eec036f9640fde3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 17:19:18 +0900 Subject: [PATCH 568/670] Add failing test for FK constraint conflict on reimporting modified beatmap with scores present --- .../Beatmaps/IO/ImportBeatmapTest.cs | 72 ++++++++++++++++++- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 16 ++--- 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0d117f8755..66b5cbed0c 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -19,7 +19,9 @@ using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Scoring; using osu.Game.Tests.Resources; +using osu.Game.Tests.Scores.IO; using osu.Game.Users; using SharpCompress.Archives; using SharpCompress.Archives.Zip; @@ -185,10 +187,58 @@ namespace osu.Game.Tests.Beatmaps.IO } } - private string hashFile(string filename) + [Test] + public async Task TestImportThenImportWithChangedHashedFile() { - using (var s = File.OpenRead(filename)) - return s.ComputeMD5Hash(); + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) + { + try + { + var osu = LoadOsuIntoHost(host); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + var imported = await LoadOszIntoOsu(osu); + + await createScoreForBeatmap(osu, imported.Beatmaps.First()); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + // arbitrary write to hashed file + // this triggers the special BeatmapManager.PreImport deletion/replacement flow. + using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText()) + await sw.WriteLineAsync("// changed"); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + var importedSecondTime = await osu.Dependencies.Get().Import(new ImportTask(temp)); + + ensureLoaded(osu); + + // check the newly "imported" beatmap is not the original. + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + } + finally + { + Directory.Delete(extractedFolder, true); + } + } + finally + { + host.Exit(); + } + } } [Test] @@ -895,6 +945,16 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); } + private Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap) + { + return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo + { + OnlineScoreID = 2, + Beatmap = beatmap, + BeatmapInfoID = beatmap.ID + }, new ImportScoreTest.TestArchiveReader()); + } + private void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false) { var manager = osu.Dependencies.Get(); @@ -904,6 +964,12 @@ namespace osu.Game.Tests.Beatmaps.IO : manager.GetAllUsableBeatmapSets().Count); } + private string hashFile(string filename) + { + using (var s = File.OpenRead(filename)) + return s.ComputeMD5Hash(); + } + private void checkBeatmapCount(OsuGameBase osu, int expected) { Assert.AreEqual(expected, osu.Dependencies.Get().QueryBeatmaps(_ => true).ToList().Count); diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 7522aca5dc..cd7d744f53 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO OnlineScoreID = 12345, }; - var imported = await loadScoreIntoOsu(osu, toImport); + var imported = await LoadScoreIntoOsu(osu, toImport); Assert.AreEqual(toImport.Rank, imported.Rank); Assert.AreEqual(toImport.TotalScore, imported.TotalScore); @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Scores.IO Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, }; - var imported = await loadScoreIntoOsu(osu, toImport); + var imported = await LoadScoreIntoOsu(osu, toImport); Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime)); @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Scores.IO } }; - var imported = await loadScoreIntoOsu(osu, toImport); + var imported = await LoadScoreIntoOsu(osu, toImport); Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]); Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]); @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Scores.IO } }; - var imported = await loadScoreIntoOsu(osu, toImport); + var imported = await LoadScoreIntoOsu(osu, toImport); var beatmapManager = osu.Dependencies.Get(); var scoreManager = osu.Dependencies.Get(); @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Scores.IO beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID))); Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true)); - var secondImport = await loadScoreIntoOsu(osu, imported); + var secondImport = await LoadScoreIntoOsu(osu, imported); Assert.That(secondImport, Is.Null); } finally @@ -163,7 +163,7 @@ namespace osu.Game.Tests.Scores.IO { var osu = LoadOsuIntoHost(host, true); - await loadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader()); + await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader()); var scoreManager = osu.Dependencies.Get(); @@ -177,7 +177,7 @@ namespace osu.Game.Tests.Scores.IO } } - private async Task loadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) + public static async Task LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) { var beatmapManager = osu.Dependencies.Get(); @@ -190,7 +190,7 @@ namespace osu.Game.Tests.Scores.IO return scoreManager.GetAllUsableScores().FirstOrDefault(); } - private class TestArchiveReader : ArchiveReader + internal class TestArchiveReader : ArchiveReader { public TestArchiveReader() : base("test_archive") From dcba7bf779fa2ab78a884cc17671eefa15a71082 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 17:19:32 +0900 Subject: [PATCH 569/670] Fix import flow potentially hitting foreign key constraint --- osu.Game/Beatmaps/BeatmapManager.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 00af06703d..e93d8c6eb5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -181,8 +181,13 @@ namespace osu.Game.Beatmaps if (existingOnlineId != null) { Delete(existingOnlineId); - beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID); - LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged."); + + // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. + existingOnlineId.OnlineBeatmapSetID = null; + foreach (var b in existingOnlineId.Beatmaps) + b.OnlineBeatmapID = null; + + LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted."); } } } From f6180b7e6a8adfa6560da82b4fe6805d5577f61e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 17:37:24 +0900 Subject: [PATCH 570/670] Mark `static` methods as such --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 66b5cbed0c..3b355da359 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -945,7 +945,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); } - private Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap) + private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap) { return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo { @@ -955,7 +955,7 @@ namespace osu.Game.Tests.Beatmaps.IO }, new ImportScoreTest.TestArchiveReader()); } - private void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false) + private static void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false) { var manager = osu.Dependencies.Get(); @@ -964,18 +964,18 @@ namespace osu.Game.Tests.Beatmaps.IO : manager.GetAllUsableBeatmapSets().Count); } - private string hashFile(string filename) + private static string hashFile(string filename) { using (var s = File.OpenRead(filename)) return s.ComputeMD5Hash(); } - private void checkBeatmapCount(OsuGameBase osu, int expected) + private static void checkBeatmapCount(OsuGameBase osu, int expected) { Assert.AreEqual(expected, osu.Dependencies.Get().QueryBeatmaps(_ => true).ToList().Count); } - private void checkSingleReferencedFileCount(OsuGameBase osu, int expected) + private static void checkSingleReferencedFileCount(OsuGameBase osu, int expected) { Assert.AreEqual(expected, osu.Dependencies.Get().QueryFiles(f => f.ReferenceCount == 1).Count()); } From 6215f2d42beb416dc3a645e9f45cca77624a8d15 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 23 Jun 2021 17:40:11 +0900 Subject: [PATCH 571/670] Remove unnecessary string interpolation --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 98ddcaa21d..bd7a1df2e4 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Mods { string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}"; string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}"; - string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : $"Spicy patterns"; + string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns"; return string.Join(", ", new[] { From d148656108320559df37f00998d9346c3e2f41e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 18:08:34 +0900 Subject: [PATCH 572/670] Update in line with framework event structural changes (and add unbind) --- osu.Game/OsuGameBase.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 43c81783fe..f81eaa08a5 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -183,11 +183,7 @@ namespace osu.Game dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); - Host.UpdateThreadPausing += () => - { - var blocking = realmFactory.BlockAllOperations(); - Schedule(() => blocking.Dispose()); - }; + Host.UpdateThread.ThreadPausing += onUpdateThreadPausing; AddInternal(realmFactory); @@ -363,6 +359,12 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); } + private void onUpdateThreadPausing() + { + var blocking = realmFactory.BlockAllOperations(); + Schedule(() => blocking.Dispose()); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -496,6 +498,9 @@ namespace osu.Game LocalConfig?.Dispose(); contextFactory.FlushConnections(); + + if (Host != null) + Host.UpdateThread.ThreadPausing -= onUpdateThreadPausing; } } } From 1bbfbb0d8e117ac9f5dad5a4f34aa3e1726ada2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 19:30:08 +0900 Subject: [PATCH 573/670] Fix test that never should have worked This was only working by luck until now. It was "correctly" matching on null online ID (see logic at https://github.com/ppy/osu/blob/abc96057b2cf50e02e0ec939645f6421684495d4/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs#L199-L207). Now it works by actually matching on the online ID. --- .../TestScenePlaylistsRoomSubScreen.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index a08a91314b..c4da2ab48a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -111,10 +111,27 @@ namespace osu.Game.Tests.Visual.Playlists public void TestBeatmapUpdatedOnReImport() { BeatmapSetInfo importedSet = null; + TestBeatmap beatmap = null; + + // this step is required to make sure the further imports actually get online IDs. + // all the playlist logic relies on online ID matching. + AddStep("remove all matching online IDs", () => + { + beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo); + + var existing = manager.QueryBeatmapSets(s => s.OnlineBeatmapSetID == beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID).ToList(); + + foreach (var s in existing) + { + s.OnlineBeatmapSetID = null; + foreach (var b in s.Beatmaps) + b.OnlineBeatmapID = null; + manager.Update(s); + } + }); AddStep("import altered beatmap", () => { - var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo); beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1; importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result; From 01a5d998a542c7d9e88d071525c7b1cbbbf46fd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Jun 2021 20:29:02 +0900 Subject: [PATCH 574/670] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1dc99bb60a..d0aff7b15e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3c52405f8e..0418e58593 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 3689ce51f2..6e2e169149 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 8a2026e3053c9c8bbc7d46c25029fa3d7f7f0506 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 23 Jun 2021 21:26:52 +0900 Subject: [PATCH 575/670] Schedule callback instead --- osu.Game/Collections/CollectionManager.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index e6e8bedcf4..fe04c70d62 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -55,6 +55,8 @@ namespace osu.Game.Collections [BackgroundDependencyLoader] private void load() { + Collections.CollectionChanged += collectionsChanged; + if (storage.Exists(database_backup_name)) { // If a backup file exists, it means the previous write operation didn't run to completion. @@ -66,10 +68,6 @@ namespace osu.Game.Collections File.Copy(storage.GetFullPath(database_backup_name), storage.GetFullPath(database_name)); } - // Only accept changes after/if the above succeeds to prevent simultaneously accessing either file. - // Todo: This really should not be delayed like this, but is unlikely to cause issues because CollectionManager loads very early in execution. - Collections.CollectionChanged += collectionsChanged; - if (storage.Exists(database_name)) { List beatmapCollections; @@ -82,7 +80,7 @@ namespace osu.Game.Collections } } - private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e) + private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { switch (e.Action) { @@ -106,7 +104,7 @@ namespace osu.Game.Collections } backgroundSave(); - } + }); /// /// Set an endpoint for notifications to be posted to. From 263370e1ff946475f394c74c3c9e6aebab2d064d Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 23 Jun 2021 16:42:14 +0200 Subject: [PATCH 576/670] Localize ruleset filter any filter button. --- .../BeatmapSearchRulesetFilterRow.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs index c2d0eea80c..e2c84c537c 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; @@ -22,14 +23,21 @@ namespace osu.Game.Overlays.BeatmapListing [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - AddItem(new RulesetInfo - { - Name = @"Any" - }); + AddTabItem(new RulesetFilterTabItemAny()); foreach (var r in rulesets.AvailableRulesets) AddItem(r); } } + + private class RulesetFilterTabItemAny : FilterTabItem + { + protected override LocalisableString LabelFor(RulesetInfo info) => BeatmapsStrings.ModeAny; + + public RulesetFilterTabItemAny() + : base(new RulesetInfo()) + { + } + } } } From d2d0f3bf9ba7e46922c03f95c33d1ca18eb33d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Jun 2021 17:55:40 +0200 Subject: [PATCH 577/670] Set more appropriate build time limits for GH Actions workflows --- .github/workflows/ci.yml | 1 + .github/workflows/report-nunit.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed3e99cb61..29cbdd2d37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ jobs: - { prettyname: macOS, fullname: macos-latest } - { prettyname: Linux, fullname: ubuntu-latest } threadingMode: ['SingleThread', 'MultiThreaded'] + timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v2 diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml index 381d2d49c5..e0ccd50989 100644 --- a/.github/workflows/report-nunit.yml +++ b/.github/workflows/report-nunit.yml @@ -21,6 +21,7 @@ jobs: - { prettyname: macOS } - { prettyname: Linux } threadingMode: ['SingleThread', 'MultiThreaded'] + timeout-minutes: 5 steps: - name: Annotate CI run with test results uses: dorny/test-reporter@v1.4.2 From c8022126dce165d7cd0e159e74c5f9cc726ef0b2 Mon Sep 17 00:00:00 2001 From: aitani9 <55509723+aitani9@users.noreply.github.com> Date: Wed, 23 Jun 2021 13:39:12 -0700 Subject: [PATCH 578/670] Add logo sound case for transitioning to song select --- osu.Game/Screens/Menu/ButtonSystem.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index a836f7bf09..da0edd07db 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -274,6 +274,10 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Play: buttonsPlay.First().Click(); return false; + + // no sound should be played if the logo is clicked on while transitioning to song select + case ButtonSystemState.EnteringMode: + return false; } } From 73590bfca1a4131581aa74c6d7b28a8378729221 Mon Sep 17 00:00:00 2001 From: Nathan Alo Date: Thu, 24 Jun 2021 07:20:31 +0800 Subject: [PATCH 579/670] Return an empty array when the sender is from system. --- osu.Game/Overlays/Chat/ChatLine.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index f43420e35e..8666d20159 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -240,6 +240,9 @@ namespace osu.Game.Overlays.Chat { get { + if (sender.Id == User.SYSTEM_USER.Id) + return Array.Empty(); + List items = new List { new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action) From 564c72bf74d6e5053f627d539f0118a9c4ee84e8 Mon Sep 17 00:00:00 2001 From: Nathan Alo Date: Thu, 24 Jun 2021 10:10:57 +0800 Subject: [PATCH 580/670] compare directly instead of comparing IDs --- osu.Game/Overlays/Chat/ChatLine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 8666d20159..987dc27802 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -240,7 +240,7 @@ namespace osu.Game.Overlays.Chat { get { - if (sender.Id == User.SYSTEM_USER.Id) + if (sender == User.SYSTEM_USER) return Array.Empty(); List items = new List From 812624a502cdff0cd276d53857f83516fc9d07da Mon Sep 17 00:00:00 2001 From: Nathan Alo Date: Thu, 24 Jun 2021 10:45:20 +0800 Subject: [PATCH 581/670] use `.Equals()` instead --- osu.Game/Overlays/Chat/ChatLine.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 987dc27802..01cfe9a55b 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -240,7 +240,7 @@ namespace osu.Game.Overlays.Chat { get { - if (sender == User.SYSTEM_USER) + if (sender.Equals(User.SYSTEM_USER)) return Array.Empty(); List items = new List @@ -248,7 +248,7 @@ namespace osu.Game.Overlays.Chat new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action) }; - if (sender.Id != api.LocalUser.Value.Id) + if (!sender.Equals(api.LocalUser.Value)) items.Add(new OsuMenuItem("Start Chat", MenuItemType.Standard, startChatAction)); return items.ToArray(); From cd6f17537531203eaa7f1782e15c73ba520f5e11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Jun 2021 13:29:06 +0900 Subject: [PATCH 582/670] Ensure beatmap is reloaded before each playlist room test run --- .../Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index c4da2ab48a..6d7a254ab9 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -40,8 +40,6 @@ namespace osu.Game.Tests.Visual.Playlists Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait(); - ((DummyAPIAccess)API).HandleRequest = req => { switch (req) @@ -58,6 +56,7 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetupSteps() { + AddStep("ensure has beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait()); AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(Room))); AddUntilStep("wait for load", () => match.IsCurrentScreen()); } From 2268d7f8a586f3121c420e2f04ceaf9bcd2a05b1 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 13:19:42 +0800 Subject: [PATCH 583/670] Extract utility methods into helper class; Better xmldoc and naming --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 79 +--------------- osu.Game.Rulesets.Osu/Utils/VectorHelper.cs | 100 ++++++++++++++++++++ 2 files changed, 102 insertions(+), 77 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Utils/VectorHelper.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 97e3d82664..bcd5274e02 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Osu.Utils; using osuTK; namespace osu.Game.Rulesets.Osu.Mods @@ -23,15 +24,6 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Description => "It never gets boring!"; - // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle. - // The closer the hit objects draw to the border, the sharper the turn - private const float playfield_edge_ratio = 0.375f; - - private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio; - private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio; - - private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2; - private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; private Random rng; @@ -113,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Mods distanceToPrev * (float)Math.Sin(current.AngleRad) ); - posRelativeToPrev = getRotatedVector(previous.EndPositionRandomised, posRelativeToPrev); + posRelativeToPrev = VectorHelper.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev); current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); @@ -185,73 +177,6 @@ namespace osu.Game.Rulesets.Osu.Mods } } - /// - /// Determines the position of the current hit object relative to the previous one. - /// - /// The position of the current hit object relative to the previous one - private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev) - { - var relativeRotationDistance = 0f; - - if (prevPosChanged.X < playfield_middle.X) - { - relativeRotationDistance = Math.Max( - (border_distance_x - prevPosChanged.X) / border_distance_x, - relativeRotationDistance - ); - } - else - { - relativeRotationDistance = Math.Max( - (prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x, - relativeRotationDistance - ); - } - - if (prevPosChanged.Y < playfield_middle.Y) - { - relativeRotationDistance = Math.Max( - (border_distance_y - prevPosChanged.Y) / border_distance_y, - relativeRotationDistance - ); - } - else - { - relativeRotationDistance = Math.Max( - (prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y, - relativeRotationDistance - ); - } - - return rotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevPosChanged, relativeRotationDistance / 2); - } - - /// - /// Rotates vector "initial" towards vector "destinantion" - /// - /// Vector to rotate to "destination" - /// Vector "initial" should be rotated to - /// The angle the vector should be rotated relative to the difference between the angles of the the two vectors. - /// Resulting vector - private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance) - { - var initialAngleRad = Math.Atan2(initial.Y, initial.X); - var destAngleRad = Math.Atan2(destination.Y, destination.X); - - var diff = destAngleRad - initialAngleRad; - - while (diff < -Math.PI) diff += 2 * Math.PI; - - while (diff > Math.PI) diff -= 2 * Math.PI; - - var finalAngleRad = initialAngleRad + relativeDistance * diff; - - return new Vector2( - initial.Length * (float)Math.Cos(finalAngleRad), - initial.Length * (float)Math.Sin(finalAngleRad) - ); - } - private class RandomObjectInfo { public float AngleRad { get; set; } diff --git a/osu.Game.Rulesets.Osu/Utils/VectorHelper.cs b/osu.Game.Rulesets.Osu/Utils/VectorHelper.cs new file mode 100644 index 0000000000..bc482a3dd7 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Utils/VectorHelper.cs @@ -0,0 +1,100 @@ +// 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.Game.Rulesets.Osu.UI; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Utils +{ + public static class VectorHelper + { + // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle. + // The closer the hit objects draw to the border, the sharper the turn + private const float playfield_edge_ratio = 0.375f; + + private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio; + private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio; + + private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2; + + /// + /// Rotate a hit object away from the playfield edge, while keeping a constant distance + /// from the previous object. + /// + /// + /// The extent of rotation depends on the position of the hit object. Hit objects + /// closer to the playfield edge will be rotated to a larger extent. + /// + /// Position of the previous hit object. + /// Position of the hit object to be rotated, relative to the previous hit object. + /// + /// The extent of rotation. + /// 0 means the hit object is never rotated. + /// 1 means the hit object will be fully rotated towards playfield center when it is originally at playfield edge. + /// + /// The new position of the hit object, relative to the previous one. + public static Vector2 RotateAwayFromEdge(Vector2 prevObjectPos, Vector2 posRelativeToPrev, float rotationRatio = 0.5f) + { + var relativeRotationDistance = 0f; + + if (prevObjectPos.X < playfield_middle.X) + { + relativeRotationDistance = Math.Max( + (border_distance_x - prevObjectPos.X) / border_distance_x, + relativeRotationDistance + ); + } + else + { + relativeRotationDistance = Math.Max( + (prevObjectPos.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x, + relativeRotationDistance + ); + } + + if (prevObjectPos.Y < playfield_middle.Y) + { + relativeRotationDistance = Math.Max( + (border_distance_y - prevObjectPos.Y) / border_distance_y, + relativeRotationDistance + ); + } + else + { + relativeRotationDistance = Math.Max( + (prevObjectPos.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y, + relativeRotationDistance + ); + } + + return RotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevObjectPos, relativeRotationDistance * rotationRatio); + } + + /// + /// Rotates vector "initial" towards vector "destination". + /// + /// The vector to be rotated. + /// The vector that "initial" should be rotated towards. + /// How much "initial" should be rotated. 0 means no rotation. 1 means "initial" is fully rotated to equal "destination". + /// The rotated vector. + public static Vector2 RotateVectorTowardsVector(Vector2 initial, Vector2 destination, float rotationRatio) + { + var initialAngleRad = Math.Atan2(initial.Y, initial.X); + var destAngleRad = Math.Atan2(destination.Y, destination.X); + + var diff = destAngleRad - initialAngleRad; + + while (diff < -Math.PI) diff += 2 * Math.PI; + + while (diff > Math.PI) diff -= 2 * Math.PI; + + var finalAngleRad = initialAngleRad + rotationRatio * diff; + + return new Vector2( + initial.Length * (float)Math.Cos(finalAngleRad), + initial.Length * (float)Math.Sin(finalAngleRad) + ); + } + } +} From 153e204d200bf554b19236c5fc283982047e148c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 13:22:10 +0800 Subject: [PATCH 584/670] Cap rotation ratio to 1 --- osu.Game.Rulesets.Osu/Utils/VectorHelper.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Utils/VectorHelper.cs b/osu.Game.Rulesets.Osu/Utils/VectorHelper.cs index bc482a3dd7..e3fe3249e8 100644 --- a/osu.Game.Rulesets.Osu/Utils/VectorHelper.cs +++ b/osu.Game.Rulesets.Osu/Utils/VectorHelper.cs @@ -68,7 +68,11 @@ namespace osu.Game.Rulesets.Osu.Utils ); } - return RotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevObjectPos, relativeRotationDistance * rotationRatio); + return RotateVectorTowardsVector( + posRelativeToPrev, + playfield_middle - prevObjectPos, + Math.Min(1, relativeRotationDistance * rotationRatio) + ); } /// From 63ab40ec24c24d9221f942f12cd2d60a3365bd42 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Jun 2021 14:37:26 +0900 Subject: [PATCH 585/670] Fix potential deadlocking behaviour (and convert `ResetEvent` to `Semaphore`) --- osu.Game/Database/RealmContextFactory.cs | 71 ++++++++++++++---------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index ed5931dd2b..4d81f8676f 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -26,6 +26,11 @@ namespace osu.Game.Database /// private readonly object writeLock = new object(); + /// + /// Lock object which is held during sections. + /// + private readonly SemaphoreSlim blockingLock = new SemaphoreSlim(1); + private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); @@ -33,8 +38,6 @@ namespace osu.Game.Database private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes"); private static readonly GlobalStatistic active_usages = GlobalStatistics.Get("Realm", "Active usages"); - private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true); - private Realm context; public Realm Context @@ -64,7 +67,7 @@ namespace osu.Game.Database public RealmUsage GetForRead() { reads.Value++; - return new RealmUsage(this); + return new RealmUsage(createContext()); } public RealmWriteUsage GetForWrite() @@ -73,8 +76,13 @@ namespace osu.Game.Database pending_writes.Value++; Monitor.Enter(writeLock); + return new RealmWriteUsage(createContext(), writeComplete); + } - return new RealmWriteUsage(this); + private void writeComplete() + { + Monitor.Exit(writeLock); + pending_writes.Value--; } protected override void Update() @@ -87,15 +95,22 @@ namespace osu.Game.Database private Realm createContext() { - blockingResetEvent.Wait(); - - contexts_created.Value++; - - return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + try { - SchemaVersion = schema_version, - MigrationCallback = onMigration, - }); + blockingLock.Wait(); + + contexts_created.Value++; + + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + { + SchemaVersion = schema_version, + MigrationCallback = onMigration, + }); + } + finally + { + blockingLock.Release(); + } } private void onMigration(Migration migration, ulong lastSchemaVersion) @@ -113,21 +128,23 @@ namespace osu.Game.Database { base.Dispose(isDisposing); - BlockAllOperations(); + // In the standard case, operations will already be blocked by the Update thread "pausing" from GameHost exit. + // This avoids waiting (forever) on an already entered semaphore. + if (context != null || active_usages.Value > 0) + BlockAllOperations(); + + blockingLock?.Dispose(); } public IDisposable BlockAllOperations() { - blockingResetEvent.Reset(); + blockingLock.Wait(); flushContexts(); - return new InvokeOnDisposal(this, r => endBlockingSection()); + return new InvokeOnDisposal(this, endBlockingSection); } - private void endBlockingSection() - { - blockingResetEvent.Set(); - } + private static void endBlockingSection(RealmContextFactory factory) => factory.blockingLock.Release(); private void flushContexts() { @@ -148,13 +165,10 @@ namespace osu.Game.Database { public readonly Realm Realm; - protected readonly RealmContextFactory Factory; - - internal RealmUsage(RealmContextFactory factory) + internal RealmUsage(Realm context) { active_usages.Value++; - Factory = factory; - Realm = factory.createContext(); + Realm = context; } /// @@ -172,11 +186,13 @@ namespace osu.Game.Database /// public class RealmWriteUsage : RealmUsage { + private readonly Action onWriteComplete; private readonly Transaction transaction; - internal RealmWriteUsage(RealmContextFactory factory) - : base(factory) + internal RealmWriteUsage(Realm context, Action onWriteComplete) + : base(context) { + this.onWriteComplete = onWriteComplete; transaction = Realm.BeginWrite(); } @@ -200,8 +216,7 @@ namespace osu.Game.Database base.Dispose(); - Monitor.Exit(Factory.writeLock); - pending_writes.Value--; + onWriteComplete(); } } } From 27735eeedba9d3947ff403d1db12924f789b13ed Mon Sep 17 00:00:00 2001 From: JimmyC7834 Date: Thu, 24 Jun 2021 13:45:38 +0800 Subject: [PATCH 586/670] fixed code --- .../BeatmapListingFilterControl.cs | 23 ++++++++----- osu.Game/Overlays/BeatmapListingOverlay.cs | 34 +++++++------------ 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index f49d913bb2..b6a0846407 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -25,8 +25,9 @@ namespace osu.Game.Overlays.BeatmapListing public class BeatmapListingFilterControl : CompositeDrawable { /// - /// Fired when a search finishes. Contains only new items in the case of pagination. - /// Fired with BeatmapListingSearchControl when non-supporter user used supporter-only filters. + /// Fired when a search finishes. + /// SearchFinished.Type = ResultsReturned when results returned. Contains only new items in the case of pagination. + /// SearchFinished.Type = SupporterOnlyFilter when a non-supporter user applied supporter-only filters. /// public Action SearchFinished; @@ -216,7 +217,7 @@ namespace osu.Game.Overlays.BeatmapListing getSetsRequest = null; // check if an non-supporter user used supporter-only filters - if (!api.LocalUser.Value.IsSupporter && (searchControl.Ranks.Any() || searchControl.Played.Value != SearchPlayed.Any)) + if (!api.LocalUser.Value.IsSupporter) { List filters = new List(); @@ -226,12 +227,14 @@ namespace osu.Game.Overlays.BeatmapListing if (searchControl.Ranks.Any()) filters.Add(BeatmapsStrings.ListingSearchFiltersRank); - SearchFinished?.Invoke(SearchResult.SupporterOnlyFilter(filters)); - } - else - { - SearchFinished?.Invoke(SearchResult.ResultsReturned(sets)); + if (filters.Any()) + { + SearchFinished?.Invoke(SearchResult.SupporterOnlyFilter(filters)); + return; + } } + + SearchFinished?.Invoke(SearchResult.ResultsReturned(sets)); }; api.Queue(getSetsRequest); @@ -259,10 +262,14 @@ namespace osu.Game.Overlays.BeatmapListing public enum SearchResultType { + // returned with Results ResultsReturned, + // non-supporter user applied supporter-only filters SupporterOnlyFilter } + // Results only valid when Type == ResultsReturned + // Filters only valid when Type == SupporterOnlyFilter public struct SearchResult { public SearchResultType Type { get; private set; } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 63800e6585..c2ba3d5bc0 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -256,7 +256,7 @@ namespace osu.Game.Overlays // using string literals as there's no proper processing for LocalizeStrings yet public class SupporterRequiredDrawable : CompositeDrawable { - private OsuSpriteText filtersText; + private LinkFlowContainer supporterRequiredText; public SupporterRequiredDrawable() { @@ -275,7 +275,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Horizontal, - Children = new[] + Children = new Drawable[] { new Sprite { @@ -285,39 +285,31 @@ namespace osu.Game.Overlays FillMode = FillMode.Fit, Texture = textures.Get(@"Online/supporter-required"), }, - createSupporterText(), + supporterRequiredText = new LinkFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Bottom = 10 }, + }, } }); } public void UpdateText(List filters) { - // use string literals for now - filtersText.Text = BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(); - } + supporterRequiredText.Clear(); - private Drawable createSupporterText() - { - LinkFlowContainer supporterRequiredText = new LinkFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Bottom = 10 }, - }; - - filtersText = (OsuSpriteText)supporterRequiredText.AddText( - "_", + supporterRequiredText.AddText( + BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(), t => { t.Font = OsuFont.GetFont(size: 16); t.Colour = Colour4.White; } - ).First(); + ); supporterRequiredText.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), @"/store/products/supporter-tag"); - - return supporterRequiredText; } } From 16d58935352391052c4ffce4fcf98b875ef0dbca Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 24 Jun 2021 15:02:45 +0900 Subject: [PATCH 587/670] Add `DroppedObjectContainer` class --- .../TestSceneCatcher.cs | 6 +++--- .../TestSceneCatcherArea.cs | 8 ++------ .../TestSceneHyperDashColouring.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 6 +----- osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 2 +- .../UI/DroppedObjectContainer.cs | 17 +++++++++++++++++ 7 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 900691ecae..28b1aaf03a 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests [Resolved] private OsuConfigManager config { get; set; } - private Container droppedObjectContainer; + private DroppedObjectContainer droppedObjectContainer; private TestCatcher catcher; @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Tests }; var trailContainer = new Container(); - droppedObjectContainer = new Container(); + droppedObjectContainer = new DroppedObjectContainer(); catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty); Child = new Container @@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public IEnumerable CaughtObjects => this.ChildrenOfType(); - public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty) + public TestCatcher(Container trailsTarget, DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty) : base(trailsTarget, droppedObjectTarget, difficulty) { } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 4af5098451..dca75eee14 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Framework.Utils; @@ -97,10 +96,7 @@ namespace osu.Game.Rulesets.Catch.Tests SetContents(_ => { - var droppedObjectContainer = new Container - { - RelativeSizeAxes = Axes.Both - }; + var droppedObjectContainer = new DroppedObjectContainer(); return new CatchInputManager(catchRuleset) { @@ -126,7 +122,7 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestCatcherArea : CatcherArea { - public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty) + public TestCatcherArea(DroppedObjectContainer droppedObjectContainer, BeatmapDifficulty beatmapDifficulty) : base(droppedObjectContainer, beatmapDifficulty) { } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 683a776dcc..4af528ef22 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("create hyper-dashing catcher", () => { - Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container()) + Child = setupSkinHierarchy(catcherArea = new CatcherArea(new DroppedObjectContainer()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 644facdabc..21501398fc 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; @@ -35,10 +34,7 @@ namespace osu.Game.Rulesets.Catch.UI public CatchPlayfield(BeatmapDifficulty difficulty) { - var droppedObjectContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }; + var droppedObjectContainer = new DroppedObjectContainer(); CatcherArea = new CatcherArea(droppedObjectContainer, difficulty) { diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 1f01dbabb5..6b42e3ddd3 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Contains objects dropped from the plate. /// - private readonly Container droppedObjectTarget; + private readonly DroppedObjectContainer droppedObjectTarget; public CatcherAnimationState CurrentState { @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.UI private readonly DrawablePool caughtBananaPool; private readonly DrawablePool caughtDropletPool; - public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null) + public Catcher([NotNull] Container trailsTarget, [NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null) { this.trailsTarget = trailsTarget; this.droppedObjectTarget = droppedObjectTarget; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index cdb15c2b4c..951a10fc01 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.UI /// private int currentDirection; - public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null) + public CatcherArea(DroppedObjectContainer droppedObjectContainer, BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); Children = new Drawable[] diff --git a/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs b/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs new file mode 100644 index 0000000000..b44b0caae4 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs @@ -0,0 +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 osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Catch.Objects.Drawables; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class DroppedObjectContainer : Container + { + public DroppedObjectContainer() + { + RelativeSizeAxes = Axes.Both; + } + } +} From ae09c23e4e262d3f6cc815ead57aec7f734a1f60 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 24 Jun 2021 15:56:53 +0900 Subject: [PATCH 588/670] Resolve `DroppedObjectContainer` via DI --- .../TestSceneCatcher.cs | 38 +++++++++++-------- .../TestSceneCatcherArea.cs | 13 ++++--- .../TestSceneHyperDashColouring.cs | 18 +++++++-- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 9 +++-- osu.Game.Rulesets.Catch/UI/Catcher.cs | 6 +-- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 4 +- 6 files changed, 54 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 28b1aaf03a..1ad45d2f13 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -6,8 +6,8 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Game.Rulesets.Catch.UI; using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.UI; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; @@ -31,10 +31,23 @@ namespace osu.Game.Rulesets.Catch.Tests [Resolved] private OsuConfigManager config { get; set; } - private DroppedObjectContainer droppedObjectContainer; + [Cached] + private readonly DroppedObjectContainer droppedObjectContainer; + + private readonly Container trailContainer; private TestCatcher catcher; + public TestSceneCatcher() + { + Add(trailContainer = new Container + { + Anchor = Anchor.Centre, + Depth = -1 + }); + Add(droppedObjectContainer = new DroppedObjectContainer()); + } + [SetUp] public void SetUp() => Schedule(() => { @@ -43,20 +56,13 @@ namespace osu.Game.Rulesets.Catch.Tests CircleSize = 0, }; - var trailContainer = new Container(); - droppedObjectContainer = new DroppedObjectContainer(); - catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty); + if (catcher != null) + Remove(catcher); - Child = new Container + Add(catcher = new TestCatcher(trailContainer, difficulty) { - Anchor = Anchor.Centre, - Children = new Drawable[] - { - trailContainer, - droppedObjectContainer, - catcher - } - }; + Anchor = Anchor.Centre + }); }); [Test] @@ -293,8 +299,8 @@ namespace osu.Game.Rulesets.Catch.Tests { public IEnumerable CaughtObjects => this.ChildrenOfType(); - public TestCatcher(Container trailsTarget, DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty) - : base(trailsTarget, droppedObjectTarget, difficulty) + public TestCatcher(Container trailsTarget, BeatmapDifficulty difficulty) + : base(trailsTarget, difficulty) { } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index dca75eee14..877e115e2f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -96,15 +96,12 @@ namespace osu.Game.Rulesets.Catch.Tests SetContents(_ => { - var droppedObjectContainer = new DroppedObjectContainer(); - return new CatchInputManager(catchRuleset) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - droppedObjectContainer, - new TestCatcherArea(droppedObjectContainer, beatmapDifficulty) + new TestCatcherArea(beatmapDifficulty) { Anchor = Anchor.Centre, Origin = Anchor.TopCentre, @@ -122,9 +119,13 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestCatcherArea : CatcherArea { - public TestCatcherArea(DroppedObjectContainer droppedObjectContainer, BeatmapDifficulty beatmapDifficulty) - : base(droppedObjectContainer, beatmapDifficulty) + [Cached] + private readonly DroppedObjectContainer droppedObjectContainer; + + public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) + : base(beatmapDifficulty) { + AddInternal(droppedObjectContainer = new DroppedObjectContainer()); } public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 4af528ef22..7fa981d492 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -118,11 +118,10 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("create hyper-dashing catcher", () => { - Child = setupSkinHierarchy(catcherArea = new CatcherArea(new DroppedObjectContainer()) + Child = setupSkinHierarchy(catcherArea = new TestCatcherArea { Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(4f), + Origin = Anchor.Centre }, skin); }); @@ -206,5 +205,18 @@ namespace osu.Game.Rulesets.Catch.Tests { } } + + private class TestCatcherArea : CatcherArea + { + [Cached] + private readonly DroppedObjectContainer droppedObjectContainer; + + public TestCatcherArea() + { + Scale = new Vector2(4f); + + AddInternal(droppedObjectContainer = new DroppedObjectContainer()); + } + } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 21501398fc..05cd29dff5 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -26,6 +26,9 @@ namespace osu.Game.Rulesets.Catch.UI /// public const float CENTER_X = WIDTH / 2; + [Cached] + private readonly DroppedObjectContainer droppedObjectContainer; + internal readonly CatcherArea CatcherArea; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => @@ -34,9 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI public CatchPlayfield(BeatmapDifficulty difficulty) { - var droppedObjectContainer = new DroppedObjectContainer(); - - CatcherArea = new CatcherArea(droppedObjectContainer, difficulty) + CatcherArea = new CatcherArea(difficulty) { Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, @@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.UI InternalChildren = new[] { - droppedObjectContainer, + droppedObjectContainer = new DroppedObjectContainer(), CatcherArea.MovableCatcher.CreateProxiedContent(), HitObjectContainer.CreateProxy(), // This ordering (`CatcherArea` before `HitObjectContainer`) is important to diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 6b42e3ddd3..dcab9459ee 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -79,7 +79,8 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Contains objects dropped from the plate. /// - private readonly DroppedObjectContainer droppedObjectTarget; + [Resolved] + private DroppedObjectContainer droppedObjectTarget { get; set; } public CatcherAnimationState CurrentState { @@ -134,10 +135,9 @@ namespace osu.Game.Rulesets.Catch.UI private readonly DrawablePool caughtBananaPool; private readonly DrawablePool caughtDropletPool; - public Catcher([NotNull] Container trailsTarget, [NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null) + public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) { this.trailsTarget = trailsTarget; - this.droppedObjectTarget = droppedObjectTarget; Origin = Anchor.TopCentre; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 951a10fc01..fea314df8d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.UI /// private int currentDirection; - public CatcherArea(DroppedObjectContainer droppedObjectContainer, BeatmapDifficulty difficulty = null) + public CatcherArea(BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); Children = new Drawable[] @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.UI Margin = new MarginPadding { Bottom = 350f }, X = CatchPlayfield.CENTER_X }, - MovableCatcher = new Catcher(this, droppedObjectContainer, difficulty) { X = CatchPlayfield.CENTER_X }, + MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X }, }; } From c0c1b8d62014bb0c23dad2e3b924051af5467303 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 24 Jun 2021 16:12:43 +0900 Subject: [PATCH 589/670] Fix catcher hyper-dash afterimage is not always displayed --- osu.Game.Rulesets.Catch/UI/CatcherTrail.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs index 80522ab36b..c961d98dc5 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs @@ -37,6 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI protected override void FreeAfterUse() { ClearTransforms(); + Alpha = 1; base.FreeAfterUse(); } } From 6922de12c692c7ae6835579d868b1a92100487f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Jun 2021 17:17:12 +0900 Subject: [PATCH 590/670] Add extra null safety in dispose call --- osu.Game/OsuGameBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7cc34114bf..d0be03f8c1 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -499,7 +499,7 @@ namespace osu.Game contextFactory.FlushConnections(); - if (Host != null) + if (Host?.UpdateThread != null) Host.UpdateThread.ThreadPausing -= onUpdateThreadPausing; } } From 3fcda83713111f24fc1436adfdd588d2aa6cea3a Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 22:00:19 +0800 Subject: [PATCH 591/670] Rename `VectorHelper` to `VectorUtils` --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 2 +- osu.Game.Rulesets.Osu/Utils/{VectorHelper.cs => VectorUtils.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Rulesets.Osu/Utils/{VectorHelper.cs => VectorUtils.cs} (99%) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index bcd5274e02..7c7e88cbba 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Mods distanceToPrev * (float)Math.Sin(current.AngleRad) ); - posRelativeToPrev = VectorHelper.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev); + posRelativeToPrev = VectorUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev); current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); diff --git a/osu.Game.Rulesets.Osu/Utils/VectorHelper.cs b/osu.Game.Rulesets.Osu/Utils/VectorUtils.cs similarity index 99% rename from osu.Game.Rulesets.Osu/Utils/VectorHelper.cs rename to osu.Game.Rulesets.Osu/Utils/VectorUtils.cs index e3fe3249e8..a829c89636 100644 --- a/osu.Game.Rulesets.Osu/Utils/VectorHelper.cs +++ b/osu.Game.Rulesets.Osu/Utils/VectorUtils.cs @@ -7,7 +7,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Utils { - public static class VectorHelper + public static class VectorUtils { // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle. // The closer the hit objects draw to the border, the sharper the turn From 4fe30ca9235d1fa1937879b2896cdddb7a3c21c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Jun 2021 18:31:13 +0200 Subject: [PATCH 592/670] Remove Traditional Chinese (HK) language option Due to no actual existing translations it was falling back to Simplified Chinese instead, making the option actively wrong/confusing. --- osu.Game/Localisation/Language.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index 3c66f31c58..dc1fac47a8 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.cs @@ -103,8 +103,11 @@ namespace osu.Game.Localisation [Description(@"简体中文")] zh, - [Description(@"繁體中文(香港)")] - zh_hk, + // Traditional Chinese (Hong Kong) is listed in web sources but has no associated localisations, + // and was wrongly falling back to Simplified Chinese. + // Can be revisited if localisations ever arrive. + // [Description(@"繁體中文(香港)")] + // zh_hk, [Description(@"繁體中文(台灣)")] zh_tw From 62566f2a4a0caf84a211e6cf7469f04abfa5900f Mon Sep 17 00:00:00 2001 From: aitani9 <55509723+aitani9@users.noreply.github.com> Date: Thu, 24 Jun 2021 14:29:47 -0700 Subject: [PATCH 593/670] Remove "Score Multiplier" text --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index e31e307d4d..85dcade986 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -330,22 +330,12 @@ namespace osu.Game.Overlays.Mods Spacing = new Vector2(footer_button_spacing / 2, 0), Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Children = new Drawable[] + Child = MultiplierLabel = new OsuSpriteText { - new OsuSpriteText - { - Text = @"Score Multiplier:", - Font = OsuFont.GetFont(size: 30), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - }, - MultiplierLabel = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Width = 70, // make width fixed so reflow doesn't occur when multiplier number changes. - }, + Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Width = 70, // make width fixed so reflow doesn't occur when multiplier number changes. }, }, } From 34ace2553e5b8db36be5dd0447bac569eeb2d843 Mon Sep 17 00:00:00 2001 From: aitani9 <55509723+aitani9@users.noreply.github.com> Date: Thu, 24 Jun 2021 14:32:00 -0700 Subject: [PATCH 594/670] Revert "Refactor the menu's max height to be a property" This reverts commit 9cb9ef5c --- osu.Game/Overlays/Settings/SettingsEnumDropdown.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index fa9973fb08..9987a0c607 100644 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs @@ -14,15 +14,13 @@ namespace osu.Game.Overlays.Settings protected new class DropdownControl : OsuEnumDropdown { - protected virtual int MenuMaxHeight => 200; - public DropdownControl() { Margin = new MarginPadding { Top = 5 }; RelativeSizeAxes = Axes.X; } - protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = MenuMaxHeight); + protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200); } } } From 93edb25acefe5004a7fd4e032d8a7696ca02bbdd Mon Sep 17 00:00:00 2001 From: aitani9 <55509723+aitani9@users.noreply.github.com> Date: Thu, 24 Jun 2021 14:32:43 -0700 Subject: [PATCH 595/670] Override `CreateMenu` instead of using a property --- osu.Game/Configuration/SettingSourceAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index cee5883d9a..55636495df 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -192,8 +192,8 @@ namespace osu.Game.Configuration private class ModDropdownControl : DropdownControl { - // Set low enough to workaround nested scroll issues (see https://github.com/ppy/osu-framework/issues/4536). - protected override int MenuMaxHeight => 100; + // Set menu's max height low enough to workaround nested scroll issues (see https://github.com/ppy/osu-framework/issues/4536). + protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 100); } } } From 26086ca1efc5107ab3c125a24a146db17940e1bb Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 25 Jun 2021 09:43:14 +0800 Subject: [PATCH 596/670] Rename `VectorUtils` to `OsuHitObjectGenerationUtils` --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 2 +- .../Utils/{VectorUtils.cs => OsuHitObjectGenerationUtils.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Rulesets.Osu/Utils/{VectorUtils.cs => OsuHitObjectGenerationUtils.cs} (98%) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 7c7e88cbba..d1212096bf 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Mods distanceToPrev * (float)Math.Sin(current.AngleRad) ); - posRelativeToPrev = VectorUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev); + posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev); current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); diff --git a/osu.Game.Rulesets.Osu/Utils/VectorUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs similarity index 98% rename from osu.Game.Rulesets.Osu/Utils/VectorUtils.cs rename to osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index a829c89636..2a60124757 100644 --- a/osu.Game.Rulesets.Osu/Utils/VectorUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -7,7 +7,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Utils { - public static class VectorUtils + public static class OsuHitObjectGenerationUtils { // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle. // The closer the hit objects draw to the border, the sharper the turn From ec8810cc2b96e92108c5fd365366c0fe8e9d97be Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 25 Jun 2021 09:44:23 +0800 Subject: [PATCH 597/670] Use `MathF` instead of `(float)Math` --- .../Utils/OsuHitObjectGenerationUtils.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index 2a60124757..06b964a647 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -84,20 +84,20 @@ namespace osu.Game.Rulesets.Osu.Utils /// The rotated vector. public static Vector2 RotateVectorTowardsVector(Vector2 initial, Vector2 destination, float rotationRatio) { - var initialAngleRad = Math.Atan2(initial.Y, initial.X); - var destAngleRad = Math.Atan2(destination.Y, destination.X); + var initialAngleRad = MathF.Atan2(initial.Y, initial.X); + var destAngleRad = MathF.Atan2(destination.Y, destination.X); var diff = destAngleRad - initialAngleRad; - while (diff < -Math.PI) diff += 2 * Math.PI; + while (diff < -MathF.PI) diff += 2 * MathF.PI; - while (diff > Math.PI) diff -= 2 * Math.PI; + while (diff > MathF.PI) diff -= 2 * MathF.PI; var finalAngleRad = initialAngleRad + rotationRatio * diff; return new Vector2( - initial.Length * (float)Math.Cos(finalAngleRad), - initial.Length * (float)Math.Sin(finalAngleRad) + initial.Length * MathF.Cos(finalAngleRad), + initial.Length * MathF.Sin(finalAngleRad) ); } } From 31c786e1c306e1b0a5161e644b2d12009cc91143 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 25 Jun 2021 09:51:34 +0800 Subject: [PATCH 598/670] Use `SettingsNumberBox.NumberBox` instead of `OsuNumberBox` --- osu.Game/Rulesets/Mods/SeedSettingsControl.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs index 5c57717d93..1280197532 100644 --- a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs @@ -5,7 +5,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods @@ -35,7 +34,7 @@ namespace osu.Game.Rulesets.Mods } } - private readonly OsuNumberBox seedNumberBox; + private readonly SettingsNumberBox.NumberBox seedNumberBox; public SeedControl() { @@ -61,7 +60,7 @@ namespace osu.Game.Rulesets.Mods { new Drawable[] { - seedNumberBox = new OsuNumberBox + seedNumberBox = new SettingsNumberBox.NumberBox { RelativeSizeAxes = Axes.X, CommitOnFocusLost = true From 57ae87956a54e7ac3365abde44338a04f5d33a45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Jun 2021 15:27:40 +0900 Subject: [PATCH 599/670] Update execution state change blocking logic in line with framework changes --- osu.Game/OsuGameBase.cs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d0be03f8c1..dec738e5b3 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -24,6 +24,7 @@ using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Logging; +using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Database; using osu.Game.Input; @@ -156,6 +157,8 @@ namespace osu.Game private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(GLOBAL_TRACK_VOLUME_ADJUST); + private IBindable updateThreadState; + public OsuGameBase() { UseDevelopmentServer = DebugUtils.IsDebugBuild; @@ -183,7 +186,8 @@ namespace osu.Game dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); - Host.UpdateThread.ThreadPausing += onUpdateThreadPausing; + updateThreadState = Host.UpdateThread.State.GetBoundCopy(); + updateThreadState.BindValueChanged(updateThreadStateChanged); AddInternal(realmFactory); @@ -359,10 +363,21 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); } - private void onUpdateThreadPausing() + private IDisposable blocking; + + private void updateThreadStateChanged(ValueChangedEvent state) { - var blocking = realmFactory.BlockAllOperations(); - Schedule(() => blocking.Dispose()); + switch (state.NewValue) + { + case GameThreadState.Running: + blocking.Dispose(); + blocking = null; + break; + + case GameThreadState.Paused: + blocking = realmFactory.BlockAllOperations(); + break; + } } protected override void LoadComplete() @@ -498,9 +513,6 @@ namespace osu.Game LocalConfig?.Dispose(); contextFactory.FlushConnections(); - - if (Host?.UpdateThread != null) - Host.UpdateThread.ThreadPausing -= onUpdateThreadPausing; } } } From 8241fee4a8aab8853d11853ca0757e08e13a3664 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 25 Jun 2021 09:22:34 +0300 Subject: [PATCH 600/670] Add failing test case --- .../TestSceneRulesetSkinProvidingContainer.cs | 100 ++++++++++++++++++ .../Testing/TestSceneRulesetDependencies.cs | 2 +- 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs new file mode 100644 index 0000000000..dd78f28351 --- /dev/null +++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs @@ -0,0 +1,100 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. + // See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using JetBrains.Annotations; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Skinning; +using osu.Game.Tests.Testing; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Rulesets +{ + public class TestSceneRulesetSkinProvidingContainer : OsuTestScene + { + [Resolved] + private SkinManager skins { get; set; } + + private SkinRequester requester; + + protected override Ruleset CreateRuleset() => new TestRuleset(); + + [Test] + public void TestEarlyAddedSkinRequester() + { + ISample transformerSampleOnBdl = null; + + // need a legacy skin to plug the TestRuleset's legacy transformer, which is required for testing this. + AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info); + + AddStep("setup provider", () => + { + var rulesetSkinProvider = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin); + + rulesetSkinProvider.Add(requester = new SkinRequester()); + + requester.OnBdl += () => transformerSampleOnBdl = requester.GetSample(new SampleInfo(TestLegacySkinTransformer.VIRTUAL_SAMPLE_NAME)); + + Child = rulesetSkinProvider; + }); + + AddAssert("requester got correct initial sample", () => transformerSampleOnBdl != null); + } + + private class SkinRequester : Drawable, ISkin + { + private ISkinSource skin; + + public event Action OnBdl; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + this.skin = skin; + + OnBdl?.Invoke(); + } + + public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component); + + public Texture GetTexture(string componentName, WrapMode wrapModeS = default, WrapMode wrapModeT = default) => skin.GetTexture(componentName); + + public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo); + + public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup); + } + + private class TestRuleset : TestSceneRulesetDependencies.TestRuleset + { + public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TestLegacySkinTransformer(skin); + } + + private class TestLegacySkinTransformer : LegacySkinTransformer + { + public const string VIRTUAL_SAMPLE_NAME = "virtual-test-sample"; + + public TestLegacySkinTransformer([NotNull] ISkin skin) + : base(skin) + { + } + + public override ISample GetSample(ISampleInfo sampleInfo) + { + if (sampleInfo.LookupNames.Single() == VIRTUAL_SAMPLE_NAME) + return new SampleVirtual(); + + return base.GetSample(sampleInfo); + } + } + } +} diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs index 97087e31ab..fb50da32f3 100644 --- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs +++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Testing Dependencies.Get() != null); } - private class TestRuleset : Ruleset + public class TestRuleset : Ruleset { public override string Description => string.Empty; public override string ShortName => string.Empty; From f07008a0a284023e50f63e7de3c2392d2de0e626 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 25 Jun 2021 09:55:29 +0300 Subject: [PATCH 601/670] Fix `RulesetSkinProvidingContainer` potentially late in setting up skin sources --- .../Skinning/RulesetSkinProvidingContainer.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index c48aeca99a..abf5cb040a 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -42,27 +42,30 @@ namespace osu.Game.Skinning }; } - [Resolved] - private ISkinSource skinSource { get; set; } + private ISkinSource parentSource; - [BackgroundDependencyLoader] - private void load() + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - UpdateSkins(); - skinSource.SourceChanged += OnSourceChanged; + parentSource = parent.Get(); + + UpdateSkinSources(); + + parentSource.SourceChanged += OnSourceChanged; + + return base.CreateChildDependencies(parent); } protected override void OnSourceChanged() { - UpdateSkins(); + UpdateSkinSources(); base.OnSourceChanged(); } - protected virtual void UpdateSkins() + protected virtual void UpdateSkinSources() { SkinSources.Clear(); - foreach (var skin in skinSource.AllSources) + foreach (var skin in parentSource.AllSources) { switch (skin) { @@ -93,8 +96,8 @@ namespace osu.Game.Skinning { base.Dispose(isDisposing); - if (skinSource != null) - skinSource.SourceChanged -= OnSourceChanged; + if (parentSource != null) + parentSource.SourceChanged -= OnSourceChanged; } } } From 58839221771cf68b5d95fc5101befb3bdf70250f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Jun 2021 16:36:31 +0900 Subject: [PATCH 602/670] Remove mod multiplier completely --- .../TestSceneModSelectOverlay.cs | 22 +--------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 42 +------------------ .../OnlinePlay/FreeModSelectOverlay.cs | 1 - 3 files changed, 3 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 2885dbee00..df8ef92a05 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -10,7 +10,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; @@ -107,7 +106,6 @@ namespace osu.Game.Tests.Visual.UserInterface var conversionMods = osu.GetModsFor(ModType.Conversion); var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail); - var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden); var doubleTimeMod = harderMods.OfType().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); @@ -120,8 +118,6 @@ namespace osu.Game.Tests.Visual.UserInterface testMultiMod(doubleTimeMod); testIncompatibleMods(easy, hardRock); testDeselectAll(easierMods.Where(m => !(m is MultiMod))); - testMultiplierTextColour(noFailMod, () => modSelect.LowMultiplierColour); - testMultiplierTextColour(hiddenMod, () => modSelect.HighMultiplierColour); testUnimplementedMod(targetMod); } @@ -149,7 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface changeRuleset(0); - AddAssert("ensure mods still selected", () => modDisplay.Current.Value.Single(m => m is OsuModNoFail) != null); + AddAssert("ensure mods still selected", () => modDisplay.Current.Value.SingleOrDefault(m => m is OsuModNoFail) != null); changeRuleset(3); @@ -316,17 +312,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("check for no selection", () => !modSelect.SelectedMods.Value.Any()); } - private void testMultiplierTextColour(Mod mod, Func getCorrectColour) - { - checkLabelColor(() => Color4.White); - selectNext(mod); - AddWaitStep("wait for changing colour", 1); - checkLabelColor(getCorrectColour); - selectPrevious(mod); - AddWaitStep("wait for changing colour", 1); - checkLabelColor(() => Color4.White); - } - private void testModsWithSameBaseType(Mod modA, Mod modB) { selectNext(modA); @@ -348,7 +333,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert($"check {mod.Name} is selected", () => { var button = modSelect.GetModButton(mod); - return modSelect.SelectedMods.Value.Single(m => m.Name == mod.Name) != null && button.SelectedMod.GetType() == mod.GetType() && button.Selected; + return modSelect.SelectedMods.Value.SingleOrDefault(m => m.Name == mod.Name) != null && button.SelectedMod.GetType() == mod.GetType() && button.Selected; }); } @@ -370,8 +355,6 @@ namespace osu.Game.Tests.Visual.UserInterface }); } - private void checkLabelColor(Func getColour) => AddAssert("check label has expected colour", () => modSelect.MultiplierLabel.Colour.AverageColour == getColour()); - private void createDisplay(Func createOverlayFunc) { Children = new Drawable[] @@ -408,7 +391,6 @@ namespace osu.Game.Tests.Visual.UserInterface return section.ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())); } - public new OsuSpriteText MultiplierLabel => base.MultiplierLabel; public new TriangleButton DeselectAllButton => base.DeselectAllButton; public new Color4 LowMultiplierColour => base.LowMultiplierColour; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 85dcade986..e4aab978fc 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -37,9 +37,6 @@ namespace osu.Game.Overlays.Mods protected readonly TriangleButton CustomiseButton; protected readonly TriangleButton CloseButton; - protected readonly Drawable MultiplierSection; - protected readonly OsuSpriteText MultiplierLabel; - protected readonly FillFlowContainer FooterContainer; protected override bool BlockNonPositionalInput => false; @@ -324,20 +321,6 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, - MultiplierSection = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(footer_button_spacing / 2, 0), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Child = MultiplierLabel = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Width = 70, // make width fixed so reflow doesn't occur when multiplier number changes. - }, - }, } } }, @@ -351,11 +334,8 @@ namespace osu.Game.Overlays.Mods } [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, AudioManager audio, OsuGameBase osu) + private void load(AudioManager audio, OsuGameBase osu) { - LowMultiplierColour = colours.Red; - HighMultiplierColour = colours.Green; - availableMods = osu.AvailableMods.GetBoundCopy(); sampleOn = audio.Samples.Get(@"UI/check-on"); @@ -485,26 +465,6 @@ namespace osu.Game.Overlays.Mods foreach (var section in ModSectionsContainer.Children) section.UpdateSelectedButtons(selectedMods); - - updateMultiplier(); - } - - private void updateMultiplier() - { - var multiplier = 1.0; - - foreach (var mod in SelectedMods.Value) - { - multiplier *= mod.ScoreMultiplier; - } - - MultiplierLabel.Text = $"{multiplier:N2}x"; - if (multiplier > 1.0) - MultiplierLabel.FadeColour(HighMultiplierColour, 200); - else if (multiplier < 1.0) - MultiplierLabel.FadeColour(LowMultiplierColour, 200); - else - MultiplierLabel.FadeColour(Color4.White, 200); } private void modButtonPressed(Mod selectedMod) diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 5e2e9fd087..d5abaaab4e 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -32,7 +32,6 @@ namespace osu.Game.Screens.OnlinePlay { IsValidMod = m => true; - MultiplierSection.Alpha = 0; DeselectAllButton.Alpha = 0; Drawable selectAllButton; From 06e357647abce9f383d0dee2c66bfc85440a41e7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 25 Jun 2021 10:40:07 +0300 Subject: [PATCH 603/670] OnBdl -> OnLoadAsync --- .../Rulesets/TestSceneRulesetSkinProvidingContainer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs index dd78f28351..3072c466e0 100644 --- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs +++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Rulesets [Test] public void TestEarlyAddedSkinRequester() { - ISample transformerSampleOnBdl = null; + ISample transformerSampleOnLoad = null; // need a legacy skin to plug the TestRuleset's legacy transformer, which is required for testing this. AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info); @@ -43,26 +43,26 @@ namespace osu.Game.Tests.Rulesets rulesetSkinProvider.Add(requester = new SkinRequester()); - requester.OnBdl += () => transformerSampleOnBdl = requester.GetSample(new SampleInfo(TestLegacySkinTransformer.VIRTUAL_SAMPLE_NAME)); + requester.OnLoadAsync += () => transformerSampleOnLoad = requester.GetSample(new SampleInfo(TestLegacySkinTransformer.VIRTUAL_SAMPLE_NAME)); Child = rulesetSkinProvider; }); - AddAssert("requester got correct initial sample", () => transformerSampleOnBdl != null); + AddAssert("requester got correct initial sample", () => transformerSampleOnLoad != null); } private class SkinRequester : Drawable, ISkin { private ISkinSource skin; - public event Action OnBdl; + public event Action OnLoadAsync; [BackgroundDependencyLoader] private void load(ISkinSource skin) { this.skin = skin; - OnBdl?.Invoke(); + OnLoadAsync?.Invoke(); } public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component); From 8d7705dc923b04ea037803260fc40fbc02f55933 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 25 Jun 2021 10:55:23 +0300 Subject: [PATCH 604/670] Test using a simple `GetTexture` lookup instead Presumes that `RulesetSkinProvidingContainer` doesn't allow falling back to parent skins, whatsoever. --- .../TestSceneRulesetSkinProvidingContainer.cs | 37 +++---------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs index 3072c466e0..50b75ea0c5 100644 --- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs +++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; -using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -12,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; -using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Tests.Testing; @@ -27,15 +24,14 @@ namespace osu.Game.Tests.Rulesets private SkinRequester requester; - protected override Ruleset CreateRuleset() => new TestRuleset(); + protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset(); [Test] public void TestEarlyAddedSkinRequester() { - ISample transformerSampleOnLoad = null; + Texture textureOnLoad = null; - // need a legacy skin to plug the TestRuleset's legacy transformer, which is required for testing this. - AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info); + AddStep("set skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info); AddStep("setup provider", () => { @@ -43,12 +39,12 @@ namespace osu.Game.Tests.Rulesets rulesetSkinProvider.Add(requester = new SkinRequester()); - requester.OnLoadAsync += () => transformerSampleOnLoad = requester.GetSample(new SampleInfo(TestLegacySkinTransformer.VIRTUAL_SAMPLE_NAME)); + requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("hitcircle"); Child = rulesetSkinProvider; }); - AddAssert("requester got correct initial sample", () => transformerSampleOnLoad != null); + AddAssert("requester got correct initial texture", () => textureOnLoad != null); } private class SkinRequester : Drawable, ISkin @@ -73,28 +69,5 @@ namespace osu.Game.Tests.Rulesets public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup); } - - private class TestRuleset : TestSceneRulesetDependencies.TestRuleset - { - public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TestLegacySkinTransformer(skin); - } - - private class TestLegacySkinTransformer : LegacySkinTransformer - { - public const string VIRTUAL_SAMPLE_NAME = "virtual-test-sample"; - - public TestLegacySkinTransformer([NotNull] ISkin skin) - : base(skin) - { - } - - public override ISample GetSample(ISampleInfo sampleInfo) - { - if (sampleInfo.LookupNames.Single() == VIRTUAL_SAMPLE_NAME) - return new SampleVirtual(); - - return base.GetSample(sampleInfo); - } - } } } From 13ed52a990cc4f1b53b3a1f1b37694859b5427cf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 25 Jun 2021 11:16:26 +0300 Subject: [PATCH 605/670] Fix weird license misindent No idea how the hell that happened... R# silent about it, of course. --- .../Rulesets/TestSceneRulesetSkinProvidingContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs index 50b75ea0c5..b6800e40e4 100644 --- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs +++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs @@ -1,5 +1,5 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. - // See the LICENCE file in the repository root for full licence text. +// See the LICENCE file in the repository root for full licence text. using System; using NUnit.Framework; From ff5e590d323ce868cce4eeaed71280645e6ae729 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 25 Jun 2021 12:00:46 +0300 Subject: [PATCH 606/670] Add local source for testing --- .../TestSceneRulesetSkinProvidingContainer.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs index b6800e40e4..b058cc3694 100644 --- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs +++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs @@ -19,11 +19,11 @@ namespace osu.Game.Tests.Rulesets { public class TestSceneRulesetSkinProvidingContainer : OsuTestScene { - [Resolved] - private SkinManager skins { get; set; } - private SkinRequester requester; + [Cached(typeof(ISkin))] + private readonly TestSkinProvider testSkin = new TestSkinProvider(); + protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset(); [Test] @@ -31,15 +31,13 @@ namespace osu.Game.Tests.Rulesets { Texture textureOnLoad = null; - AddStep("set skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info); - AddStep("setup provider", () => { var rulesetSkinProvider = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin); rulesetSkinProvider.Add(requester = new SkinRequester()); - requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("hitcircle"); + requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture(TestSkinProvider.TEXTURE_NAME); Child = rulesetSkinProvider; }); @@ -69,5 +67,18 @@ namespace osu.Game.Tests.Rulesets public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup); } + + private class TestSkinProvider : ISkin + { + public const string TEXTURE_NAME = "some-texture"; + + public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException(); + + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => componentName == TEXTURE_NAME ? Texture.WhitePixel : null; + + public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); + + public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); + } } } From 84c9ede966af09a0b4b1337ce7750d01b14d7c03 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 25 Jun 2021 13:17:11 +0300 Subject: [PATCH 607/670] Fix incorrect pushed changes This should've been in the original commit, but for some reason got deleted out. --- .../TestSceneRulesetSkinProvidingContainer.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs index b058cc3694..0dde0a8194 100644 --- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs +++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -21,11 +22,15 @@ namespace osu.Game.Tests.Rulesets { private SkinRequester requester; - [Cached(typeof(ISkin))] - private readonly TestSkinProvider testSkin = new TestSkinProvider(); - protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset(); + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(new TestSkinProvider()); + return dependencies; + } + [Test] public void TestEarlyAddedSkinRequester() { @@ -68,7 +73,7 @@ namespace osu.Game.Tests.Rulesets public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup); } - private class TestSkinProvider : ISkin + private class TestSkinProvider : ISkinSource { public const string TEXTURE_NAME = "some-texture"; @@ -79,6 +84,16 @@ namespace osu.Game.Tests.Rulesets public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); + + public event Action SourceChanged + { + add { } + remove { } + } + + public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null; + + public IEnumerable AllSources => new[] { this }; } } } From 6bc71590c539ef0dba6993aab0a5a48a95dfea7e Mon Sep 17 00:00:00 2001 From: aitani9 <55509723+aitani9@users.noreply.github.com> Date: Fri, 25 Jun 2021 09:21:26 -0700 Subject: [PATCH 608/670] Disable logo click sound when exiting --- osu.Game/Screens/Menu/ButtonSystem.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index da0edd07db..38290a6530 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -261,7 +261,7 @@ namespace osu.Game.Screens.Menu switch (state) { default: - return true; + return false; case ButtonSystemState.Initial: State = ButtonSystemState.TopLevel; @@ -274,10 +274,6 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Play: buttonsPlay.First().Click(); return false; - - // no sound should be played if the logo is clicked on while transitioning to song select - case ButtonSystemState.EnteringMode: - return false; } } From 50c27d26357f2646548c126c7201398a23ee489e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Jun 2021 19:10:04 +0200 Subject: [PATCH 609/670] Update usages of `IHasTooltip` in line with framework localisation changes --- osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs | 3 ++- .../Sliders/Components/PathControlPointPiece.cs | 3 ++- osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs | 3 ++- osu.Game/Configuration/SettingSourceAttribute.cs | 2 +- osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs | 3 ++- osu.Game/Graphics/Containers/OsuClickableContainer.cs | 3 ++- osu.Game/Graphics/Cursor/OsuTooltipContainer.cs | 3 ++- osu.Game/Graphics/UserInterface/ExternalLinkButton.cs | 3 ++- osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 3 ++- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 3 ++- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 5 +++-- osu.Game/Overlays/BeatmapSet/BasicStats.cs | 2 +- osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs | 3 ++- .../Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs | 3 ++- osu.Game/Overlays/Comments/DrawableComment.cs | 5 +++-- osu.Game/Overlays/Mods/ModButton.cs | 3 ++- osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs | 3 ++- .../Overlays/Profile/Header/Components/DrawableBadge.cs | 3 ++- .../Profile/Header/Components/ExpandDetailsButton.cs | 3 ++- .../Overlays/Profile/Header/Components/FollowersButton.cs | 3 ++- osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs | 3 ++- .../Profile/Header/Components/LevelProgressBar.cs | 3 ++- .../Profile/Header/Components/MappingSubscribersButton.cs | 3 ++- .../Profile/Header/Components/MessageUserButton.cs | 3 ++- .../Profile/Header/Components/OverlinedTotalPlayTime.cs | 3 ++- .../Overlays/Profile/Header/Components/SupporterIcon.cs | 3 ++- .../Sections/Historical/DrawableMostPlayedBeatmap.cs | 2 +- osu.Game/Overlays/RestoreDefaultValueButton.cs | 3 ++- .../Overlays/Settings/Sections/Audio/OffsetSettings.cs | 3 ++- .../Overlays/Settings/Sections/Graphics/LayoutSettings.cs | 2 +- .../Overlays/Settings/Sections/Input/MouseSettings.cs | 3 ++- osu.Game/Overlays/Settings/Sections/SizeSlider.cs | 3 ++- .../Settings/Sections/UserInterface/GeneralSettings.cs | 3 ++- .../Settings/Sections/UserInterface/SongSelectSettings.cs | 5 +++-- osu.Game/Overlays/Settings/SettingsButton.cs | 8 +++++--- osu.Game/Overlays/Settings/SettingsItem.cs | 4 ++-- osu.Game/Rulesets/UI/ModIcon.cs | 3 ++- .../Screens/Edit/Compose/Components/SelectionBoxButton.cs | 3 ++- .../Screens/OnlinePlay/Components/DrawableGameType.cs | 3 ++- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- osu.Game/Users/Drawables/ClickableAvatar.cs | 7 ++++--- osu.Game/Users/Drawables/DrawableFlag.cs | 3 ++- 42 files changed, 87 insertions(+), 50 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 1c89d9cd00..f89750a96e 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mania.Configuration; @@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Mania private class TimeSlider : OsuSliderBar { - public override string TooltipText => Current.Value.ToString("N0") + "ms"; + public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms"; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 48e4db11ca..5b476526c9 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; @@ -283,6 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } - public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty; + public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty; } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs index c5374d50ab..096bccae9e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; @@ -59,7 +60,7 @@ namespace osu.Game.Tests.Visual.UserInterface private class Icon : Container, IHasTooltip { - public string TooltipText { get; } + public LocalisableString TooltipText { get; } public SpriteIcon SpriteIcon { get; } diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 55636495df..f373e59417 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -31,7 +31,7 @@ namespace osu.Game.Configuration { public LocalisableString Label { get; } - public string Description { get; } + public LocalisableString Description { get; } public int? OrderPosition { get; } diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs index 75c73af0ce..ce8a9c8f9f 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs @@ -4,12 +4,13 @@ using Markdig.Syntax.Inlines; using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; namespace osu.Game.Graphics.Containers.Markdown { public class OsuMarkdownImage : MarkdownImage, IHasTooltip { - public string TooltipText { get; } + public LocalisableString TooltipText { get; } public OsuMarkdownImage(LinkInline linkInline) : base(linkInline.Url) diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs index 60ded8952d..0bc3c876e1 100644 --- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs +++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.Containers @@ -24,7 +25,7 @@ namespace osu.Game.Graphics.Containers this.sampleSet = sampleSet; } - public virtual string TooltipText { get; set; } + public virtual LocalisableString TooltipText { get; set; } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index 57f39bb8c7..81dca99ddd 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.Cursor @@ -32,7 +33,7 @@ namespace osu.Game.Graphics.Cursor public override bool SetContent(object content) { - if (!(content is string contentString)) + if (!(content is LocalisableString contentString)) return false; if (contentString == text.Text) return true; diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs index 5a1eb53fe1..6ad88eaaba 100644 --- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs +++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Platform; using osuTK; using osuTK.Graphics; @@ -58,6 +59,6 @@ namespace osu.Game.Graphics.UserInterface return true; } - public string TooltipText => "view in browser"; + public LocalisableString TooltipText => "view in browser"; } } diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index ac6f5ceb1b..8e82f4a7c1 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Platform; namespace osu.Game.Graphics.UserInterface @@ -105,7 +106,7 @@ namespace osu.Game.Graphics.UserInterface private class CapsWarning : SpriteIcon, IHasTooltip { - public string TooltipText => @"caps lock is active"; + public LocalisableString TooltipText => "caps lock is active"; public CapsWarning() { diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index f58962f8e1..ae16169123 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -34,7 +35,7 @@ namespace osu.Game.Graphics.UserInterface private readonly Box rightBox; private readonly Container nubContainer; - public virtual string TooltipText { get; private set; } + public virtual LocalisableString TooltipText { get; private set; } /// /// Whether to format the tooltip as a percentage or the actual value. diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 795540b65d..e35d3d6461 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -295,7 +296,7 @@ namespace osu.Game.Online.Leaderboards public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos); - public string TooltipText { get; } + public LocalisableString TooltipText { get; } public ScoreComponentLabel(LeaderboardScoreStatistic statistic) { @@ -365,7 +366,7 @@ namespace osu.Game.Online.Leaderboards }; } - public string TooltipText { get; } + public LocalisableString TooltipText { get; } } public class LeaderboardScoreStatistic diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index cf74c0d4d3..b81c60a5b9 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.BeatmapSet { private readonly OsuSpriteText value; - public string TooltipText { get; } + public LocalisableString TooltipText { get; } public LocalisableString Value { diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs index 7ad6906cea..bb87e7151b 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -28,7 +29,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly IBindable localUser = new Bindable(); - public string TooltipText + public LocalisableString TooltipText { get { diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs index 6d27342049..cef623e59b 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -26,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly bool noVideo; - public string TooltipText => button.Enabled.Value ? "download this beatmap" : "login to download"; + public LocalisableString TooltipText => button.Enabled.Value ? "download this beatmap" : "login to download"; private readonly IBindable localUser = new Bindable(); diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 7c47ac655f..d94f8c4b8b 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -20,6 +20,7 @@ using System; using osu.Framework.Graphics.Shapes; using osu.Framework.Extensions.IEnumerableExtensions; using System.Collections.Specialized; +using osu.Framework.Localisation; using osu.Game.Overlays.Comments.Buttons; namespace osu.Game.Overlays.Comments @@ -395,7 +396,7 @@ namespace osu.Game.Overlays.Comments private class ParentUsername : FillFlowContainer, IHasTooltip { - public string TooltipText => getParentMessage(); + public LocalisableString TooltipText => getParentMessage(); private readonly Comment parentComment; @@ -427,7 +428,7 @@ namespace osu.Game.Overlays.Comments if (parentComment == null) return string.Empty; - return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? @"deleted" : string.Empty; + return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? "deleted" : string.Empty; } } } diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 70424101fd..d0bd24496a 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -14,6 +14,7 @@ using System; using System.Linq; using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -34,7 +35,7 @@ namespace osu.Game.Overlays.Mods /// public Action SelectionChanged; - public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty; + public LocalisableString TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty; private const Easing mod_switch_easing = Easing.InOutSine; private const double mod_switch_duration = 120; diff --git a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs index 87b9d89d4d..0ece96b56c 100644 --- a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs +++ b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics.UserInterface; using osu.Framework.Allocation; using osuTK.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; namespace osu.Game.Overlays { @@ -56,7 +57,7 @@ namespace osu.Game.Overlays [Resolved] private OverlayColourProvider colourProvider { get; set; } - public string TooltipText => $@"{Value} view"; + public LocalisableString TooltipText => $@"{Value} view"; private readonly SpriteIcon icon; diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs index 7eed4d3b6b..74f3ed846b 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; using osu.Game.Users; using osuTK; @@ -42,6 +43,6 @@ namespace osu.Game.Overlays.Profile.Header.Components InternalChild.FadeInFromZero(200); } - public string TooltipText => badge.Description; + public LocalisableString TooltipText => badge.Description; } } diff --git a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs index 29e13e4f51..527c70685f 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osuTK; namespace osu.Game.Overlays.Profile.Header.Components @@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly BindableBool DetailsVisible = new BindableBool(); - public override string TooltipText => DetailsVisible.Value ? "collapse" : "expand"; + public override LocalisableString TooltipText => DetailsVisible.Value ? "collapse" : "expand"; private SpriteIcon icon; diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index bd8aa7b3bd..db94762efd 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public override string TooltipText => "followers"; + public override LocalisableString TooltipText => "followers"; protected override IconUsage Icon => FontAwesome.Solid.User; diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs index 29471375b5..a0b8ef0f11 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Users; @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public string TooltipText { get; } + public LocalisableString TooltipText { get; } private OsuSpriteText levelText; diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs index c97df3bc4d..528b05a80a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public string TooltipText { get; } + public LocalisableString TooltipText { get; } private Bar levelProgressBar; private OsuSpriteText levelProgressText; diff --git a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs index b4d7c9a05c..ae3d024fbf 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public override string TooltipText => "mapping subscribers"; + public override LocalisableString TooltipText => "mapping subscribers"; protected override IconUsage Icon => FontAwesome.Solid.Bell; diff --git a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs index 228765ee1a..4c2cc768ce 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Users; @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public override string TooltipText => "send message"; + public override LocalisableString TooltipText => "send message"; [Resolved(CanBeNull = true)] private ChannelManager channelManager { get; set; } diff --git a/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs b/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs index be96840217..aa7cb8636a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs +++ b/osu.Game/Overlays/Profile/Header/Components/OverlinedTotalPlayTime.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components @@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public string TooltipText { get; set; } + public LocalisableString TooltipText { get; set; } private OverlinedInfoContainer info; diff --git a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs index d581e2750c..9a43997030 100644 --- a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs +++ b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Graphics; namespace osu.Game.Overlays.Profile.Header.Components @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private readonly FillFlowContainer iconContainer; private readonly CircularContainer content; - public string TooltipText => "osu!supporter"; + public LocalisableString TooltipText => "osu!supporter"; public int SupportLevel { diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index 6d6ff32aac..6f1869966a 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -143,7 +143,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private class PlayCountText : CompositeDrawable, IHasTooltip { - public string TooltipText => "times played"; + public LocalisableString TooltipText => "times played"; public PlayCountText(int playCount) { diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RestoreDefaultValueButton.cs index 213ad2ba68..fe36f6ba6d 100644 --- a/osu.Game/Overlays/RestoreDefaultValueButton.cs +++ b/osu.Game/Overlays/RestoreDefaultValueButton.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -76,7 +77,7 @@ namespace osu.Game.Overlays UpdateState(); } - public string TooltipText => "revert to default"; + public LocalisableString TooltipText => "revert to default"; protected override bool OnHover(HoverEvent e) { diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index c9a81b955b..1ae297f2a9 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -32,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio private class OffsetSlider : OsuSliderBar { - public override string TooltipText => Current.Value.ToString(@"0ms"); + public override LocalisableString TooltipText => Current.Value.ToString(@"0ms"); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 937bcc8abf..669753d2cb 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -233,7 +233,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private class UIScaleSlider : OsuSliderBar { - public override string TooltipText => base.TooltipText + "x"; + public override LocalisableString TooltipText => base.TooltipText + "x"; } private class ResolutionSettingsDropdown : SettingsDropdown diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index fb908a7669..e87572e2ca 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Input.Handlers.Mouse; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -116,7 +117,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private class SensitivitySlider : OsuSliderBar { - public override string TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x"; + public override LocalisableString TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x"; } } } diff --git a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs index 101d8f43f7..8aeb440be1 100644 --- a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs +++ b/osu.Game/Overlays/Settings/Sections/SizeSlider.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 osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections @@ -10,6 +11,6 @@ namespace osu.Game.Overlays.Settings.Sections /// internal class SizeSlider : OsuSliderBar { - public override string TooltipText => Current.Value.ToString(@"0.##x"); + public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x"); } } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 19adfc5dd9..a6eb008623 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -44,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface private class TimeSlider : OsuSliderBar { - public override string TooltipText => Current.Value.ToString("N0") + "ms"; + public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms"; } } } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index c73a783d37..2470c0a6c5 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -62,12 +63,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface private class MaximumStarsSlider : StarsSlider { - public override string TooltipText => Current.IsDefault ? "no limit" : base.TooltipText; + public override LocalisableString TooltipText => Current.IsDefault ? "no limit" : base.TooltipText; } private class StarsSlider : OsuSliderBar { - public override string TooltipText => Current.Value.ToString(@"0.## stars"); + public override LocalisableString TooltipText => Current.Value.ToString(@"0.## stars"); } } } diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index 088d69c031..87b1aa0e46 100644 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings @@ -17,14 +18,15 @@ namespace osu.Game.Overlays.Settings Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }; } - public string TooltipText { get; set; } + public LocalisableString TooltipText { get; set; } public override IEnumerable FilterTerms { get { - if (TooltipText != null) - return base.FilterTerms.Append(TooltipText); + if (TooltipText != default) + // TODO: this won't work as intended once the tooltip text is translated. + return base.FilterTerms.Append(TooltipText.ToString()); return base.FilterTerms; } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 807916e7f6..15a0a42d31 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings public bool ShowsDefaultIndicator = true; - public string TooltipText { get; set; } + public LocalisableString TooltipText { get; set; } [Resolved] private OsuColour colours { get; set; } @@ -142,4 +142,4 @@ namespace osu.Game.Overlays.Settings labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index cae5da3d16..725cfa9c26 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osuTK; using osu.Framework.Bindables; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.UI { @@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.UI private const float size = 80; - public virtual string TooltipText => showTooltip ? mod.IconTooltip : null; + public virtual LocalisableString TooltipText => showTooltip ? mod.IconTooltip : null; private Mod mod; private readonly bool showTooltip; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs index 3b1dae6c3d..3ac40fda0f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osuTK; using osuTK.Graphics; @@ -58,6 +59,6 @@ namespace osu.Game.Screens.Edit.Compose.Components icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint); } - public string TooltipText { get; } + public LocalisableString TooltipText { get; } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs index c4dc2a2b8f..ae1ca1b967 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Online.Rooms; @@ -16,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { private readonly GameType type; - public string TooltipText => type.Name; + public LocalisableString TooltipText => type.Name; public DrawableGameType(GameType type) { diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index e1cf0cef4e..4a35202df2 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -431,7 +431,7 @@ namespace osu.Game.Screens.Select public class InfoLabel : Container, IHasTooltip { - public string TooltipText { get; } + public LocalisableString TooltipText { get; } public InfoLabel(BeatmapStatistic statistic) { diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index c3bf740108..f73489ac61 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Containers; namespace osu.Game.Users.Drawables @@ -68,11 +69,11 @@ namespace osu.Game.Users.Drawables private class ClickableArea : OsuClickableContainer { - private string tooltip = default_tooltip_text; + private LocalisableString tooltip = default_tooltip_text; - public override string TooltipText + public override LocalisableString TooltipText { - get => Enabled.Value ? tooltip : null; + get => Enabled.Value ? tooltip : default; set => tooltip = value; } diff --git a/osu.Game/Users/Drawables/DrawableFlag.cs b/osu.Game/Users/Drawables/DrawableFlag.cs index 1d648e46b6..aea40a01ae 100644 --- a/osu.Game/Users/Drawables/DrawableFlag.cs +++ b/osu.Game/Users/Drawables/DrawableFlag.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; namespace osu.Game.Users.Drawables { @@ -13,7 +14,7 @@ namespace osu.Game.Users.Drawables { private readonly Country country; - public string TooltipText => country?.FullName; + public LocalisableString TooltipText => country?.FullName; public DrawableFlag(Country country) { From 3b822cd5cf45af3cd21bbfed9471b4f3b60a07c5 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 26 Jun 2021 11:19:14 +0800 Subject: [PATCH 610/670] Refactor `SeedSettingsControl` and related controls --- .../Overlays/Settings/OutlinedNumberBox.cs | 10 ++++ osu.Game/Overlays/Settings/OutlinedTextBox.cs | 49 +++++++++++++++++++ .../Overlays/Settings/SettingsNumberBox.cs | 8 +-- osu.Game/Overlays/Settings/SettingsTextBox.cs | 47 +----------------- osu.Game/Rulesets/Mods/SeedSettingsControl.cs | 33 +++---------- 5 files changed, 70 insertions(+), 77 deletions(-) create mode 100644 osu.Game/Overlays/Settings/OutlinedNumberBox.cs create mode 100644 osu.Game/Overlays/Settings/OutlinedTextBox.cs diff --git a/osu.Game/Overlays/Settings/OutlinedNumberBox.cs b/osu.Game/Overlays/Settings/OutlinedNumberBox.cs new file mode 100644 index 0000000000..6fcadc09e1 --- /dev/null +++ b/osu.Game/Overlays/Settings/OutlinedNumberBox.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays.Settings +{ + public class OutlinedNumberBox : OutlinedTextBox + { + protected override bool CanAddCharacter(char character) => char.IsNumber(character); + } +} diff --git a/osu.Game/Overlays/Settings/OutlinedTextBox.cs b/osu.Game/Overlays/Settings/OutlinedTextBox.cs new file mode 100644 index 0000000000..93eaf74b77 --- /dev/null +++ b/osu.Game/Overlays/Settings/OutlinedTextBox.cs @@ -0,0 +1,49 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Settings +{ + public class OutlinedTextBox : OsuTextBox + { + private const float border_thickness = 3; + + private Color4 borderColourFocused; + private Color4 borderColourUnfocused; + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + borderColourUnfocused = colour.Gray4.Opacity(0.5f); + borderColourFocused = BorderColour; + + updateBorder(); + } + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + updateBorder(); + } + + protected override void OnFocusLost(FocusLostEvent e) + { + base.OnFocusLost(e); + + updateBorder(); + } + + private void updateBorder() + { + BorderThickness = border_thickness; + BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused; + } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index ca9a8e9c08..d4d1fc8610 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -7,15 +7,11 @@ namespace osu.Game.Overlays.Settings { public class SettingsNumberBox : SettingsItem { - protected override Drawable CreateControl() => new NumberBox + protected override Drawable CreateControl() => new OutlinedNumberBox { Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, + CommitOnFocusLost = true }; - - public class NumberBox : SettingsTextBox.TextBox - { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); - } } } diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index 25424e85a1..d28dbf1068 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs @@ -1,60 +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 osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osuTK.Graphics; namespace osu.Game.Overlays.Settings { public class SettingsTextBox : SettingsItem { - protected override Drawable CreateControl() => new TextBox + protected override Drawable CreateControl() => new OutlinedTextBox { Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true, + CommitOnFocusLost = true }; - - public class TextBox : OsuTextBox - { - private const float border_thickness = 3; - - private Color4 borderColourFocused; - private Color4 borderColourUnfocused; - - [BackgroundDependencyLoader] - private void load(OsuColour colour) - { - borderColourUnfocused = colour.Gray4.Opacity(0.5f); - borderColourFocused = BorderColour; - - updateBorder(); - } - - protected override void OnFocus(FocusEvent e) - { - base.OnFocus(e); - - updateBorder(); - } - - protected override void OnFocusLost(FocusLostEvent e) - { - base.OnFocusLost(e); - - updateBorder(); - } - - private void updateBorder() - { - BorderThickness = border_thickness; - BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused; - } - } } } diff --git a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs index 1280197532..1eaf31874b 100644 --- a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mods } } - private readonly SettingsNumberBox.NumberBox seedNumberBox; + private readonly OutlinedNumberBox seedNumberBox; public SeedControl() { @@ -42,31 +42,11 @@ namespace osu.Game.Rulesets.Mods InternalChildren = new[] { - new GridContainer + seedNumberBox = new OutlinedNumberBox { + Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 2), - new Dimension(GridSizeMode.Relative, 0.25f) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - seedNumberBox = new SettingsNumberBox.NumberBox - { - RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true - } - } - } + CommitOnFocusLost = true } }; @@ -83,8 +63,9 @@ namespace osu.Game.Rulesets.Mods protected override void Update() { - if (current.Value == null) - seedNumberBox.Text = current.Current.Value.ToString(); + base.Update(); + if (Current.Value == null) + seedNumberBox.Current.Value = ""; } } } From 8e1bcc4d6bd751a5112451f3f9c0ed06a8937714 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 26 Jun 2021 21:02:31 +0700 Subject: [PATCH 611/670] add overall difficulty in filter criteria --- osu.Game/Screens/Select/FilterCriteria.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 208048380a..b9e912df8e 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -24,6 +24,7 @@ namespace osu.Game.Screens.Select public OptionalRange ApproachRate; public OptionalRange DrainRate; public OptionalRange CircleSize; + public OptionalRange OverallDifficulty; public OptionalRange Length; public OptionalRange BPM; public OptionalRange BeatDivisor; From 4df4afe533aed92ee7c8d7f22d14b1047007283b Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 26 Jun 2021 21:02:57 +0700 Subject: [PATCH 612/670] add test for overall difficulty filter query --- .../NonVisual/Filtering/FilterQueryParserTest.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 9bd262a569..a55bdd2df8 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -90,6 +90,20 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.Less(filterCriteria.DrainRate.Min, 6.1f); } + [Test] + public void TestApplyOverallDifficultyQueries() + { + const string query = "od>4 easy od<8"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("easy", filterCriteria.SearchText.Trim()); + Assert.AreEqual(1, filterCriteria.SearchTerms.Length); + Assert.Greater(filterCriteria.OverallDifficulty.Min, 4.0); + Assert.Less(filterCriteria.OverallDifficulty.Min, 4.1); + Assert.Greater(filterCriteria.OverallDifficulty.Max, 7.9); + Assert.Less(filterCriteria.OverallDifficulty.Max, 8.0); + } + [Test] public void TestApplyBPMQueries() { From 2b1d3c8e9c9ce4eb5e5790032d64da556a0f7c6f Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Sat, 26 Jun 2021 21:05:01 +0700 Subject: [PATCH 613/670] add od filter in search filter --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 1 + osu.Game/Screens/Select/FilterQueryParser.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 521b90202d..f95ddfee41 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -42,6 +42,7 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate); match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate); match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize); + match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(Beatmap.BaseDifficulty.OverallDifficulty); match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(Beatmap.Length); match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(Beatmap.BPM); diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index db2803d29a..72d10019b2 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -51,6 +51,9 @@ namespace osu.Game.Screens.Select case "cs": return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value); + case "od": + return TryUpdateCriteriaRange(ref criteria.OverallDifficulty, op, value); + case "bpm": return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2); From d8117fa73032af9b130c768fd1b1c0a694268580 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 19:20:34 +0200 Subject: [PATCH 614/670] Add muted objects check --- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 1 + .../Rulesets/Edit/Checks/CheckMutedObjects.cs | 133 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index d208c7fe07..462a87af85 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Edit // Audio new CheckAudioPresence(), new CheckAudioQuality(), + new CheckMutedObjects(), // Compose new CheckUnsnappedObjects(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs new file mode 100644 index 0000000000..cbe7c7fbab --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs @@ -0,0 +1,133 @@ +// 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.Utils; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckMutedObjects : ICheck + { + /// + /// Volume percentages lower than this are typically inaudible. + /// + private const int muted_threshold = 5; + + /// + /// Volume percentages lower than this can sometimes be inaudible depending on sample used and music volume. + /// + private const int low_volume_threshold = 20; + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Low volume hitobjects"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateMutedActive(this), + new IssueTemplateLowVolumeActive(this), + new IssueTemplateMutedPassive(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + foreach (var hitObject in context.Beatmap.HitObjects) + { + // Worth keeping in mind: The samples of an object always play at its end time. + // Objects like spinners have no sound at its start because of this, while hold notes have nested objects to accomplish this. + foreach (var nestedHitObject in hitObject.NestedHitObjects) + { + foreach (var issue in getVolumeIssues(hitObject, nestedHitObject)) + yield return issue; + } + + foreach (var issue in getVolumeIssues(hitObject)) + yield return issue; + } + } + + private IEnumerable getVolumeIssues(HitObject hitObject, HitObject sampledHitObject = null) + { + sampledHitObject ??= hitObject; + if (!sampledHitObject.Samples.Any()) + yield break; + + // Samples that allow themselves to be overridden by control points have a volume of 0. + int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume); + double samplePlayTime = sampledHitObject.GetEndTime(); + + bool head = Precision.AlmostEquals(samplePlayTime, hitObject.StartTime, 1f); + bool tail = Precision.AlmostEquals(samplePlayTime, hitObject.GetEndTime(), 1f); + bool repeat = false; + + if (hitObject is IHasRepeats hasRepeats && !head && !tail) + { + double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount(); + repeat = Precision.AlmostEquals((samplePlayTime - hitObject.StartTime) % spanDuration, 0f, 1f); + } + + // We only care about samples played on the edges of objects, not ones like spinnerspin or slidertick. + if (!head && !tail && !repeat) + yield break; + + string postfix = null; + if (hitObject is IHasDuration) + postfix = head ? "head" : tail ? "tail" : "repeat"; + + if (maxVolume <= muted_threshold) + { + if (head) + yield return new IssueTemplateMutedActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix); + else + yield return new IssueTemplateMutedPassive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix); + } + else if (maxVolume <= low_volume_threshold && head) + { + yield return new IssueTemplateLowVolumeActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix); + } + } + + public abstract class IssueTemplateMuted : IssueTemplate + { + protected IssueTemplateMuted(ICheck check, IssueType type, string unformattedMessage) + : base(check, type, unformattedMessage) + { + } + + public Issue Create(HitObject hitobject, double volume, double time, string postfix = "") + { + string objectName = hitobject.GetType().Name; + if (!string.IsNullOrEmpty(postfix)) + objectName += " " + postfix; + + return new Issue(hitobject, this, objectName, volume) { Time = time }; + } + } + + public class IssueTemplateMutedActive : IssueTemplateMuted + { + public IssueTemplateMutedActive(ICheck check) + : base(check, IssueType.Problem, "{0} has a volume of {1:0%}. Clickable objects must have clearly audible feedback.") + { + } + } + + public class IssueTemplateLowVolumeActive : IssueTemplateMuted + { + public IssueTemplateLowVolumeActive(ICheck check) + : base(check, IssueType.Warning, "{0} has a volume of {1:0%}, ensure this is audible.") + { + } + } + + public class IssueTemplateMutedPassive : IssueTemplateMuted + { + public IssueTemplateMutedPassive(ICheck check) + : base(check, IssueType.Negligible, "{0} has a volume of {1:0%}, ensure there is no distinct sound here in the song if inaudible.") + { + } + } + } +} From 4b436b774dcb3c35d145410fb56edc12e2c66ebb Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 19:20:46 +0200 Subject: [PATCH 615/670] Add few hitsounds check --- osu.Game/Rulesets/Edit/BeatmapVerifier.cs | 1 + .../Rulesets/Edit/Checks/CheckFewHitsounds.cs | 161 ++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 462a87af85..706eec226c 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Edit new CheckAudioPresence(), new CheckAudioQuality(), new CheckMutedObjects(), + new CheckFewHitsounds(), // Compose new CheckUnsnappedObjects(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs new file mode 100644 index 0000000000..07ca470e62 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs @@ -0,0 +1,161 @@ +// 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.Game.Audio; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckFewHitsounds : ICheck + { + /// + /// 2 measures (4/4) of 120 BPM, typically makes up a few patterns in the map. + /// This is almost always ok, but can still be useful for the mapper to make sure hitsounding coverage is good. + /// + private const int negligible_threshold_time = 4000; + + /// + /// 4 measures (4/4) of 120 BPM, typically makes up a large portion of a section in the song. + /// This is ok if the section is a quiet intro, for example. + /// + private const int warning_threshold_time = 8000; + + /// + /// 12 measures (4/4) of 120 BPM, typically makes up multiple sections in the song. + /// + private const int problem_threshold_time = 24000; + + // Should pass at least this many objects without hitsounds to be considered an issue (should work for Easy diffs too). + private const int warning_threshold_objects = 4; + private const int problem_threshold_objects = 16; + + private static readonly string[] hitsound_types = { HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_FINISH }; + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Few or no hitsounds"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateLongPeriodProblem(this), + new IssueTemplateLongPeriodWarning(this), + new IssueTemplateNoHitsounds(this) + }; + + private bool hasHitsounds; + private int objectsWithoutHitsounds; + private double lastHitsoundTime; + + public IEnumerable Run(BeatmapVerifierContext context) + { + if (!context.Beatmap.HitObjects.Any()) + yield break; + + hasHitsounds = false; + objectsWithoutHitsounds = 0; + lastHitsoundTime = context.Beatmap.HitObjects.First().StartTime; + + var hitObjectCount = context.Beatmap.HitObjects.Count; + + for (int i = 0; i < hitObjectCount; ++i) + { + var hitObject = context.Beatmap.HitObjects[i]; + + // Samples play on the end of objects. Some objects have nested objects to accomplish playing them elsewhere (e.g. slider head/repeat). + foreach (var nestedHitObject in hitObject.NestedHitObjects) + { + foreach (var issue in applyHitsoundUpdate(nestedHitObject)) + yield return issue; + } + + // This is used to perform an update at the end so that the period after the last hitsounded object can be an issue. + bool isLastObject = i == hitObjectCount - 1; + + foreach (var issue in applyHitsoundUpdate(hitObject, isLastObject)) + yield return issue; + } + + if (!hasHitsounds) + yield return new IssueTemplateNoHitsounds(this).Create(); + } + + private IEnumerable applyHitsoundUpdate(HitObject hitObject, bool isLastObject = false) + { + var time = hitObject.GetEndTime(); + + // Only generating issues on hitsounded or last objects ensures we get one issue per long period. + // If there are no hitsounds we let the "No hitsounds" template take precedence. + if (hasHitsound(hitObject) || isLastObject && hasHitsounds) + { + var timeWithoutHitsounds = time - lastHitsoundTime; + + if (timeWithoutHitsounds > problem_threshold_time && objectsWithoutHitsounds > problem_threshold_objects) + yield return new IssueTemplateLongPeriodProblem(this).Create(lastHitsoundTime, timeWithoutHitsounds); + else if (timeWithoutHitsounds > warning_threshold_time && objectsWithoutHitsounds > warning_threshold_objects) + yield return new IssueTemplateLongPeriodWarning(this).Create(lastHitsoundTime, timeWithoutHitsounds); + else if (timeWithoutHitsounds > negligible_threshold_time && objectsWithoutHitsounds > warning_threshold_objects) + yield return new IssueTemplateLongPeriodNegligible(this).Create(lastHitsoundTime, timeWithoutHitsounds); + } + + if (hasHitsound(hitObject)) + { + hasHitsounds = true; + objectsWithoutHitsounds = 0; + lastHitsoundTime = time; + } + else if (couldHaveHitsound(hitObject)) + ++objectsWithoutHitsounds; + } + + private bool hasHitsound(HitObject hitObject) => hitObject.Samples.Any(isHitsound); + private bool couldHaveHitsound(HitObject hitObject) => hitObject.Samples.Any(isHitnormal); + + private bool isHitsound(HitSampleInfo sample) => hitsound_types.Any(sample.Name.Contains); + private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL); + + public abstract class IssueTemplateLongPeriod : IssueTemplate + { + protected IssueTemplateLongPeriod(ICheck check, IssueType type) + : base(check, type, "Long period without hitsounds ({0:F1} seconds).") + { + } + + public Issue Create(double time, double duration) => new Issue(this, duration / 1000f) { Time = time }; + } + + public class IssueTemplateLongPeriodProblem : IssueTemplateLongPeriod + { + public IssueTemplateLongPeriodProblem(ICheck check) + : base(check, IssueType.Problem) + { + } + } + + public class IssueTemplateLongPeriodWarning : IssueTemplateLongPeriod + { + public IssueTemplateLongPeriodWarning(ICheck check) + : base(check, IssueType.Warning) + { + } + } + + public class IssueTemplateLongPeriodNegligible : IssueTemplateLongPeriod + { + public IssueTemplateLongPeriodNegligible(ICheck check) + : base(check, IssueType.Negligible) + { + } + } + + public class IssueTemplateNoHitsounds : IssueTemplate + { + public IssueTemplateNoHitsounds(ICheck check) + : base(check, IssueType.Problem, "There are no hitsounds.") + { + } + + public Issue Create() => new Issue(this); + } + } +} From 7b9569a1176d7a982ef6e9212574a9385c9d665b Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 19:21:01 +0200 Subject: [PATCH 616/670] Add muted object check tests --- .../Editing/Checks/CheckMutedObjectsTest.cs | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs new file mode 100644 index 0000000000..4bfe62a64d --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs @@ -0,0 +1,317 @@ +// 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 System.Threading; +using NUnit.Framework; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckMutedObjectsTest + { + private CheckMutedObjects check; + private ControlPointInfo cpi; + + private const int volume_regular = 50; + private const int volume_low = 15; + private const int volume_muted = 5; + + private sealed class MockNestableHitObject : HitObject, IHasDuration + { + private readonly IEnumerable toBeNested; + + public MockNestableHitObject(IEnumerable toBeNested, double startTime, double endTime) + { + this.toBeNested = toBeNested; + StartTime = startTime; + EndTime = endTime; + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + foreach (var hitObject in toBeNested) + AddNested(hitObject); + } + + public double EndTime { get; } + + public double Duration + { + get => EndTime - StartTime; + set => throw new System.NotImplementedException(); + } + } + + [SetUp] + public void Setup() + { + check = new CheckMutedObjects(); + + cpi = new ControlPointInfo(); + cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular }); + cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low }); + cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted }); + } + + [Test] + public void TestNormalControlPointVolume() + { + var hitcircle = new HitCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertOk(new List { hitcircle }); + } + + [Test] + public void TestLowControlPointVolume() + { + var hitcircle = new HitCircle + { + StartTime = 1000, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertLowVolume(new List { hitcircle }); + } + + [Test] + public void TestMutedControlPointVolume() + { + var hitcircle = new HitCircle + { + StartTime = 2000, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMuted(new List { hitcircle }); + } + + [Test] + public void TestNormalSampleVolume() + { + // The sample volume should take precedence over the control point volume. + var hitcircle = new HitCircle + { + StartTime = 2000, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertOk(new List { hitcircle }); + } + + [Test] + public void TestLowSampleVolume() + { + var hitcircle = new HitCircle + { + StartTime = 2000, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_low) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertLowVolume(new List { hitcircle }); + } + + [Test] + public void TestMutedSampleVolume() + { + var hitcircle = new HitCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } + }; + hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMuted(new List { hitcircle }); + } + + [Test] + public void TestNormalSampleVolumeSlider() + { + var sliderHead = new SliderHeadCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var sliderTick = new SliderTick + { + StartTime = 250, + Samples = new List { new HitSampleInfo("slidertick", volume: volume_muted) } // Should be fine. + }; + sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500) + { + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + slider.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertOk(new List { slider }); + } + + [Test] + public void TestMutedSampleVolumeSliderHead() + { + var sliderHead = new SliderHeadCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } + }; + sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var sliderTick = new SliderTick + { + StartTime = 250, + Samples = new List { new HitSampleInfo("slidertick") } + }; + sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500) + { + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail. + }; + slider.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMuted(new List { slider }); + } + + [Test] + public void TestMutedSampleVolumeSliderTail() + { + var sliderHead = new SliderHeadCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var sliderTick = new SliderTick + { + StartTime = 250, + Samples = new List { new HitSampleInfo("slidertick") } + }; + sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500) + { + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } // Applies to the tail. + }; + slider.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMutedPassive(new List { slider }); + } + + [Test] + public void TestMutedControlPointVolumeSliderHead() + { + var sliderHead = new SliderHeadCircle + { + StartTime = 2000, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var sliderTick = new SliderTick + { + StartTime = 2250, + Samples = new List { new HitSampleInfo("slidertick") } + }; + sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500) + { + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } + }; + slider.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMuted(new List { slider }); + } + + [Test] + public void TestMutedControlPointVolumeSliderTail() + { + var sliderHead = new SliderHeadCircle + { + StartTime = 0, + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var sliderTick = new SliderTick + { + StartTime = 250, + Samples = new List { new HitSampleInfo("slidertick") } + }; + sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); + + // Ends after the 5% control point. + var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500) + { + Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } + }; + slider.ApplyDefaults(cpi, new BeatmapDifficulty()); + + assertMutedPassive(new List { slider }); + } + + private void assertOk(List hitObjects) + { + Assert.That(check.Run(getContext(hitObjects)), Is.Empty); + } + + private void assertLowVolume(List hitObjects, int count = 1) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateLowVolumeActive)); + } + + private void assertMuted(List hitObjects, int count = 1) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedActive)); + } + + private void assertMutedPassive(List hitObjects) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Any(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedPassive)); + } + + private BeatmapVerifierContext getContext(List hitObjects) + { + var beatmap = new Beatmap + { + ControlPointInfo = cpi, + HitObjects = hitObjects + }; + + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} From a5abc664f30b25c249cb39e50ee67067eccebfb7 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 19:21:15 +0200 Subject: [PATCH 617/670] Add few hitsounds check tests --- .../Editing/Checks/CheckFewHitsoundsTest.cs | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs new file mode 100644 index 0000000000..8ae8cd8163 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs @@ -0,0 +1,166 @@ +// 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 NUnit.Framework; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckFewHitsoundsTest + { + private CheckFewHitsounds check; + + [SetUp] + public void Setup() + { + check = new CheckFewHitsounds(); + } + + [Test] + public void TestHitsounded() + { + var hitObjects = new List(); + + for (int i = 0; i < 16; ++i) + { + var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + + if ((i + 1) % 2 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP)); + if ((i + 1) % 3 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE)); + if ((i + 1) % 4 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + assertOk(hitObjects); + } + + [Test] + public void TestLightlyHitsounded() + { + var hitObjects = new List(); + + for (int i = 0; i < 30; ++i) + { + var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + + if (i % 8 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE)); + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + assertLongPeriodNegligible(hitObjects, count: 3); + } + + [Test] + public void TestRarelyHitsounded() + { + var hitObjects = new List(); + + for (int i = 0; i < 30; ++i) + { + var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + + if (i == 0 || i == 15) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE)); + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + // Should prompt one warning between 1st and 11th, and another between 11th and 20th. + assertLongPeriodWarning(hitObjects, count: 2); + } + + [Test] + public void TestExtremelyRarelyHitsounded() + { + var hitObjects = new List(); + + for (int i = 0; i < 80; ++i) + { + var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + + if (i == 40) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE)); + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + // Should prompt one problem between 1st and 40th, and another between 40th and 80th. + assertLongPeriodProblem(hitObjects, count: 2); + } + + [Test] + public void TestNotHitsounded() + { + var hitObjects = new List(); + + for (int i = 0; i < 20; ++i) + { + var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + // Should prompt one problem between 1st and 40th, and another between 40th and 80th. + assertNoHitsounds(hitObjects); + } + + private void assertOk(List hitObjects) + { + Assert.That(check.Run(getContext(hitObjects)), Is.Empty); + } + + private void assertLongPeriodProblem(List hitObjects, int count = 1) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodProblem)); + } + + private void assertLongPeriodWarning(List hitObjects, int count = 1) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodWarning)); + } + + private void assertLongPeriodNegligible(List hitObjects, int count = 1) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodNegligible)); + } + + private void assertNoHitsounds(List hitObjects) + { + var issues = check.Run(getContext(hitObjects)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds)); + } + + private BeatmapVerifierContext getContext(List hitObjects) + { + var beatmap = new Beatmap { HitObjects = hitObjects }; + + return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); + } + } +} From 82b64f5589a950b43afc9dc9d6b0e02c548e285c Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 19:57:12 +0200 Subject: [PATCH 618/670] Add hitsounded with break test --- .../Editing/Checks/CheckFewHitsoundsTest.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs index 8ae8cd8163..8edc6d096e 100644 --- a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs @@ -47,6 +47,31 @@ namespace osu.Game.Tests.Editing.Checks assertOk(hitObjects); } + [Test] + public void TestHitsoundedWithBreak() + { + var hitObjects = new List(); + + for (int i = 0; i < 32; ++i) + { + var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + + if ((i + 1) % 2 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP)); + if ((i + 1) % 3 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE)); + if ((i + 1) % 4 == 0) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); + // Leaves a gap in which no hitsounds exist or can be added, and so shouldn't be an issue. + if (i > 8 && i < 24) + continue; + + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); + } + + assertOk(hitObjects); + } + [Test] public void TestLightlyHitsounded() { From 51888d0d5a3fbbcf8fbd4787ffa58b9f3d7b56e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Jun 2021 19:27:34 +0200 Subject: [PATCH 619/670] Rename test methods --- .../Visual/Online/TestSceneBeatmapListingOverlay.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index cd382c2bb2..16122e496f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestNonSupportUseSupporterOnlyFiltersPlaceholderNoBeatmaps() + public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithoutResults() { AddStep("fetch for 0 beatmaps", () => fetchFor()); AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestSupportUseSupporterOnlyFiltersPlaceholderNoBeatmaps() + public void TestUserWithSupporterUsesSupporterOnlyFiltersWithoutResults() { AddStep("fetch for 0 beatmaps", () => fetchFor()); AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestNonSupporterUseSupporterOnlyFiltersPlaceholderOneBeatmap() + public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithResults() { AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); @@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestSupporterUseSupporterOnlyFiltersPlaceholderOneBeatmap() + public void TestUserWithSupporterUsesSupporterOnlyFiltersWithResults() { AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); From b7c4fe2052a38f30aa1789f8c8b9103660b51b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Jun 2021 20:24:12 +0200 Subject: [PATCH 620/670] Rewrite test helpers to also handle clearing filters --- .../Online/TestSceneBeatmapListingOverlay.cs | 100 +++++++++--------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 16122e496f..66e7248207 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.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 System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -14,6 +15,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; +using osu.Game.Scoring; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -77,27 +79,27 @@ namespace osu.Game.Tests.Visual.Online AddStep("fetch for 0 beatmaps", () => fetchFor()); AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); - // test non-supporter on Rank Achieved filter - toggleRankFilter(Scoring.ScoreRank.XH); + // only Rank Achieved filter + setRankAchievedFilter(new[] { ScoreRank.XH }); supporterRequiredPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + setRankAchievedFilter(Array.Empty()); notFoundPlaceholderShown(); - // test non-supporter on Played filter - toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + // only Played filter + setPlayedFilter(SearchPlayed.Played); supporterRequiredPlaceholderShown(); - AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + setPlayedFilter(SearchPlayed.Any); notFoundPlaceholderShown(); - // test non-supporter on both Rank Achieved and Played filter - toggleRankFilter(Scoring.ScoreRank.XH); - toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + // both RankAchieved and Played filters + setRankAchievedFilter(new[] { ScoreRank.XH }); + setPlayedFilter(SearchPlayed.Played); supporterRequiredPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); - AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + setRankAchievedFilter(Array.Empty()); + setPlayedFilter(SearchPlayed.Any); notFoundPlaceholderShown(); } @@ -107,27 +109,27 @@ namespace osu.Game.Tests.Visual.Online AddStep("fetch for 0 beatmaps", () => fetchFor()); AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); - // test supporter on Rank Achieved filter - toggleRankFilter(Scoring.ScoreRank.XH); + // only Rank Achieved filter + setRankAchievedFilter(new[] { ScoreRank.XH }); notFoundPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + setRankAchievedFilter(Array.Empty()); notFoundPlaceholderShown(); - // test supporter on Played filter - toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + // only Played filter + setPlayedFilter(SearchPlayed.Played); notFoundPlaceholderShown(); - AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + setPlayedFilter(SearchPlayed.Any); notFoundPlaceholderShown(); - // test supporter on both Rank Achieved and Played filter - toggleRankFilter(Scoring.ScoreRank.XH); - toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + // both Rank Achieved and Played filters + setRankAchievedFilter(new[] { ScoreRank.XH }); + setPlayedFilter(SearchPlayed.Played); notFoundPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); - AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + setRankAchievedFilter(Array.Empty()); + setPlayedFilter(SearchPlayed.Any); notFoundPlaceholderShown(); } @@ -137,27 +139,27 @@ namespace osu.Game.Tests.Visual.Online AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); - // test non-supporter on Rank Achieved filter - toggleRankFilter(Scoring.ScoreRank.XH); + // only Rank Achieved filter + setRankAchievedFilter(new[] { ScoreRank.XH }); supporterRequiredPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + setRankAchievedFilter(Array.Empty()); noPlaceholderShown(); - // test non-supporter on Played filter - toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + // only Played filter + setPlayedFilter(SearchPlayed.Played); supporterRequiredPlaceholderShown(); - AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + setPlayedFilter(SearchPlayed.Any); noPlaceholderShown(); - // test non-supporter on both Rank Achieved and Played filter - toggleRankFilter(Scoring.ScoreRank.XH); - toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + // both Rank Achieved and Played filters + setRankAchievedFilter(new[] { ScoreRank.XH }); + setPlayedFilter(SearchPlayed.Played); supporterRequiredPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); - AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + setRankAchievedFilter(Array.Empty()); + setPlayedFilter(SearchPlayed.Any); noPlaceholderShown(); } @@ -167,27 +169,27 @@ namespace osu.Game.Tests.Visual.Online AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); - // test supporter on Rank Achieved filter - toggleRankFilter(Scoring.ScoreRank.XH); + // only Rank Achieved filter + setRankAchievedFilter(new[] { ScoreRank.XH }); noPlaceholderShown(); - AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + setRankAchievedFilter(Array.Empty()); noPlaceholderShown(); - // test supporter on Played filter - toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + // only Played filter + setPlayedFilter(SearchPlayed.Played); noPlaceholderShown(); - AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); + setPlayedFilter(SearchPlayed.Any); noPlaceholderShown(); - // test supporter on both Rank Achieved and Played filter - toggleRankFilter(Scoring.ScoreRank.XH); - toggleSupporterOnlyPlayedFilter(SearchPlayed.Played); + // both Rank Achieved and Played filters + setRankAchievedFilter(new[] { ScoreRank.XH }); + setPlayedFilter(SearchPlayed.Played); noPlaceholderShown(); - AddStep("Set Played filter to Any", () => searchControl.Played.Value = SearchPlayed.Any); - AddStep("Clear Rank Achieved filter", () => searchControl.Ranks.Clear()); + setRankAchievedFilter(Array.Empty()); + setPlayedFilter(SearchPlayed.Any); noPlaceholderShown(); } @@ -200,18 +202,18 @@ namespace osu.Game.Tests.Visual.Online searchControl.Query.TriggerChange(); } - private void toggleRankFilter(Scoring.ScoreRank rank) + private void setRankAchievedFilter(ScoreRank[] ranks) { - AddStep("toggle Rank Achieved filter", () => + AddStep($"set Rank Achieved filter to [{string.Join(',', ranks)}]", () => { searchControl.Ranks.Clear(); - searchControl.Ranks.Add(rank); + searchControl.Ranks.AddRange(ranks); }); } - private void toggleSupporterOnlyPlayedFilter(SearchPlayed played) + private void setPlayedFilter(SearchPlayed played) { - AddStep("toggle Played filter", () => searchControl.Played.Value = played); + AddStep($"set Played filter to {played}", () => searchControl.Played.Value = played); } private void supporterRequiredPlaceholderShown() From 709e555566a80bcc9a7623c6721c99c645da2e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Jun 2021 20:27:15 +0200 Subject: [PATCH 621/670] Rename test steps for legibility --- .../Visual/Online/TestSceneBeatmapListingOverlay.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 66e7248207..5bfb676f81 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -218,17 +218,19 @@ namespace osu.Game.Tests.Visual.Online private void supporterRequiredPlaceholderShown() { - AddUntilStep("supporter-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + AddUntilStep("\"supporter required\" placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); } private void notFoundPlaceholderShown() { - AddUntilStep("not-found-placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + AddUntilStep("\"no maps found\" placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); } private void noPlaceholderShown() { - AddUntilStep("no placeholder shown", () => !overlay.ChildrenOfType().Any() && !overlay.ChildrenOfType().Any()); + AddUntilStep("no placeholder shown", () => + !overlay.ChildrenOfType().Any() + && !overlay.ChildrenOfType().Any()); } private class TestAPIBeatmapSet : APIBeatmapSet From b56dd7ff25db96dbcd73c1e0f651987bda726e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Jun 2021 20:31:26 +0200 Subject: [PATCH 622/670] Fix naming and xmldocs in new beatmap search result structures --- .../BeatmapListingFilterControl.cs | 43 +++++++++++++------ osu.Game/Overlays/BeatmapListingOverlay.cs | 4 +- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index b6a0846407..d80ef075e9 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -26,8 +26,6 @@ namespace osu.Game.Overlays.BeatmapListing { /// /// Fired when a search finishes. - /// SearchFinished.Type = ResultsReturned when results returned. Contains only new items in the case of pagination. - /// SearchFinished.Type = SupporterOnlyFilter when a non-supporter user applied supporter-only filters. /// public Action SearchFinished; @@ -216,7 +214,7 @@ namespace osu.Game.Overlays.BeatmapListing lastResponse = response; getSetsRequest = null; - // check if an non-supporter user used supporter-only filters + // check if a non-supporter used supporter-only filters if (!api.LocalUser.Value.IsSupporter) { List filters = new List(); @@ -229,7 +227,7 @@ namespace osu.Game.Overlays.BeatmapListing if (filters.Any()) { - SearchFinished?.Invoke(SearchResult.SupporterOnlyFilter(filters)); + SearchFinished?.Invoke(SearchResult.SupporterOnlyFilters(filters)); return; } } @@ -260,21 +258,40 @@ namespace osu.Game.Overlays.BeatmapListing base.Dispose(isDisposing); } + /// + /// Indicates the type of result of a user-requested beatmap search. + /// public enum SearchResultType { - // returned with Results + /// + /// Actual results have been returned from API. + /// ResultsReturned, - // non-supporter user applied supporter-only filters - SupporterOnlyFilter + + /// + /// The user is not a supporter, but used supporter-only search filters. + /// + SupporterOnlyFilters } - // Results only valid when Type == ResultsReturned - // Filters only valid when Type == SupporterOnlyFilter + /// + /// Describes the result of a user-requested beatmap search. + /// public struct SearchResult { public SearchResultType Type { get; private set; } + + /// + /// Contains the beatmap sets returned from API. + /// Valid for read if and only if is . + /// public List Results { get; private set; } - public List Filters { get; private set; } + + /// + /// Contains the names of supporter-only filters requested by the user. + /// Valid for read if and only if is . + /// + public List SupporterOnlyFiltersUsed { get; private set; } public static SearchResult ResultsReturned(List results) => new SearchResult { @@ -282,10 +299,10 @@ namespace osu.Game.Overlays.BeatmapListing Results = results }; - public static SearchResult SupporterOnlyFilter(List filters) => new SearchResult + public static SearchResult SupporterOnlyFilters(List filters) => new SearchResult { - Type = SearchResultType.SupporterOnlyFilter, - Filters = filters + Type = SearchResultType.SupporterOnlyFilters, + SupporterOnlyFiltersUsed = filters }; } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index c2ba3d5bc0..5489f0277f 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -123,9 +123,9 @@ namespace osu.Game.Overlays private void onSearchFinished(BeatmapListingFilterControl.SearchResult searchResult) { // non-supporter user used supporter-only filters - if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilter) + if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters) { - supporterRequiredContent.UpdateText(searchResult.Filters); + supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed); addContentToPlaceholder(supporterRequiredContent); return; } From 9061ab0a278e058704db7dd4c2ffa6ea476463d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Jun 2021 20:40:54 +0200 Subject: [PATCH 623/670] Update/reword comments in listing overlay --- osu.Game/Overlays/BeatmapListingOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 5489f0277f..460b4ba4c9 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -122,7 +122,6 @@ namespace osu.Game.Overlays private void onSearchFinished(BeatmapListingFilterControl.SearchResult searchResult) { - // non-supporter user used supporter-only filters if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters) { supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed); @@ -185,7 +184,7 @@ namespace osu.Game.Overlays if (lastContent == notFoundContent || lastContent == supporterRequiredContent) { - // the placeholder may be used multiple times, so don't expire/dispose it. + // the placeholders may be used multiple times, so don't expire/dispose them. transform.Schedule(() => panelTarget.Remove(lastContent)); } else @@ -253,7 +252,8 @@ namespace osu.Game.Overlays } } - // using string literals as there's no proper processing for LocalizeStrings yet + // TODO: localisation requires Text/LinkFlowContainer support for localising strings with links inside + // (https://github.com/ppy/osu-framework/issues/4530) public class SupporterRequiredDrawable : CompositeDrawable { private LinkFlowContainer supporterRequiredText; From 51147405c59e7c01fd035efa55f8ac68fadd755d Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 20:44:39 +0200 Subject: [PATCH 624/670] Make || and && priority explicit --- osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs index 07ca470e62..f9897ea20c 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Edit.Checks // Only generating issues on hitsounded or last objects ensures we get one issue per long period. // If there are no hitsounds we let the "No hitsounds" template take precedence. - if (hasHitsound(hitObject) || isLastObject && hasHitsounds) + if (hasHitsound(hitObject) || (isLastObject && hasHitsounds)) { var timeWithoutHitsounds = time - lastHitsoundTime; From f78cc9397eff96d67f4b198ffd758934ba8b09c3 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 20:45:31 +0200 Subject: [PATCH 625/670] Factor out edge type logic --- .../Rulesets/Edit/Checks/CheckMutedObjects.cs | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs index cbe7c7fbab..23d89a2f44 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs @@ -22,6 +22,14 @@ namespace osu.Game.Rulesets.Edit.Checks /// private const int low_volume_threshold = 20; + private enum EdgeType + { + Head, + Repeat, + Tail, + None + } + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Low volume hitobjects"); public IEnumerable PossibleTemplates => new IssueTemplate[] @@ -58,37 +66,43 @@ namespace osu.Game.Rulesets.Edit.Checks int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume); double samplePlayTime = sampledHitObject.GetEndTime(); - bool head = Precision.AlmostEquals(samplePlayTime, hitObject.StartTime, 1f); - bool tail = Precision.AlmostEquals(samplePlayTime, hitObject.GetEndTime(), 1f); - bool repeat = false; - - if (hitObject is IHasRepeats hasRepeats && !head && !tail) - { - double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount(); - repeat = Precision.AlmostEquals((samplePlayTime - hitObject.StartTime) % spanDuration, 0f, 1f); - } - + EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime); // We only care about samples played on the edges of objects, not ones like spinnerspin or slidertick. - if (!head && !tail && !repeat) + if (edgeType == EdgeType.None) yield break; - string postfix = null; - if (hitObject is IHasDuration) - postfix = head ? "head" : tail ? "tail" : "repeat"; + string postfix = hitObject is IHasDuration ? edgeType.ToString().ToLower() : null; if (maxVolume <= muted_threshold) { - if (head) + if (edgeType == EdgeType.Head) yield return new IssueTemplateMutedActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix); else yield return new IssueTemplateMutedPassive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix); } - else if (maxVolume <= low_volume_threshold && head) + else if (maxVolume <= low_volume_threshold && edgeType == EdgeType.Head) { yield return new IssueTemplateLowVolumeActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix); } } + private EdgeType getEdgeAtTime(HitObject hitObject, double time) + { + if (Precision.AlmostEquals(time, hitObject.StartTime, 1f)) + return EdgeType.Head; + if (Precision.AlmostEquals(time, hitObject.GetEndTime(), 1f)) + return EdgeType.Tail; + + if (hitObject is IHasRepeats hasRepeats) + { + double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount(); + if (Precision.AlmostEquals((time - hitObject.StartTime) % spanDuration, 0f, 1f)) + return EdgeType.Repeat; + } + + return EdgeType.None; + } + public abstract class IssueTemplateMuted : IssueTemplate { protected IssueTemplateMuted(ICheck check, IssueType type, string unformattedMessage) From 5642d321b70f55550b4bc992c0ccf992514cf5b9 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 23:43:08 +0200 Subject: [PATCH 626/670] Fix comments in few hitsounds check tests --- osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs index 8edc6d096e..7561e94d2e 100644 --- a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Editing.Checks hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); } - // Should prompt one warning between 1st and 11th, and another between 11th and 20th. + // Should prompt one warning between 1st and 16th, and another between 16th and 31st. assertLongPeriodWarning(hitObjects, count: 2); } @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Editing.Checks hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); } - // Should prompt one problem between 1st and 40th, and another between 40th and 80th. + // Should prompt one problem between 1st and 41st, and another between 41st and 81st. assertLongPeriodProblem(hitObjects, count: 2); } @@ -140,7 +140,6 @@ namespace osu.Game.Tests.Editing.Checks hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); } - // Should prompt one problem between 1st and 40th, and another between 40th and 80th. assertNoHitsounds(hitObjects); } From 191308434215634a96f88dc3501bacee9a2da354 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 23:48:28 +0200 Subject: [PATCH 627/670] Use `HitSampleInfo.AllAdditions` instead of new list --- osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs index f9897ea20c..acdac83141 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs @@ -32,8 +32,6 @@ namespace osu.Game.Rulesets.Edit.Checks private const int warning_threshold_objects = 4; private const int problem_threshold_objects = 16; - private static readonly string[] hitsound_types = { HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_FINISH }; - public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Few or no hitsounds"); public IEnumerable PossibleTemplates => new IssueTemplate[] @@ -111,7 +109,7 @@ namespace osu.Game.Rulesets.Edit.Checks private bool hasHitsound(HitObject hitObject) => hitObject.Samples.Any(isHitsound); private bool couldHaveHitsound(HitObject hitObject) => hitObject.Samples.Any(isHitnormal); - private bool isHitsound(HitSampleInfo sample) => hitsound_types.Any(sample.Name.Contains); + private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.Any(sample.Name.Contains); private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL); public abstract class IssueTemplateLongPeriod : IssueTemplate From d29e6f46953624bbfac6dbea4f5c0929a0071ab6 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 23:49:06 +0200 Subject: [PATCH 628/670] Add negligible template to `PossibleTemplates` --- osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs index acdac83141..1de3652a39 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs @@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Edit.Checks { new IssueTemplateLongPeriodProblem(this), new IssueTemplateLongPeriodWarning(this), + new IssueTemplateLongPeriodNegligible(this), new IssueTemplateNoHitsounds(this) }; From 5bc08ebadb95ff4c215a9f7214dfde7d66a11031 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sat, 26 Jun 2021 23:49:25 +0200 Subject: [PATCH 629/670] Rename `hasHitsounds` -> `mapHasHitsounds` --- osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs index 1de3652a39..8d8243938a 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Edit.Checks new IssueTemplateNoHitsounds(this) }; - private bool hasHitsounds; + private bool mapHasHitsounds; private int objectsWithoutHitsounds; private double lastHitsoundTime; @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Edit.Checks if (!context.Beatmap.HitObjects.Any()) yield break; - hasHitsounds = false; + mapHasHitsounds = false; objectsWithoutHitsounds = 0; lastHitsoundTime = context.Beatmap.HitObjects.First().StartTime; @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Edit.Checks yield return issue; } - if (!hasHitsounds) + if (!mapHasHitsounds) yield return new IssueTemplateNoHitsounds(this).Create(); } @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Edit.Checks // Only generating issues on hitsounded or last objects ensures we get one issue per long period. // If there are no hitsounds we let the "No hitsounds" template take precedence. - if (hasHitsound(hitObject) || (isLastObject && hasHitsounds)) + if (hasHitsound(hitObject) || (isLastObject && mapHasHitsounds)) { var timeWithoutHitsounds = time - lastHitsoundTime; @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Edit.Checks if (hasHitsound(hitObject)) { - hasHitsounds = true; + mapHasHitsounds = true; objectsWithoutHitsounds = 0; lastHitsoundTime = time; } From 4796b1b2089f6e5c64449bbb01e81182231491fd Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 27 Jun 2021 00:04:30 +0200 Subject: [PATCH 630/670] Use local variables for `hasHitsound` & `couldHaveHitsound` --- osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs index 8d8243938a..1fca6b26a4 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs @@ -82,10 +82,12 @@ namespace osu.Game.Rulesets.Edit.Checks private IEnumerable applyHitsoundUpdate(HitObject hitObject, bool isLastObject = false) { var time = hitObject.GetEndTime(); + bool hasHitsound = hitObject.Samples.Any(isHitsound); + bool couldHaveHitsound = hitObject.Samples.Any(isHitnormal); // Only generating issues on hitsounded or last objects ensures we get one issue per long period. // If there are no hitsounds we let the "No hitsounds" template take precedence. - if (hasHitsound(hitObject) || (isLastObject && mapHasHitsounds)) + if (hasHitsound || (isLastObject && mapHasHitsounds)) { var timeWithoutHitsounds = time - lastHitsoundTime; @@ -97,19 +99,16 @@ namespace osu.Game.Rulesets.Edit.Checks yield return new IssueTemplateLongPeriodNegligible(this).Create(lastHitsoundTime, timeWithoutHitsounds); } - if (hasHitsound(hitObject)) + if (hasHitsound) { mapHasHitsounds = true; objectsWithoutHitsounds = 0; lastHitsoundTime = time; } - else if (couldHaveHitsound(hitObject)) + else if (couldHaveHitsound) ++objectsWithoutHitsounds; } - private bool hasHitsound(HitObject hitObject) => hitObject.Samples.Any(isHitsound); - private bool couldHaveHitsound(HitObject hitObject) => hitObject.Samples.Any(isHitnormal); - private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.Any(sample.Name.Contains); private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL); From 0c0fd291d9381d718668e0588d6fb5dee60e91c7 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 27 Jun 2021 01:25:03 +0200 Subject: [PATCH 631/670] Order hitobjects by endtime --- .../Rulesets/Edit/Checks/CheckFewHitsounds.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs index 1fca6b26a4..5185ba6c99 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs @@ -55,18 +55,23 @@ namespace osu.Game.Rulesets.Edit.Checks objectsWithoutHitsounds = 0; lastHitsoundTime = context.Beatmap.HitObjects.First().StartTime; - var hitObjectCount = context.Beatmap.HitObjects.Count; + var hitObjectsIncludingNested = new List(); + + foreach (var hitObject in context.Beatmap.HitObjects) + { + // Samples play on the end of objects. Some objects have nested objects to accomplish playing them elsewhere (e.g. slider head/repeat). + foreach (var nestedHitObject in hitObject.NestedHitObjects) + hitObjectsIncludingNested.Add(nestedHitObject); + + hitObjectsIncludingNested.Add(hitObject); + } + + var hitObjectsByEndTime = hitObjectsIncludingNested.OrderBy(o => o.GetEndTime()).ToList(); + var hitObjectCount = hitObjectsByEndTime.Count; for (int i = 0; i < hitObjectCount; ++i) { - var hitObject = context.Beatmap.HitObjects[i]; - - // Samples play on the end of objects. Some objects have nested objects to accomplish playing them elsewhere (e.g. slider head/repeat). - foreach (var nestedHitObject in hitObject.NestedHitObjects) - { - foreach (var issue in applyHitsoundUpdate(nestedHitObject)) - yield return issue; - } + var hitObject = hitObjectsByEndTime[i]; // This is used to perform an update at the end so that the period after the last hitsounded object can be an issue. bool isLastObject = i == hitObjectCount - 1; From 2cd7eda3c4d62e7b7898c7a60de3d78ea5447b5f Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 27 Jun 2021 02:30:12 +0200 Subject: [PATCH 632/670] Add "or equal to" to volume threshold xmldocs --- osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs index 23d89a2f44..2ac1efc636 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs @@ -13,12 +13,12 @@ namespace osu.Game.Rulesets.Edit.Checks public class CheckMutedObjects : ICheck { /// - /// Volume percentages lower than this are typically inaudible. + /// Volume percentages lower than or equal to this are typically inaudible. /// private const int muted_threshold = 5; /// - /// Volume percentages lower than this can sometimes be inaudible depending on sample used and music volume. + /// Volume percentages lower than or equal to this can sometimes be inaudible depending on sample used and music volume. /// private const int low_volume_threshold = 20; From 4cfa0ae5ec79bf2e1922132f23515baa374f2b4e Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 27 Jun 2021 03:26:35 +0200 Subject: [PATCH 633/670] Improve precision for repeat edges --- osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs index 2ac1efc636..c743b5693e 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.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 System.Collections.Generic; using System.Linq; using osu.Framework.Utils; @@ -96,7 +97,9 @@ namespace osu.Game.Rulesets.Edit.Checks if (hitObject is IHasRepeats hasRepeats) { double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount(); - if (Precision.AlmostEquals((time - hitObject.StartTime) % spanDuration, 0f, 1f)) + double spans = (time - hitObject.StartTime) / spanDuration; + + if (Precision.AlmostEquals(spans, Math.Ceiling(spans)) || Precision.AlmostEquals(spans, Math.Floor(spans))) return EdgeType.Repeat; } From d1f852d102489ae8ae69069dc5ae1416ea0927da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Jun 2021 13:06:20 +0900 Subject: [PATCH 634/670] Make `Populate` abstract to avoid unnecessary base call async complexity --- osu.Game/Database/ArchiveModelManager.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 4 ++++ osu.Game/Skinning/SkinManager.cs | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 8efd451857..c1a4a6e18a 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -727,7 +727,7 @@ namespace osu.Game.Database /// The model to populate. /// The archive to use as a reference for population. May be null. /// An optional cancellation token. - protected virtual Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default) => Task.CompletedTask; + protected abstract Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default); /// /// Perform any final actions before the import to database executes. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 9d3b952ada..d5bea0affc 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Linq.Expressions; using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework.Bindables; @@ -72,6 +73,9 @@ namespace osu.Game.Scoring } } + protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) + => Task.CompletedTask; + protected override void ExportModelTo(ScoreInfo model, Stream outputStream) { var file = model.Files.SingleOrDefault(); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 4cde4cd2b8..645c943d09 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -142,16 +142,16 @@ namespace osu.Game.Skinning return base.ComputeHash(item, reader); } - protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) + protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) { - await base.Populate(model, archive, cancellationToken).ConfigureAwait(false); - var instance = GetSkin(model); model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo(); if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true) populateMetadata(model, instance); + + return Task.CompletedTask; } private void populateMetadata(SkinInfo item, Skin instance) From 46f8100f4371b64e2bda9b484493597b4a868bf5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Jun 2021 13:06:47 +0900 Subject: [PATCH 635/670] Remove overly verbose logging during beatmap imports --- osu.Game/Beatmaps/BeatmapManager.cs | 2 -- osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs | 1 - 2 files changed, 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 00af06703d..86c8fb611f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -191,8 +191,6 @@ namespace osu.Game.Beatmaps { var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); - LogForModel(beatmapSet, $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps..."); - // ensure all IDs are unique if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) { diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index 5dff4fe282..7824205257 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -48,7 +48,6 @@ namespace osu.Game.Beatmaps public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) { - LogForModel(beatmapSet, "Performing online lookups..."); return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray()); } From 15af28d2a0af88ac7f167341d1d438121ccf3a62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Jun 2021 14:48:51 +0900 Subject: [PATCH 636/670] Remove comparison of online beatmap IDs during dedupe checks --- .../Beatmaps/IO/ImportBeatmapTest.cs | 35 ------------------- osu.Game/Beatmaps/BeatmapManager.cs | 12 ------- osu.Game/Database/ArchiveModelManager.cs | 4 +-- 3 files changed, 2 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0d117f8755..2087bdf144 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -439,41 +439,6 @@ namespace osu.Game.Tests.Beatmaps.IO } } - [TestCase(true)] - [TestCase(false)] - public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}")) - { - try - { - var osu = LoadOsuIntoHost(host); - - var imported = await LoadOszIntoOsu(osu); - - if (set) - imported.OnlineBeatmapSetID = 1234; - else - imported.Beatmaps.First().OnlineBeatmapID = 1234; - - osu.Dependencies.Get().Update(imported); - - deleteBeatmapSet(imported, osu); - - var importedSecondTime = await LoadOszIntoOsu(osu); - - // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched) - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); - } - finally - { - host.Exit(); - } - } - } - [Test] public async Task TestImportWithDuplicateBeatmapIDs() { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 00af06703d..221774b018 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -319,18 +319,6 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); - protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) - { - if (!base.CanReuseExisting(existing, import)) - return false; - - var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); - var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); - - // force re-import if we are not in a sane state. - return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds); - } - /// /// Returns a list of all usable s. /// diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 8efd451857..29c83b4699 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -377,7 +377,7 @@ namespace osu.Game.Database if (existing != null) { - if (CanReuseExisting(existing, item)) + if (canReuseExisting(existing, item)) { Undelete(existing); LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); @@ -751,7 +751,7 @@ namespace osu.Game.Database /// The existing model. /// The newly imported model. /// Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import. - protected virtual bool CanReuseExisting(TModel existing, TModel import) => + private bool canReuseExisting(TModel existing, TModel import) => // for the best or worst, we copy and import files of a new import before checking whether // it is a duplicate. so to check if anything has changed, we can just compare all FileInfo IDs. getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) && From e493685c14acd80e430092e62700bcc75e59f618 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Jun 2021 16:34:40 +0900 Subject: [PATCH 637/670] Add optimised existing check earlier in import process --- osu.Game/Database/ArchiveModelManager.cs | 61 +++++++++++++++++++----- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 29c83b4699..80faa64953 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -317,7 +317,11 @@ namespace osu.Game.Database /// protected virtual string ComputeHash(TModel item, ArchiveReader reader = null) { - // for now, concatenate all .osu files in the set to create a unique hash. + if (reader != null) + // fast hashing for cases where the item's files may not be populated. + return computeHashFast(reader); + + // for now, concatenate all hashable files in the set to create a unique hash. MemoryStream hashable = new MemoryStream(); foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename)) @@ -329,12 +333,25 @@ namespace osu.Game.Database if (hashable.Length > 0) return hashable.ComputeSHA2Hash(); - if (reader != null) - return reader.Name.ComputeSHA2Hash(); - return item.Hash; } + private string computeHashFast(ArchiveReader reader) + { + MemoryStream hashable = new MemoryStream(); + + foreach (var file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f)) + { + using (Stream s = reader.GetStream(file)) + s.CopyTo(hashable); + } + + if (hashable.Length > 0) + return hashable.ComputeSHA2Hash(); + + return reader.Name.ComputeSHA2Hash(); + } + /// /// Silently import an item from a . /// @@ -348,6 +365,21 @@ namespace osu.Game.Database delayEvents(); + if (archive != null) + { + // fast bail to improve large import performance. + item.Hash = computeHashFast(archive); + + var fastExisting = CheckForExisting(item); + + if (fastExisting != null) + { + // bare minimum comparisons + if (getFilenames(fastExisting.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened))) + return fastExisting; + } + } + void rollback() { if (!Delete(item)) @@ -644,18 +676,14 @@ namespace osu.Game.Database { var fileInfos = new List(); - string prefix = reader.Filenames.GetCommonPrefix(); - if (!(prefix.EndsWith('/') || prefix.EndsWith('\\'))) - prefix = string.Empty; - // import files to manager - foreach (string file in reader.Filenames) + foreach (var filenames in getShortenedFilenames(reader)) { - using (Stream s = reader.GetStream(file)) + using (Stream s = reader.GetStream(filenames.original)) { fileInfos.Add(new TFileModel { - Filename = file.Substring(prefix.Length).ToStandardisedPath(), + Filename = filenames.shortened, FileInfo = files.Add(s) }); } @@ -664,6 +692,17 @@ namespace osu.Game.Database return fileInfos; } + private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader) + { + string prefix = reader.Filenames.GetCommonPrefix(); + if (!(prefix.EndsWith('/') || prefix.EndsWith('\\'))) + prefix = string.Empty; + + // import files to manager + foreach (string file in reader.Filenames) + yield return (file, file.Substring(prefix.Length).ToStandardisedPath()); + } + #region osu-stable import /// From 44f875b802f8d958d58cb34b065474fcc954b5ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Jun 2021 16:35:13 +0900 Subject: [PATCH 638/670] Bypass optimised existing check in `SkinManager` (due to custom hashing function) --- osu.Game/Database/ArchiveModelManager.cs | 8 +++++++- osu.Game/Skinning/SkinManager.cs | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 80faa64953..22e5486909 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -309,6 +309,12 @@ namespace osu.Game.Database Logger.Log($"{prefix} {message}", LoggingTarget.Database); } + /// + /// Whether the implementation overrides with a custom implementation. + /// Custom has implementations must bypass the early exit in the import flow (see usage). + /// + protected virtual bool HasCustomHashFunction => false; + /// /// Create a SHA-2 hash from the provided archive based on file content of all files matching . /// @@ -365,7 +371,7 @@ namespace osu.Game.Database delayEvents(); - if (archive != null) + if (archive != null && !HasCustomHashFunction) { // fast bail to improve large import performance. item.Hash = computeHashFast(archive); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 4cde4cd2b8..43cf6b6874 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -125,6 +125,8 @@ namespace osu.Game.Skinning private const string unknown_creator_string = "Unknown"; + protected override bool HasCustomHashFunction => true; + protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null) { // we need to populate early to create a hash based off skin.ini contents From 9120321731baac949045283dd0057ab1e8a723bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Jun 2021 16:48:42 +0900 Subject: [PATCH 639/670] Add comments mentioning shortcomings and avoid potential double check --- osu.Game/Database/ArchiveModelManager.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 22e5486909..ed5b54f446 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -371,18 +371,25 @@ namespace osu.Game.Database delayEvents(); + bool checkedExisting = false; + TModel existing = null; + if (archive != null && !HasCustomHashFunction) { - // fast bail to improve large import performance. + // this is a fast bail condition to improve large import performance. item.Hash = computeHashFast(archive); - var fastExisting = CheckForExisting(item); + checkedExisting = true; + existing = CheckForExisting(item); - if (fastExisting != null) + if (existing != null) { // bare minimum comparisons - if (getFilenames(fastExisting.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened))) - return fastExisting; + // + // note that this should really be checking filesizes on disk (of existing files) for some degree of sanity. + // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files. + if (getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) + return existing; } } @@ -411,7 +418,8 @@ namespace osu.Game.Database { if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}"); - var existing = CheckForExisting(item); + if (!checkedExisting) + existing = CheckForExisting(item); if (existing != null) { From f2164049526e8c94e40ad4db5ec96edecdc257d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Jun 2021 20:22:48 +0900 Subject: [PATCH 640/670] Fix missing undelete call on using existing --- osu.Game/Database/ArchiveModelManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ed5b54f446..7ea6fe067c 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -389,7 +389,10 @@ namespace osu.Game.Database // note that this should really be checking filesizes on disk (of existing files) for some degree of sanity. // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files. if (getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) + { + Undelete(existing); return existing; + } } } From cd9aa38d3debcc69730ef7e89f5330d463b2c634 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Jun 2021 20:24:16 +0900 Subject: [PATCH 641/670] Add back ignore cases for intentionally broken tests --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 2087bdf144..79c85f6c61 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -192,6 +192,7 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] + [Ignore("intentionally broken by import optimisations")] public async Task TestImportThenImportWithChangedFile() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) @@ -294,6 +295,7 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] + [Ignore("intentionally broken by import optimisations")] public async Task TestImportCorruptThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. From f470b7095d9e6aafa201a9abf1ba098093ede571 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Jun 2021 20:36:01 +0900 Subject: [PATCH 642/670] Move private method down in class --- osu.Game/Database/ArchiveModelManager.cs | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 7ea6fe067c..9b89037334 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -342,22 +342,6 @@ namespace osu.Game.Database return item.Hash; } - private string computeHashFast(ArchiveReader reader) - { - MemoryStream hashable = new MemoryStream(); - - foreach (var file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f)) - { - using (Stream s = reader.GetStream(file)) - s.CopyTo(hashable); - } - - if (hashable.Length > 0) - return hashable.ComputeSHA2Hash(); - - return reader.Name.ComputeSHA2Hash(); - } - /// /// Silently import an item from a . /// @@ -686,6 +670,22 @@ namespace osu.Game.Database } } + private string computeHashFast(ArchiveReader reader) + { + MemoryStream hashable = new MemoryStream(); + + foreach (var file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f)) + { + using (Stream s = reader.GetStream(file)) + s.CopyTo(hashable); + } + + if (hashable.Length > 0) + return hashable.ComputeSHA2Hash(); + + return reader.Name.ComputeSHA2Hash(); + } + /// /// Create all required s for the provided archive, adding them to the global file store. /// From e755dcc34dffbaa8b6415fa9adf5fe61580cf940 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Jun 2021 20:37:12 +0900 Subject: [PATCH 643/670] Add log method for new flow --- osu.Game/Database/ArchiveModelManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 9b89037334..e1202ed95f 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -374,6 +374,7 @@ namespace osu.Game.Database // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files. if (getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) { + LogForModel(item, $"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); Undelete(existing); return existing; } From c2ceb83bbb20c53edff6c4973fb45e48a984cad2 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:16:40 +0200 Subject: [PATCH 644/670] Move `MockNestedHitObject` to own class --- .../Editing/Checks/CheckMutedObjectsTest.cs | 28 --------------- .../Editing/Checks/MockNestableHitObject.cs | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs diff --git a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs index 4bfe62a64d..41a8f72305 100644 --- a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading; using NUnit.Framework; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -11,7 +10,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Beatmaps; @@ -27,32 +25,6 @@ namespace osu.Game.Tests.Editing.Checks private const int volume_low = 15; private const int volume_muted = 5; - private sealed class MockNestableHitObject : HitObject, IHasDuration - { - private readonly IEnumerable toBeNested; - - public MockNestableHitObject(IEnumerable toBeNested, double startTime, double endTime) - { - this.toBeNested = toBeNested; - StartTime = startTime; - EndTime = endTime; - } - - protected override void CreateNestedHitObjects(CancellationToken cancellationToken) - { - foreach (var hitObject in toBeNested) - AddNested(hitObject); - } - - public double EndTime { get; } - - public double Duration - { - get => EndTime - StartTime; - set => throw new System.NotImplementedException(); - } - } - [SetUp] public void Setup() { diff --git a/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs b/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs new file mode 100644 index 0000000000..29938839d3 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/MockNestableHitObject.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.Collections.Generic; +using System.Threading; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Tests.Editing.Checks +{ + public sealed class MockNestableHitObject : HitObject, IHasDuration + { + private readonly IEnumerable toBeNested; + + public MockNestableHitObject(IEnumerable toBeNested, double startTime, double endTime) + { + this.toBeNested = toBeNested; + StartTime = startTime; + EndTime = endTime; + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + foreach (var hitObject in toBeNested) + AddNested(hitObject); + } + + public double EndTime { get; } + + public double Duration + { + get => EndTime - StartTime; + set => throw new System.NotImplementedException(); + } + } +} From 1d5bff166043e0b7b41c98603c65af3e411ec115 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:26:52 +0200 Subject: [PATCH 645/670] Add concurrent hitobjects test for few hitsounds check See https://github.com/ppy/osu/pull/13669#discussion_r659314980 --- .../Editing/Checks/CheckFewHitsoundsTest.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs index 7561e94d2e..d681c3fd3d 100644 --- a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; @@ -143,6 +144,35 @@ namespace osu.Game.Tests.Editing.Checks assertNoHitsounds(hitObjects); } + [Test] + public void TestConcurrentObjects() + { + var hitObjects = new List(); + + var notHitsounded = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + var hitsounded = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL), + new HitSampleInfo(HitSampleInfo.HIT_FINISH) + }; + + var ticks = new List(); + for (int i = 1; i < 10; ++i) + ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded }); + + var nested = new MockNestableHitObject(ticks.ToList(), 0, 50000) + { + Samples = notHitsounded + }; + nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + hitObjects.Add(nested); + + for (int i = 1; i <= 6; ++i) + hitObjects.Add(new HitCircle { StartTime = 10000 * i, Samples = notHitsounded }); + + assertOk(hitObjects); + } + private void assertOk(List hitObjects) { Assert.That(check.Run(getContext(hitObjects)), Is.Empty); From a4a5325b73e8e6108137aacd40dbdf93c074f980 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:39:31 +0200 Subject: [PATCH 646/670] Improve acceptable difference for repeat edges Likelihood that `spanDuration` is greater than E+7 is quite low in any realistic case, so this should work fine. --- osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs index c743b5693e..0559a8b0cd 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs @@ -98,9 +98,13 @@ namespace osu.Game.Rulesets.Edit.Checks { double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount(); double spans = (time - hitObject.StartTime) / spanDuration; + double acceptableDifference = 1 / spanDuration; // 1 ms of acceptable difference, as with head/tail above. - if (Precision.AlmostEquals(spans, Math.Ceiling(spans)) || Precision.AlmostEquals(spans, Math.Floor(spans))) + if (Precision.AlmostEquals(spans, Math.Ceiling(spans), acceptableDifference) || + Precision.AlmostEquals(spans, Math.Floor(spans), acceptableDifference)) + { return EdgeType.Repeat; + } } return EdgeType.None; From 9f9e96ce9ee797024585af8515338f67bc0175d6 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:40:09 +0200 Subject: [PATCH 647/670] Add check for `spanDuration` <= 0 prior to division --- osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs index 0559a8b0cd..a4ff921b7e 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs @@ -97,6 +97,10 @@ namespace osu.Game.Rulesets.Edit.Checks if (hitObject is IHasRepeats hasRepeats) { double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount(); + if (spanDuration <= 0) + // Prevents undefined behaviour in cases like where zero/negative-length sliders/hold notes exist. + return EdgeType.None; + double spans = (time - hitObject.StartTime) / spanDuration; double acceptableDifference = 1 / spanDuration; // 1 ms of acceptable difference, as with head/tail above. From 1dbac76da5c4366bab40f1962faa40e0322d0c5b Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:57:41 +0200 Subject: [PATCH 648/670] Use local variables for common sample lists --- .../Editing/Checks/CheckFewHitsoundsTest.cs | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs index d681c3fd3d..21e274c2c0 100644 --- a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs @@ -20,10 +20,19 @@ namespace osu.Game.Tests.Editing.Checks { private CheckFewHitsounds check; + private List notHitsounded; + private List hitsounded; + [SetUp] public void Setup() { check = new CheckFewHitsounds(); + notHitsounded = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; + hitsounded = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL), + new HitSampleInfo(HitSampleInfo.HIT_FINISH) + }; } [Test] @@ -80,10 +89,7 @@ namespace osu.Game.Tests.Editing.Checks for (int i = 0; i < 30; ++i) { - var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; - - if (i % 8 == 0) - samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE)); + var samples = i % 8 == 0 ? hitsounded : notHitsounded; hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); } @@ -98,10 +104,7 @@ namespace osu.Game.Tests.Editing.Checks for (int i = 0; i < 30; ++i) { - var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; - - if (i == 0 || i == 15) - samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE)); + var samples = (i == 0 || i == 15) ? hitsounded : notHitsounded; hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); } @@ -117,10 +120,7 @@ namespace osu.Game.Tests.Editing.Checks for (int i = 0; i < 80; ++i) { - var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; - - if (i == 40) - samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE)); + var samples = i == 40 ? hitsounded : notHitsounded; hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); } @@ -135,11 +135,7 @@ namespace osu.Game.Tests.Editing.Checks var hitObjects = new List(); for (int i = 0; i < 20; ++i) - { - var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; - - hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples }); - } + hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = notHitsounded }); assertNoHitsounds(hitObjects); } @@ -149,13 +145,6 @@ namespace osu.Game.Tests.Editing.Checks { var hitObjects = new List(); - var notHitsounded = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }; - var hitsounded = new List - { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL), - new HitSampleInfo(HitSampleInfo.HIT_FINISH) - }; - var ticks = new List(); for (int i = 1; i < 10; ++i) ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded }); From b58644106c188d09802dd8d1e455633193651f5d Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:58:00 +0200 Subject: [PATCH 649/670] Add nested hitobject tests for few hitsounds check --- .../Editing/Checks/CheckFewHitsoundsTest.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs index 21e274c2c0..cf5b3a42a4 100644 --- a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs @@ -140,6 +140,38 @@ namespace osu.Game.Tests.Editing.Checks assertNoHitsounds(hitObjects); } + [Test] + public void TestNestedObjectsHitsounded() + { + var ticks = new List(); + for (int i = 1; i < 16; ++i) + ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = hitsounded }); + + var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000) + { + Samples = hitsounded + }; + nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + assertOk(new List { nested }); + } + + [Test] + public void TestNestedObjectsRarelyHitsounded() + { + var ticks = new List(); + for (int i = 1; i < 16; ++i) + ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = i == 0 ? hitsounded : notHitsounded }); + + var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000) + { + Samples = hitsounded + }; + nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + assertLongPeriodWarning(new List { nested }); + } + [Test] public void TestConcurrentObjects() { From 9a96cd4a1df625322e644fc9b708676fbe62cd04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 09:54:18 +0900 Subject: [PATCH 650/670] Revert "Remove comparison of online beatmap IDs during dedupe checks" This reverts commit 15af28d2a0af88ac7f167341d1d438121ccf3a62. --- .../Beatmaps/IO/ImportBeatmapTest.cs | 35 +++++++++++++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 12 +++++++ osu.Game/Database/ArchiveModelManager.cs | 4 +-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 79c85f6c61..990e75df81 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -441,6 +441,41 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [TestCase(true)] + [TestCase(false)] + public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) + { + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}")) + { + try + { + var osu = LoadOsuIntoHost(host); + + var imported = await LoadOszIntoOsu(osu); + + if (set) + imported.OnlineBeatmapSetID = 1234; + else + imported.Beatmaps.First().OnlineBeatmapID = 1234; + + osu.Dependencies.Get().Update(imported); + + deleteBeatmapSet(imported, osu); + + var importedSecondTime = await LoadOszIntoOsu(osu); + + // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched) + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + } + finally + { + host.Exit(); + } + } + } + [Test] public async Task TestImportWithDuplicateBeatmapIDs() { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 221774b018..00af06703d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -319,6 +319,18 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); + protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) + { + if (!base.CanReuseExisting(existing, import)) + return false; + + var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); + var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); + + // force re-import if we are not in a sane state. + return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds); + } + /// /// Returns a list of all usable s. /// diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index e1202ed95f..84473f57fe 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -411,7 +411,7 @@ namespace osu.Game.Database if (existing != null) { - if (canReuseExisting(existing, item)) + if (CanReuseExisting(existing, item)) { Undelete(existing); LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); @@ -808,7 +808,7 @@ namespace osu.Game.Database /// The existing model. /// The newly imported model. /// Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import. - private bool canReuseExisting(TModel existing, TModel import) => + protected virtual bool CanReuseExisting(TModel existing, TModel import) => // for the best or worst, we copy and import files of a new import before checking whether // it is a duplicate. so to check if anything has changed, we can just compare all FileInfo IDs. getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) && From 90b87cbb9eaba15ade0585a881ded8978192d871 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 10:11:27 +0900 Subject: [PATCH 651/670] Add back unidirectional online id check --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 10 +++------- osu.Game/Beatmaps/BeatmapManager.cs | 8 ++++++++ osu.Game/Database/ArchiveModelManager.cs | 12 +++++++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 990e75df81..e87b82cd2a 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -441,9 +441,7 @@ namespace osu.Game.Tests.Beatmaps.IO } } - [TestCase(true)] - [TestCase(false)] - public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) + public async Task TestImportThenDeleteThenImportWithOnlineIDsMissing() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}")) @@ -454,10 +452,8 @@ namespace osu.Game.Tests.Beatmaps.IO var imported = await LoadOszIntoOsu(osu); - if (set) - imported.OnlineBeatmapSetID = 1234; - else - imported.Beatmaps.First().OnlineBeatmapID = 1234; + foreach (var b in imported.Beatmaps) + b.OnlineBeatmapID = null; osu.Dependencies.Get().Update(imported); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 00af06703d..f854a5fecb 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -319,6 +319,14 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); + protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) + { + if (!base.CanReuseExisting(existing, import)) + return false; + + return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null); + } + protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) { if (!base.CanReuseExisting(existing, import)) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 84473f57fe..6d8b671fd8 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -372,7 +372,8 @@ namespace osu.Game.Database // // note that this should really be checking filesizes on disk (of existing files) for some degree of sanity. // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files. - if (getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) + if (CanSkipImport(existing, item) && + getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) { LogForModel(item, $"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); Undelete(existing); @@ -801,6 +802,15 @@ namespace osu.Game.Database /// An existing model which matches the criteria to skip importing, else null. protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); + /// + /// Whether inport can be skipped after finding an existing import early in the process. + /// Only valid when is not overridden. + /// + /// The existing model. + /// The newly imported model. + /// Whether to skip this import completely. + protected virtual bool CanSkipImport(TModel existing, TModel import) => true; + /// /// After an existing is found during an import process, the default behaviour is to use/restore the existing /// item and skip the import. This method allows changing that behaviour. From 128f08ccbadaf5a2b027f88b651c8ace8a7696f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 10:42:28 +0900 Subject: [PATCH 652/670] Fix test oversights --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index e87b82cd2a..02b1d8bcfc 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -441,10 +441,11 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] public async Task TestImportThenDeleteThenImportWithOnlineIDsMissing() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}")) { try { From 4a557e73a781b4d71f79f797ccfe876c90b9d89c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 10:42:42 +0900 Subject: [PATCH 653/670] Add logging to help understand existing case skips better --- osu.Game/Database/ArchiveModelManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 6d8b671fd8..91ffe2966a 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -379,6 +379,8 @@ namespace osu.Game.Database Undelete(existing); return existing; } + + LogForModel(item, $"Found existing (optimised) but failed pre-check."); } } @@ -422,6 +424,7 @@ namespace osu.Game.Database return existing; } + LogForModel(item, $"Found existing but failed re-use check."); Delete(existing); ModelStore.PurgeDeletable(s => s.ID == existing.ID); } From 0cceef8da50264bbfa0c539e183acd04ee1d6e68 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 28 Jun 2021 11:00:07 +0800 Subject: [PATCH 654/670] Moved the `string` to `int?` conversion logic into `SettingsNumberBox` --- .../Screens/Editors/RoundEditorScreen.cs | 12 ++-- .../Screens/Editors/SeedingEditorScreen.cs | 14 ++-- .../Screens/Editors/TeamEditorScreen.cs | 12 ++-- .../Overlays/Settings/SettingsNumberBox.cs | 59 +++++++++++++-- osu.Game/Rulesets/Mods/ModRandom.cs | 3 +- osu.Game/Rulesets/Mods/SeedSettingsControl.cs | 72 ------------------- 6 files changed, 72 insertions(+), 100 deletions(-) delete mode 100644 osu.Game/Rulesets/Mods/SeedSettingsControl.cs diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 069ddfa4db..e91eed420e 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved] protected IAPIProvider API { get; private set; } - private readonly Bindable beatmapId = new Bindable(); + private readonly Bindable beatmapId = new Bindable(); private readonly Bindable mods = new Bindable(); @@ -220,14 +220,12 @@ namespace osu.Game.Tournament.Screens.Editors [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - beatmapId.Value = Model.ID.ToString(); - beatmapId.BindValueChanged(idString => + beatmapId.Value = Model.ID; + beatmapId.BindValueChanged(idInt => { - int.TryParse(idString.NewValue, out var parsed); + Model.ID = idInt.NewValue ?? 0; - Model.ID = parsed; - - if (idString.NewValue != idString.OldValue) + if (idInt.NewValue != idInt.OldValue) Model.BeatmapInfo = null; if (Model.BeatmapInfo != null) diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 7bd8d3f6a0..4b50d74dc8 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved] protected IAPIProvider API { get; private set; } - private readonly Bindable beatmapId = new Bindable(); + private readonly Bindable beatmapId = new Bindable(); private readonly Bindable score = new Bindable(); @@ -228,16 +228,12 @@ namespace osu.Game.Tournament.Screens.Editors [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - beatmapId.Value = Model.ID.ToString(); - beatmapId.BindValueChanged(idString => + beatmapId.Value = Model.ID; + beatmapId.BindValueChanged(idInt => { - int parsed; + Model.ID = idInt.NewValue ?? 0; - int.TryParse(idString.NewValue, out parsed); - - Model.ID = parsed; - - if (idString.NewValue != idString.OldValue) + if (idInt.NewValue != idInt.OldValue) Model.BeatmapInfo = null; if (Model.BeatmapInfo != null) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index aa1be143ea..22ec2d3398 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -214,7 +214,7 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved] private TournamentGameBase game { get; set; } - private readonly Bindable userId = new Bindable(); + private readonly Bindable userId = new Bindable(); private readonly Container drawableContainer; @@ -278,14 +278,12 @@ namespace osu.Game.Tournament.Screens.Editors [BackgroundDependencyLoader] private void load() { - userId.Value = user.Id.ToString(); - userId.BindValueChanged(idString => + userId.Value = user.Id; + userId.BindValueChanged(idInt => { - int.TryParse(idString.NewValue, out var parsed); + user.Id = idInt.NewValue ?? 0; - user.Id = parsed; - - if (idString.NewValue != idString.OldValue) + if (idInt.NewValue != idInt.OldValue) user.Username = string.Empty; if (!string.IsNullOrEmpty(user.Username)) diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index d4d1fc8610..bc86f2c6dc 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -1,17 +1,68 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { - public class SettingsNumberBox : SettingsItem + public class SettingsNumberBox : SettingsItem { - protected override Drawable CreateControl() => new OutlinedNumberBox + protected override Drawable CreateControl() => new NumberControl { - Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true + Margin = new MarginPadding { Top = 5 } }; + + private sealed class NumberControl : CompositeDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + private readonly OutlinedNumberBox numberBox; + + public Bindable Current + { + get => current; + set + { + current.Current = value; + numberBox.Text = value.Value.ToString(); + } + } + + public NumberControl() + { + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + numberBox = new OutlinedNumberBox + { + Margin = new MarginPadding { Top = 5 }, + RelativeSizeAxes = Axes.X, + CommitOnFocusLost = true + } + }; + + numberBox.Current.BindValueChanged(e => + { + int? value = null; + + if (int.TryParse(e.NewValue, out var intVal)) + value = intVal; + + current.Value = value; + }); + } + + protected override void Update() + { + base.Update(); + if (Current.Value == null) + numberBox.Current.Value = ""; + } + } } } diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 61297c162d..1f7742b075 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods { @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.Dice; public override double ScoreMultiplier => 1; - [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))] + [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] public Bindable Seed { get; } = new Bindable { Default = null, diff --git a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs b/osu.Game/Rulesets/Mods/SeedSettingsControl.cs deleted file mode 100644 index 1eaf31874b..0000000000 --- a/osu.Game/Rulesets/Mods/SeedSettingsControl.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Overlays.Settings; - -namespace osu.Game.Rulesets.Mods -{ - /// - /// A settings control for use by mods which have a customisable seed value. - /// - public class SeedSettingsControl : SettingsItem - { - protected override Drawable CreateControl() => new SeedControl - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 5 } - }; - - private sealed class SeedControl : CompositeDrawable, IHasCurrentValue - { - private readonly BindableWithCurrent current = new BindableWithCurrent(); - - public Bindable Current - { - get => current; - set - { - current.Current = value; - seedNumberBox.Text = value.Value.ToString(); - } - } - - private readonly OutlinedNumberBox seedNumberBox; - - public SeedControl() - { - AutoSizeAxes = Axes.Y; - - InternalChildren = new[] - { - seedNumberBox = new OutlinedNumberBox - { - Margin = new MarginPadding { Top = 5 }, - RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true - } - }; - - seedNumberBox.Current.BindValueChanged(e => - { - int? value = null; - - if (int.TryParse(e.NewValue, out var intVal)) - value = intVal; - - current.Value = value; - }); - } - - protected override void Update() - { - base.Update(); - if (Current.Value == null) - seedNumberBox.Current.Value = ""; - } - } - } -} From 4d6002ab889e449313f2fe6a8d6718377640a9f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 12:13:11 +0900 Subject: [PATCH 655/670] Remove redundant string interpolation (and mark all local logging strings as verbatim) --- osu.Game/Database/ArchiveModelManager.cs | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 91ffe2966a..5446c3b851 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -78,7 +78,7 @@ namespace osu.Game.Database private readonly Bindable> itemRemoved = new Bindable>(); - public virtual IEnumerable HandledExtensions => new[] { ".zip" }; + public virtual IEnumerable HandledExtensions => new[] { @".zip" }; protected readonly FileStore Files; @@ -99,7 +99,7 @@ namespace osu.Game.Database ModelStore.ItemUpdated += item => handleEvent(() => itemUpdated.Value = new WeakReference(item)); ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference(item)); - exportStorage = storage.GetStorageForDirectory("exports"); + exportStorage = storage.GetStorageForDirectory(@"exports"); Files = new FileStore(contextFactory, storage); @@ -282,7 +282,7 @@ namespace osu.Game.Database } catch (Exception e) { - LogForModel(model, $"Model creation of {archive.Name} failed.", e); + LogForModel(model, @$"Model creation of {archive.Name} failed.", e); return null; } @@ -375,12 +375,12 @@ namespace osu.Game.Database if (CanSkipImport(existing, item) && getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) { - LogForModel(item, $"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); + LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); Undelete(existing); return existing; } - LogForModel(item, $"Found existing (optimised) but failed pre-check."); + LogForModel(item, @"Found existing (optimised) but failed pre-check."); } } @@ -389,14 +389,14 @@ namespace osu.Game.Database if (!Delete(item)) { // We may have not yet added the model to the underlying table, but should still clean up files. - LogForModel(item, "Dereferencing files for incomplete import."); + LogForModel(item, @"Dereferencing files for incomplete import."); Files.Dereference(item.Files.Select(f => f.FileInfo).ToArray()); } } try { - LogForModel(item, "Beginning import..."); + LogForModel(item, @"Beginning import..."); item.Files = archive != null ? createFileInfos(archive, Files) : new List(); item.Hash = ComputeHash(item, archive); @@ -407,7 +407,7 @@ namespace osu.Game.Database { try { - if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}"); + if (!write.IsTransactionLeader) throw new InvalidOperationException(@$"Ensure there is no parent transaction so errors can correctly be handled by {this}"); if (!checkedExisting) existing = CheckForExisting(item); @@ -417,14 +417,14 @@ namespace osu.Game.Database if (CanReuseExisting(existing, item)) { Undelete(existing); - LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); + LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); // existing item will be used; rollback new import and exit early. rollback(); flushEvents(true); return existing; } - LogForModel(item, $"Found existing but failed re-use check."); + LogForModel(item, @"Found existing but failed re-use check."); Delete(existing); ModelStore.PurgeDeletable(s => s.ID == existing.ID); } @@ -441,12 +441,12 @@ namespace osu.Game.Database } } - LogForModel(item, "Import successfully completed!"); + LogForModel(item, @"Import successfully completed!"); } catch (Exception e) { if (!(e is TaskCanceledException)) - LogForModel(item, "Database import or population failed and has been rolled back.", e); + LogForModel(item, @"Database import or population failed and has been rolled back.", e); rollback(); flushEvents(false); @@ -466,7 +466,7 @@ namespace osu.Game.Database var retrievedItem = ModelStore.ConsumableItems.FirstOrDefault(s => s.ID == item.ID); if (retrievedItem == null) - throw new ArgumentException("Specified model could not be found", nameof(item)); + throw new ArgumentException(@"Specified model could not be found", nameof(item)); using (var outputStream = exportStorage.GetStream($"{getValidFilename(item.ToString())}{HandledExtensions.First()}", FileAccess.Write, FileMode.Create)) ExportModelTo(retrievedItem, outputStream); @@ -757,7 +757,7 @@ namespace osu.Game.Database { string fullPath = storage.GetFullPath(ImportFromStablePath); - Logger.Log($"Folder \"{fullPath}\" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error); + Logger.Log(@$"Folder ""{fullPath}"" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error); return Task.CompletedTask; } @@ -841,7 +841,7 @@ namespace osu.Game.Database private DbSet queryModel() => ContextFactory.Get().Set(); - protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}"; + protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; #region Event handling / delaying From 3d19364a7121702bb9b6c61068e82fb7223129af Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 28 Jun 2021 11:20:00 +0800 Subject: [PATCH 656/670] Use `BindValueChanged` instead of setting the value in property setter --- .../Overlays/Settings/SettingsNumberBox.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index bc86f2c6dc..de3e5a6bb0 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -20,22 +20,18 @@ namespace osu.Game.Overlays.Settings { private readonly BindableWithCurrent current = new BindableWithCurrent(); - private readonly OutlinedNumberBox numberBox; - public Bindable Current { - get => current; - set - { - current.Current = value; - numberBox.Text = value.Value.ToString(); - } + get => current.Current; + set => current.Current = value; } public NumberControl() { AutoSizeAxes = Axes.Y; + OutlinedNumberBox numberBox; + InternalChildren = new[] { numberBox = new OutlinedNumberBox @@ -55,13 +51,11 @@ namespace osu.Game.Overlays.Settings current.Value = value; }); - } - protected override void Update() - { - base.Update(); - if (Current.Value == null) - numberBox.Current.Value = ""; + Current.BindValueChanged(e => + { + numberBox.Current.Value = e.NewValue?.ToString(); + }); } } } From a3946a1265fbee6968daf04dbf563b95e06330b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 14:07:21 +0900 Subject: [PATCH 657/670] Fix typo in newly added xmldoc Co-authored-by: Salman Ahmed --- osu.Game/Database/ArchiveModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 5446c3b851..3798c0c6ae 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -311,7 +311,7 @@ namespace osu.Game.Database /// /// Whether the implementation overrides with a custom implementation. - /// Custom has implementations must bypass the early exit in the import flow (see usage). + /// Custom hash implementations must bypass the early exit in the import flow (see usage). /// protected virtual bool HasCustomHashFunction => false; From e387feb1d6261b491246e014de199e921a290103 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 14:39:55 +0900 Subject: [PATCH 658/670] Add inline comment mentioning why `CreateChildDependencies` is being used in this instance --- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index abf5cb040a..cb8b0fb3c8 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -47,11 +47,11 @@ namespace osu.Game.Skinning protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { parentSource = parent.Get(); - - UpdateSkinSources(); - parentSource.SourceChanged += OnSourceChanged; + // ensure sources are populated and ready for use before childrens' asynchronous load flow. + UpdateSkinSources(); + return base.CreateChildDependencies(parent); } From 73bd88cb318ae1c8b4690a8398587bb87f83b3ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 14:44:52 +0900 Subject: [PATCH 659/670] Simplify caching in test --- .../Rulesets/TestSceneRulesetSkinProvidingContainer.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs index 0dde0a8194..25619de323 100644 --- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs +++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs @@ -24,12 +24,8 @@ namespace osu.Game.Tests.Rulesets protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset(); - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new TestSkinProvider()); - return dependencies; - } + [Cached(typeof(ISkinSource))] + private readonly ISkinSource testSource = new TestSkinProvider(); [Test] public void TestEarlyAddedSkinRequester() From c281e43cd8beb91f50e0853579fab33699718502 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 15:04:14 +0900 Subject: [PATCH 660/670] Remove `Dispose()` special case and add explicit exception to make debugging issues non-deadlock --- osu.Game/Database/RealmContextFactory.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 4d81f8676f..2f70d8af22 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -97,6 +97,9 @@ namespace osu.Game.Database { try { + if (IsDisposed) + throw new InvalidOperationException(@"Attempted to retrieve a context after the factor has already been disposed."); + blockingLock.Wait(); contexts_created.Value++; @@ -128,11 +131,8 @@ namespace osu.Game.Database { base.Dispose(isDisposing); - // In the standard case, operations will already be blocked by the Update thread "pausing" from GameHost exit. - // This avoids waiting (forever) on an already entered semaphore. - if (context != null || active_usages.Value > 0) - BlockAllOperations(); - + // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. + BlockAllOperations(); blockingLock?.Dispose(); } From 7dd566dc46639a28e86fda9bd0ba13030aaf74ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 15:08:49 +0900 Subject: [PATCH 661/670] Add null check for safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/OsuGameBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dec738e5b3..bf1b449292 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -370,7 +370,7 @@ namespace osu.Game switch (state.NewValue) { case GameThreadState.Running: - blocking.Dispose(); + blocking?.Dispose(); blocking = null; break; From 88c6143aaeedc656e9fe81d2ea2efa7ce8069209 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 15:23:19 +0900 Subject: [PATCH 662/670] Rename variables in line with standards --- osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs | 6 +++--- osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs | 6 +++--- osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index e91eed420e..27ad6650d1 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -221,11 +221,11 @@ namespace osu.Game.Tournament.Screens.Editors private void load(RulesetStore rulesets) { beatmapId.Value = Model.ID; - beatmapId.BindValueChanged(idInt => + beatmapId.BindValueChanged(id => { - Model.ID = idInt.NewValue ?? 0; + Model.ID = id.NewValue ?? 0; - if (idInt.NewValue != idInt.OldValue) + if (id.NewValue != id.OldValue) Model.BeatmapInfo = null; if (Model.BeatmapInfo != null) diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 4b50d74dc8..6418bf97da 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -229,11 +229,11 @@ namespace osu.Game.Tournament.Screens.Editors private void load(RulesetStore rulesets) { beatmapId.Value = Model.ID; - beatmapId.BindValueChanged(idInt => + beatmapId.BindValueChanged(id => { - Model.ID = idInt.NewValue ?? 0; + Model.ID = id.NewValue ?? 0; - if (idInt.NewValue != idInt.OldValue) + if (id.NewValue != id.OldValue) Model.BeatmapInfo = null; if (Model.BeatmapInfo != null) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 22ec2d3398..0d2e64f300 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -279,11 +279,11 @@ namespace osu.Game.Tournament.Screens.Editors private void load() { userId.Value = user.Id; - userId.BindValueChanged(idInt => + userId.BindValueChanged(id => { - user.Id = idInt.NewValue ?? 0; + user.Id = id.NewValue ?? 0; - if (idInt.NewValue != idInt.OldValue) + if (id.NewValue != id.OldValue) user.Username = string.Empty; if (!string.IsNullOrEmpty(user.Username)) From eeb56970ef036f6dedebe72e57d218410d5b7b88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 15:24:55 +0900 Subject: [PATCH 663/670] Make `OutlinedNumberBox` private and nested again --- osu.Game/Overlays/Settings/OutlinedNumberBox.cs | 10 ---------- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 5 +++++ 2 files changed, 5 insertions(+), 10 deletions(-) delete mode 100644 osu.Game/Overlays/Settings/OutlinedNumberBox.cs diff --git a/osu.Game/Overlays/Settings/OutlinedNumberBox.cs b/osu.Game/Overlays/Settings/OutlinedNumberBox.cs deleted file mode 100644 index 6fcadc09e1..0000000000 --- a/osu.Game/Overlays/Settings/OutlinedNumberBox.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Overlays.Settings -{ - public class OutlinedNumberBox : OutlinedTextBox - { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); - } -} diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index de3e5a6bb0..2fbe522479 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -58,5 +58,10 @@ namespace osu.Game.Overlays.Settings }); } } + + private class OutlinedNumberBox : OutlinedTextBox + { + protected override bool CanAddCharacter(char character) => char.IsNumber(character); + } } } From e4780d4b06f9eb7b2b420114853f9b99b93481c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 15:29:47 +0900 Subject: [PATCH 664/670] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 3c4380e355..c845d7f276 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f91620bd25..f047859dbb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 22c4340ba2..304047ad12 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 842f033522efc6b90ee88ff8d8684897e09d09c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 16:11:55 +0900 Subject: [PATCH 665/670] Remove no longer necessary exception --- osu.Game/Database/RealmContextFactory.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 2f70d8af22..461444870e 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -44,9 +44,6 @@ namespace osu.Game.Database { get { - if (IsDisposed) - throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory"); - if (context == null) { context = createContext(); From 90f0bc87f5002b0479ec01508584c637d042358f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 16:12:21 +0900 Subject: [PATCH 666/670] Add safety against double disposal --- osu.Game/Database/RealmContextFactory.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 461444870e..0b0263a16b 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -126,22 +126,28 @@ namespace osu.Game.Database protected override void Dispose(bool isDisposing) { - base.Dispose(isDisposing); + if (!IsDisposed) + { + // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. + BlockAllOperations(); + blockingLock?.Dispose(); + } - // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. - BlockAllOperations(); - blockingLock?.Dispose(); + base.Dispose(isDisposing); } public IDisposable BlockAllOperations() { + if (IsDisposed) + throw new InvalidOperationException(@"Attempted to block operations after already disposed."); + blockingLock.Wait(); flushContexts(); return new InvokeOnDisposal(this, endBlockingSection); - } - private static void endBlockingSection(RealmContextFactory factory) => factory.blockingLock.Release(); + static void endBlockingSection(RealmContextFactory factory) => factory.blockingLock.Release(); + } private void flushContexts() { From f78cedd0e1d799aaec1562bcb1d1677c72bb5536 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 16:14:14 +0900 Subject: [PATCH 667/670] Reorder methods and add xmldoc for `BlockAllOperations` --- osu.Game/Database/RealmContextFactory.cs | 65 ++++++++++++++---------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0b0263a16b..6214f4ca81 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -76,10 +76,26 @@ namespace osu.Game.Database return new RealmWriteUsage(createContext(), writeComplete); } - private void writeComplete() + /// + /// Flush any active contexts and block any further writes. + /// + /// + /// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm. + /// ie. to move the realm backing file to a new location. + /// + /// An which should be disposed to end the blocking section. + /// Thrown if this context is already disposed. + public IDisposable BlockAllOperations() { - Monitor.Exit(writeLock); - pending_writes.Value--; + if (IsDisposed) + throw new InvalidOperationException(@"Attempted to block operations after already disposed."); + + blockingLock.Wait(); + flushContexts(); + + return new InvokeOnDisposal(this, endBlockingSection); + + static void endBlockingSection(RealmContextFactory factory) => factory.blockingLock.Release(); } protected override void Update() @@ -113,6 +129,12 @@ namespace osu.Game.Database } } + private void writeComplete() + { + Monitor.Exit(writeLock); + pending_writes.Value--; + } + private void onMigration(Migration migration, ulong lastSchemaVersion) { switch (lastSchemaVersion) @@ -124,31 +146,6 @@ namespace osu.Game.Database } } - protected override void Dispose(bool isDisposing) - { - if (!IsDisposed) - { - // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. - BlockAllOperations(); - blockingLock?.Dispose(); - } - - base.Dispose(isDisposing); - } - - public IDisposable BlockAllOperations() - { - if (IsDisposed) - throw new InvalidOperationException(@"Attempted to block operations after already disposed."); - - blockingLock.Wait(); - flushContexts(); - - return new InvokeOnDisposal(this, endBlockingSection); - - static void endBlockingSection(RealmContextFactory factory) => factory.blockingLock.Release(); - } - private void flushContexts() { var previousContext = context; @@ -161,6 +158,18 @@ namespace osu.Game.Database previousContext?.Dispose(); } + protected override void Dispose(bool isDisposing) + { + if (!IsDisposed) + { + // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. + BlockAllOperations(); + blockingLock?.Dispose(); + } + + base.Dispose(isDisposing); + } + /// /// A usage of realm from an arbitrary thread. /// From d4ea73d727ec94ed118e3279bc8752758b9a73bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Jun 2021 16:17:09 +0900 Subject: [PATCH 668/670] Simplify disposal exceptions --- osu.Game/Database/RealmContextFactory.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 6214f4ca81..71617b258d 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -84,11 +84,10 @@ namespace osu.Game.Database /// ie. to move the realm backing file to a new location. /// /// An which should be disposed to end the blocking section. - /// Thrown if this context is already disposed. public IDisposable BlockAllOperations() { if (IsDisposed) - throw new InvalidOperationException(@"Attempted to block operations after already disposed."); + throw new ObjectDisposedException(nameof(RealmContextFactory)); blockingLock.Wait(); flushContexts(); @@ -111,7 +110,7 @@ namespace osu.Game.Database try { if (IsDisposed) - throw new InvalidOperationException(@"Attempted to retrieve a context after the factor has already been disposed."); + throw new ObjectDisposedException(nameof(RealmContextFactory)); blockingLock.Wait(); From b5c3c9d55099a47d775d51ce5e64ed4d554a3244 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 28 Jun 2021 17:37:58 +0900 Subject: [PATCH 669/670] Disable realm analytics submission --- FodyWeavers.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FodyWeavers.xml b/FodyWeavers.xml index cc07b89533..ea490e3297 100644 --- a/FodyWeavers.xml +++ b/FodyWeavers.xml @@ -1,3 +1,3 @@  - + \ No newline at end of file From e4ca6a4266ce8624e189e9409128b97bd02b44bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Jun 2021 01:55:09 +0900 Subject: [PATCH 670/670] Serialise and send ruleset ID as part of score submission --- osu.Game/Scoring/ScoreInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 3944c1d3de..4fd1d00fef 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -46,7 +46,7 @@ namespace osu.Game.Scoring [JsonIgnore] public int Combo { get; set; } // Todo: Shouldn't exist in here - [JsonIgnore] + [JsonProperty("ruleset_id")] public int RulesetID { get; set; } [JsonProperty("passed")]