From 81b07dee563b9ef01da5b8571061c09ccf0b417d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:41:33 +0300 Subject: [PATCH 001/306] Introduce `IExpandable` interface --- osu.Game/Overlays/IExpandable.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 osu.Game/Overlays/IExpandable.cs diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Overlays/IExpandable.cs new file mode 100644 index 0000000000..770ac97847 --- /dev/null +++ b/osu.Game/Overlays/IExpandable.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 osu.Framework.Graphics; + +namespace osu.Game.Overlays +{ + /// + /// An interface for drawables with ability to expand/contract. + /// + public interface IExpandable : IDrawable + { + /// + /// Whether this drawable is in an expanded state. + /// + BindableBool Expanded { get; } + } +} From 62a2bccd7655f78a66d74a7d77d4dec73f311c3a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:41:45 +0300 Subject: [PATCH 002/306] Abstractify expansion logic from `ExpandingButtonContainer` --- osu.Game/Overlays/ExpandingButtonContainer.cs | 136 ++---------------- .../Overlays/ExpandingControlContainer.cs | 124 ++++++++++++++++ osu.Game/Overlays/IExpandingContainer.cs | 16 +++ osu.Game/Overlays/SettingsPanel.cs | 2 +- 4 files changed, 150 insertions(+), 128 deletions(-) create mode 100644 osu.Game/Overlays/ExpandingControlContainer.cs create mode 100644 osu.Game/Overlays/IExpandingContainer.cs diff --git a/osu.Game/Overlays/ExpandingButtonContainer.cs b/osu.Game/Overlays/ExpandingButtonContainer.cs index 4eb8c47a1f..d7ff285707 100644 --- a/osu.Game/Overlays/ExpandingButtonContainer.cs +++ b/osu.Game/Overlays/ExpandingButtonContainer.cs @@ -1,141 +1,23 @@ // 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 osu.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Framework.Testing; -using osu.Framework.Threading; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osuTK; namespace osu.Game.Overlays { - public abstract class ExpandingButtonContainer : Container, IStateful + /// + /// An with a long hover expansion delay for buttons. + /// + /// + /// Mostly used for buttons with explanatory labels, in which the label would display after a "long hover". + /// + public class ExpandingButtonContainer : ExpandingControlContainer { - private readonly float contractedWidth; - private readonly float expandedWidth; - - public event Action StateChanged; - - protected override Container Content => FillFlow; - - protected FillFlowContainer FillFlow { get; } - protected ExpandingButtonContainer(float contractedWidth, float expandedWidth) + : base(contractedWidth, expandedWidth) { - this.contractedWidth = contractedWidth; - this.expandedWidth = expandedWidth; - - RelativeSizeAxes = Axes.Y; - Width = contractedWidth; - - InternalChildren = new Drawable[] - { - new SidebarScrollContainer - { - Children = new[] - { - FillFlow = new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - } - } - }, - }; } - private ScheduledDelegate expandEvent; - private ExpandedState state; - - protected override bool OnHover(HoverEvent e) - { - queueExpandIfHovering(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - expandEvent?.Cancel(); - hoveredButton = null; - State = ExpandedState.Contracted; - - base.OnHoverLost(e); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - queueExpandIfHovering(); - return base.OnMouseMove(e); - } - - private class SidebarScrollContainer : OsuScrollContainer - { - public SidebarScrollContainer() - { - RelativeSizeAxes = Axes.Both; - ScrollbarVisible = false; - } - } - - public ExpandedState State - { - get => state; - set - { - expandEvent?.Cancel(); - - if (state == value) return; - - state = value; - - switch (state) - { - default: - this.ResizeTo(new Vector2(contractedWidth, Height), 500, Easing.OutQuint); - break; - - case ExpandedState.Expanded: - this.ResizeTo(new Vector2(expandedWidth, Height), 500, Easing.OutQuint); - break; - } - - StateChanged?.Invoke(State); - } - } - - private Drawable hoveredButton; - - private void queueExpandIfHovering() - { - // if the same button is hovered, let the scheduled expand play out.. - if (hoveredButton?.IsHovered == true) - return; - - // ..otherwise check whether a new button is hovered, and if so, queue a new hover operation. - - // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way - // to handle cases like the editor where the buttons may be nested within a child hierarchy. - hoveredButton = FillFlow.ChildrenOfType().FirstOrDefault(c => c.IsHovered); - - expandEvent?.Cancel(); - - if (hoveredButton?.IsHovered == true && State != ExpandedState.Expanded) - expandEvent = Scheduler.AddDelayed(() => State = ExpandedState.Expanded, 750); - } - } - - public enum ExpandedState - { - Contracted, - Expanded, + protected override double HoverExpansionDelay => 750; } } diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs new file mode 100644 index 0000000000..8e02cab923 --- /dev/null +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -0,0 +1,124 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Framework.Testing; +using osu.Framework.Threading; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Overlays +{ + /// + /// Represents a with the ability to expand/contract when hovering the controls within it. + /// + /// The type of UI control to lookup for hover expansion. + public class ExpandingControlContainer : Container, IExpandingContainer + where TControl : class, IDrawable + { + private readonly float contractedWidth; + private readonly float expandedWidth; + + public BindableBool Expanded { get; } = new BindableBool(); + + /// + /// Delay before the container switches to expanded state from hover. + /// + protected virtual double HoverExpansionDelay => 0; + + protected override Container Content => FillFlow; + + protected FillFlowContainer FillFlow { get; } + + protected ExpandingControlContainer(float contractedWidth, float expandedWidth) + { + this.contractedWidth = contractedWidth; + this.expandedWidth = expandedWidth; + + RelativeSizeAxes = Axes.Y; + Width = contractedWidth; + + InternalChild = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = FillFlow = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }, + }; + } + + private ScheduledDelegate hoverExpandEvent; + private TControl activeControl; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(v => + { + this.ResizeWidthTo(v.NewValue ? expandedWidth : contractedWidth, 500, Easing.OutQuint); + }, true); + } + + protected override void Update() + { + base.Update(); + + // if the container was expanded from hovering over a control, we have to check per-frame whether we can contract it back. + // that's because contracting the container depends not only on whether it's no longer hovered, + // but also on whether the hovered control is no longer in a dragged state (if it was). + if (hoverExpandEvent != null && !IsHovered && (activeControl == null || !isControlActive(activeControl))) + { + hoverExpandEvent?.Cancel(); + + Expanded.Value = false; + hoverExpandEvent = null; + activeControl = null; + } + } + + protected override bool OnHover(HoverEvent e) + { + queueExpandIfHovering(); + return true; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + queueExpandIfHovering(); + return base.OnMouseMove(e); + } + + private void queueExpandIfHovering() + { + // if the same control is hovered or dragged, let the scheduled expand play out.. + if (activeControl != null && isControlActive(activeControl)) + return; + + // ..otherwise check whether a new control is hovered, and if so, queue a new hover operation. + hoverExpandEvent?.Cancel(); + + // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way + // to handle cases like the editor where the controls may be nested within a child hierarchy. + activeControl = FillFlow.ChildrenOfType().FirstOrDefault(isControlActive); + + if (activeControl != null && !Expanded.Value) + hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); + } + + /// + /// Whether the given control is currently active, by checking whether it's hovered or dragged. + /// + private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged; + } +} diff --git a/osu.Game/Overlays/IExpandingContainer.cs b/osu.Game/Overlays/IExpandingContainer.cs new file mode 100644 index 0000000000..ec5f0c90f4 --- /dev/null +++ b/osu.Game/Overlays/IExpandingContainer.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 osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Overlays +{ + /// + /// A target expanding container that should be resolved by children s to propagate state changes. + /// + [Cached(typeof(IExpandingContainer))] + public interface IExpandingContainer : IContainer, IExpandable + { + } +} diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index ba7118cffe..b11b6fde27 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -265,7 +265,7 @@ namespace osu.Game.Overlays return; SectionsContainer.ScrollTo(section); - Sidebar.State = ExpandedState.Contracted; + Sidebar.Expanded.Value = false; }, }; } From 326f12f8477da2838a368e921329d1ee726e4c44 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:43:05 +0300 Subject: [PATCH 003/306] Add `IExpandable` support for `SettingsToolboxGroup` --- osu.Game/Overlays/SettingsToolboxGroup.cs | 80 +++++++++++-------- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 2 +- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index ca0980a9c9..9e7223df9d 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; @@ -18,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public abstract class SettingsToolboxGroup : Container + public class SettingsToolboxGroup : Container, IExpandable { private const float transition_duration = 250; private const int container_width = 270; @@ -34,30 +35,7 @@ namespace osu.Game.Overlays private readonly FillFlowContainer content; private readonly IconButton button; - private bool expanded = true; - - public bool Expanded - { - get => expanded; - set - { - if (expanded == value) return; - - expanded = value; - - content.ClearTransforms(); - - if (expanded) - content.AutoSizeAxes = Axes.Y; - else - { - content.AutoSizeAxes = Axes.None; - content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); - } - - updateExpanded(); - } - } + public BindableBool Expanded { get; } = new BindableBool(true); private Color4 expandedColour; @@ -67,7 +45,7 @@ namespace osu.Game.Overlays /// Create a new instance. /// /// The title to be displayed in the header of this group. - protected SettingsToolboxGroup(string title) + public SettingsToolboxGroup(string title) { AutoSizeAxes = Axes.Y; Width = container_width; @@ -115,7 +93,7 @@ namespace osu.Game.Overlays Position = new Vector2(-15, 0), Icon = FontAwesome.Solid.Bars, Scale = new Vector2(0.75f), - Action = () => Expanded = !Expanded, + Action = () => Expanded.Toggle(), }, } }, @@ -155,23 +133,58 @@ namespace osu.Game.Overlays headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); } + [Resolved(canBeNull: true)] + private IExpandingContainer expandingContainer { get; set; } + + private bool expandedByContainer; + protected override void LoadComplete() { base.LoadComplete(); - this.Delay(600).FadeTo(inactive_alpha, fade_duration, Easing.OutQuint); - updateExpanded(); + expandingContainer?.Expanded.BindValueChanged(containerExpanded => + { + if (containerExpanded.NewValue && !Expanded.Value) + { + Expanded.Value = true; + expandedByContainer = true; + } + else if (!containerExpanded.NewValue && expandedByContainer) + { + Expanded.Value = false; + expandedByContainer = false; + } + + updateActiveState(); + }, true); + + Expanded.BindValueChanged(v => + { + content.ClearTransforms(); + + if (v.NewValue) + content.AutoSizeAxes = Axes.Y; + else + { + content.AutoSizeAxes = Axes.None; + content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); + } + + button.FadeColour(Expanded.Value ? expandedColour : Color4.White, 200, Easing.InOutQuint); + }, true); + + this.Delay(600).Schedule(updateActiveState); } protected override bool OnHover(HoverEvent e) { - this.FadeIn(fade_duration, Easing.OutQuint); + updateActiveState(); return false; } protected override void OnHoverLost(HoverLostEvent e) { - this.FadeTo(inactive_alpha, fade_duration, Easing.OutQuint); + updateActiveState(); base.OnHoverLost(e); } @@ -181,7 +194,10 @@ namespace osu.Game.Overlays expandedColour = colours.Yellow; } - private void updateExpanded() => button.FadeColour(expanded ? expandedColour : Color4.White, 200, Easing.InOutQuint); + private void updateActiveState() + { + this.FadeTo(IsHovered || expandingContainer?.Expanded.Value == true ? 1 : inactive_alpha, fade_duration, Easing.OutQuint); + } protected override Container Content => content; diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index ffcbb06fb3..807b4989c7 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD //CollectionSettings = new CollectionSettings(), //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings(), - VisualSettings = new VisualSettings { Expanded = false } + VisualSettings = new VisualSettings { Expanded = { Value = false } } } }; } From f4c7a332c343b1d23df835914150fe85992ef7ee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:45:08 +0300 Subject: [PATCH 004/306] Add `IExpandable` support for `SettingsItem` --- osu.Game/Overlays/Settings/ISettingsItem.cs | 9 +- osu.Game/Overlays/Settings/SettingsItem.cs | 91 ++++++++++++++++----- 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index e7afa48502..fe21f0664a 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -2,12 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Graphics; namespace osu.Game.Overlays.Settings { - public interface ISettingsItem : IDrawable, IDisposable + /// + /// A non-generic interface for s. + /// + public interface ISettingsItem : IExpandable, IDisposable { + /// + /// Invoked when the setting value has changed. + /// event Action SettingChanged; } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index e709be1343..cc8d5b36d0 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Settings protected readonly FillFlowContainer FlowContent; - private SpriteText labelText; + private SpriteText label; private OsuTextFlowContainer warningText; @@ -42,21 +42,34 @@ namespace osu.Game.Overlays.Settings [Resolved] private OsuColour colours { get; set; } + private LocalisableString labelText; + public virtual LocalisableString LabelText { - get => labelText?.Text ?? string.Empty; + get => labelText; set { - if (labelText == null) - { - // construct lazily for cases where the label is not needed (may be provided by the Control). - FlowContent.Insert(-1, labelText = new OsuSpriteText()); + ensureLabelCreated(); - updateDisabled(); - } + labelText = value; + updateLabelText(); + } + } - labelText.Text = value; - updateLayout(); + private LocalisableString? contractedLabelText; + + /// + /// Text to be displayed in place of when this is in a contracted state. + /// + public LocalisableString? ContractedLabelText + { + get => contractedLabelText; + set + { + ensureLabelCreated(); + + contractedLabelText = value; + updateLabelText(); } } @@ -90,6 +103,10 @@ namespace osu.Game.Overlays.Settings set => controlWithCurrent.Current = value; } + public BindableBool Expanded { get; } = new BindableBool(true); + + public event Action SettingChanged; + public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); public IEnumerable Keywords { get; set; } @@ -101,8 +118,6 @@ namespace osu.Game.Overlays.Settings public bool FilteringActive { get; set; } - public event Action SettingChanged; - protected SettingsItem() { RelativeSizeAxes = Axes.X; @@ -151,23 +166,59 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.Centre, Origin = Anchor.Centre }); - updateLayout(); } } - private void updateLayout() - { - bool hasLabel = labelText != null && !string.IsNullOrEmpty(labelText.Text.ToString()); + [Resolved(canBeNull: true)] + private IExpandingContainer expandingContainer { get; set; } - // if the settings item is providing a label, the default value indicator should be centred vertically to the left of the label. + protected override void LoadComplete() + { + base.LoadComplete(); + + expandingContainer?.Expanded.BindValueChanged(containerExpanded => Expanded.Value = containerExpanded.NewValue, true); + + Expanded.BindValueChanged(v => + { + updateLabelText(); + + Control.FadeTo(v.NewValue ? 1 : 0, 500, Easing.OutQuint); + Control.BypassAutoSizeAxes = v.NewValue ? Axes.None : Axes.Both; + }, true); + + FinishTransforms(true); + } + + private void ensureLabelCreated() + { + if (label != null) + return; + + // construct lazily for cases where the label is not needed (may be provided by the Control). + FlowContent.Insert(-1, label = new OsuSpriteText()); + + updateDisabled(); + } + + private void updateLabelText() + { + if (label != null) + { + if (contractedLabelText is LocalisableString contractedText) + label.Text = Expanded.Value ? labelText : contractedText; + else + label.Text = labelText; + } + + // if the settings item is providing a non-empty label, the default value indicator should be centred vertically to the left of the label. // otherwise, it should be centred vertically to the left of the main control of the settings item. - defaultValueIndicatorContainer.Height = hasLabel ? labelText.DrawHeight : Control.DrawHeight; + defaultValueIndicatorContainer.Height = !string.IsNullOrEmpty(label?.Text.ToString()) ? label.DrawHeight : Control.DrawHeight; } private void updateDisabled() { - if (labelText != null) - labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; + if (label != null) + label.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } } } From 648e7f6bbc4883d591ed7c01994f511006caa770 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:47:11 +0300 Subject: [PATCH 005/306] Handle controls with dragging state wrapped in underlying components I'm not 100% sure about this approach but it'll do for now. --- osu.Game/Overlays/ExpandingControlContainer.cs | 2 +- osu.Game/Overlays/Settings/ISettingsItem.cs | 5 +++++ osu.Game/Overlays/Settings/SettingsItem.cs | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs index 8e02cab923..2accd63fb9 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -119,6 +119,6 @@ namespace osu.Game.Overlays /// /// Whether the given control is currently active, by checking whether it's hovered or dragged. /// - private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged; + private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is ISettingsItem item && item.IsControlDragged); } } diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index fe21f0664a..20e2f48f96 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -14,5 +14,10 @@ namespace osu.Game.Overlays.Settings /// Invoked when the setting value has changed. /// event Action SettingChanged; + + /// + /// Returns whether the UI control is currently in a dragged state. + /// + bool IsControlDragged { get; } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index cc8d5b36d0..29980dc5a8 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -105,6 +105,8 @@ namespace osu.Game.Overlays.Settings public BindableBool Expanded { get; } = new BindableBool(true); + public bool IsControlDragged => Control.IsDragged; + public event Action SettingChanged; public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); From 6b35c0fe01795d664fc3599085a4ec2aa17d614d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 18:39:58 +0300 Subject: [PATCH 006/306] Add test scene for `ExpandingControlContainer` --- .../TestSceneExpandingControlContainer.cs | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs new file mode 100644 index 0000000000..d75089ceac --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs @@ -0,0 +1,183 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneExpandingControlContainer : OsuManualInputManagerTestScene + { + private TestExpandingContainer container; + private SettingsToolboxGroup toolboxGroup; + + private SettingsSlider slider1; + private SettingsSlider slider2; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = container = new TestExpandingContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Height = 0.33f, + Child = toolboxGroup = new SettingsToolboxGroup("sliders") + { + RelativeSizeAxes = Axes.X, + Width = 1, + Children = new Drawable[] + { + slider1 = new SettingsSlider + { + Current = new BindableFloat + { + Default = 1.0f, + MinValue = 1.0f, + MaxValue = 10.0f, + Precision = 0.01f, + }, + }, + slider2 = new SettingsSlider + { + Current = new BindableDouble + { + Default = 1.0, + MinValue = 1.0, + MaxValue = 10.0, + Precision = 0.01, + }, + }, + } + } + }; + + slider1.Current.BindValueChanged(v => + { + slider1.LabelText = $"Slider One ({v.NewValue:0.##x})"; + slider1.ContractedLabelText = $"S. 1. ({v.NewValue:0.##x})"; + }, true); + + slider2.Current.BindValueChanged(v => + { + slider2.LabelText = $"Slider Two ({v.NewValue:N2})"; + slider2.ContractedLabelText = $"S. 2. ({v.NewValue:N2})"; + }, true); + }); + + [Test] + public void TestDisplay() + { + AddStep("switch to contracted", () => container.Expanded.Value = false); + AddStep("switch to expanded", () => container.Expanded.Value = true); + AddStep("set left origin", () => container.Origin = Anchor.CentreLeft); + AddStep("set centre origin", () => container.Origin = Anchor.Centre); + AddStep("set right origin", () => container.Origin = Anchor.CentreRight); + } + + /// + /// Tests hovering over controls expands the parenting container appropriately and does not contract until hover is lost from container. + /// + [Test] + public void TestHoveringControlExpandsContainer() + { + AddAssert("ensure container contracted", () => !container.Expanded.Value); + + AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); + AddAssert("container expanded", () => container.Expanded.Value); + AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("hover group top", () => InputManager.MoveMouseTo(toolboxGroup.ScreenSpaceDrawQuad.TopLeft + new Vector2(5))); + AddAssert("container still expanded", () => container.Expanded.Value); + AddAssert("controls still expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("container contracted", () => !container.Expanded.Value); + AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); + } + + /// + /// Tests dragging a UI control (e.g. ) outside its parenting container does not contract it until dragging is finished. + /// + [Test] + public void TestDraggingControlOutsideDoesntContractContainer() + { + AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); + AddAssert("container expanded", () => container.Expanded.Value); + + AddStep("hover slider nub", () => InputManager.MoveMouseTo(slider1.ChildrenOfType().Single())); + AddStep("hold slider nub", () => InputManager.PressButton(MouseButton.Left)); + AddStep("drag outside container", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("container still expanded", () => container.Expanded.Value); + + AddStep("release slider nub", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("container contracted", () => !container.Expanded.Value); + } + + /// + /// Tests expanding a container will expand underlying groups if contracted. + /// + [Test] + public void TestExpandingContainerExpandsContractedGroup() + { + AddStep("contract group", () => toolboxGroup.Expanded.Value = false); + + AddStep("expand container", () => container.Expanded.Value = true); + AddAssert("group expanded", () => toolboxGroup.Expanded.Value); + AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("contract container", () => container.Expanded.Value = false); + AddAssert("group contracted", () => !toolboxGroup.Expanded.Value); + AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); + } + + /// + /// Tests contracting a container does not contract underlying groups if expanded by user (i.e. by setting directly). + /// + [Test] + public void TestContractingContainerDoesntContractUserExpandedGroup() + { + AddAssert("ensure group expanded", () => toolboxGroup.Expanded.Value); + + AddStep("expand container", () => container.Expanded.Value = true); + AddAssert("group still expanded", () => toolboxGroup.Expanded.Value); + AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("contract container", () => container.Expanded.Value = false); + AddAssert("group still expanded", () => toolboxGroup.Expanded.Value); + AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); + } + + /// + /// Tests expanding a container via does not get contracted by losing hover. + /// + [Test] + public void TestExpandingContainerDoesntGetContractedByHover() + { + AddStep("expand container", () => container.Expanded.Value = true); + + AddStep("hover control", () => InputManager.MoveMouseTo(slider1)); + AddAssert("container still expanded", () => container.Expanded.Value); + + AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("container still expanded", () => container.Expanded.Value); + } + + private class TestExpandingContainer : ExpandingControlContainer + { + public TestExpandingContainer() + : base(120, 250) + { + } + } + } +} From b5e6352137d4254323d2888f308c81dc2ced3208 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 09:31:29 +0300 Subject: [PATCH 007/306] Revert `SettingsItem`-related changes --- osu.Game/Overlays/Settings/ISettingsItem.cs | 14 +--- osu.Game/Overlays/Settings/SettingsItem.cs | 91 +++++---------------- 2 files changed, 21 insertions(+), 84 deletions(-) diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index 20e2f48f96..e7afa48502 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -2,22 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Graphics; namespace osu.Game.Overlays.Settings { - /// - /// A non-generic interface for s. - /// - public interface ISettingsItem : IExpandable, IDisposable + public interface ISettingsItem : IDrawable, IDisposable { - /// - /// Invoked when the setting value has changed. - /// event Action SettingChanged; - - /// - /// Returns whether the UI control is currently in a dragged state. - /// - bool IsControlDragged { get; } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 29980dc5a8..e709be1343 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Settings protected readonly FillFlowContainer FlowContent; - private SpriteText label; + private SpriteText labelText; private OsuTextFlowContainer warningText; @@ -42,34 +42,21 @@ namespace osu.Game.Overlays.Settings [Resolved] private OsuColour colours { get; set; } - private LocalisableString labelText; - public virtual LocalisableString LabelText { - get => labelText; + get => labelText?.Text ?? string.Empty; set { - ensureLabelCreated(); + if (labelText == null) + { + // construct lazily for cases where the label is not needed (may be provided by the Control). + FlowContent.Insert(-1, labelText = new OsuSpriteText()); - labelText = value; - updateLabelText(); - } - } + updateDisabled(); + } - private LocalisableString? contractedLabelText; - - /// - /// Text to be displayed in place of when this is in a contracted state. - /// - public LocalisableString? ContractedLabelText - { - get => contractedLabelText; - set - { - ensureLabelCreated(); - - contractedLabelText = value; - updateLabelText(); + labelText.Text = value; + updateLayout(); } } @@ -103,12 +90,6 @@ namespace osu.Game.Overlays.Settings set => controlWithCurrent.Current = value; } - public BindableBool Expanded { get; } = new BindableBool(true); - - public bool IsControlDragged => Control.IsDragged; - - public event Action SettingChanged; - public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); public IEnumerable Keywords { get; set; } @@ -120,6 +101,8 @@ namespace osu.Game.Overlays.Settings public bool FilteringActive { get; set; } + public event Action SettingChanged; + protected SettingsItem() { RelativeSizeAxes = Axes.X; @@ -168,59 +151,23 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.Centre, Origin = Anchor.Centre }); + updateLayout(); } } - [Resolved(canBeNull: true)] - private IExpandingContainer expandingContainer { get; set; } - - protected override void LoadComplete() + private void updateLayout() { - base.LoadComplete(); + bool hasLabel = labelText != null && !string.IsNullOrEmpty(labelText.Text.ToString()); - expandingContainer?.Expanded.BindValueChanged(containerExpanded => Expanded.Value = containerExpanded.NewValue, true); - - Expanded.BindValueChanged(v => - { - updateLabelText(); - - Control.FadeTo(v.NewValue ? 1 : 0, 500, Easing.OutQuint); - Control.BypassAutoSizeAxes = v.NewValue ? Axes.None : Axes.Both; - }, true); - - FinishTransforms(true); - } - - private void ensureLabelCreated() - { - if (label != null) - return; - - // construct lazily for cases where the label is not needed (may be provided by the Control). - FlowContent.Insert(-1, label = new OsuSpriteText()); - - updateDisabled(); - } - - private void updateLabelText() - { - if (label != null) - { - if (contractedLabelText is LocalisableString contractedText) - label.Text = Expanded.Value ? labelText : contractedText; - else - label.Text = labelText; - } - - // if the settings item is providing a non-empty label, the default value indicator should be centred vertically to the left of the label. + // if the settings item is providing a label, the default value indicator should be centred vertically to the left of the label. // otherwise, it should be centred vertically to the left of the main control of the settings item. - defaultValueIndicatorContainer.Height = !string.IsNullOrEmpty(label?.Text.ToString()) ? label.DrawHeight : Control.DrawHeight; + defaultValueIndicatorContainer.Height = hasLabel ? labelText.DrawHeight : Control.DrawHeight; } private void updateDisabled() { - if (label != null) - label.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; + if (labelText != null) + labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } } } From 699826677006b6e7a230f061c505c53b2fbd73c6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 10:18:06 +0300 Subject: [PATCH 008/306] Add simplified implementation of an expandable slider --- osu.Game/Overlays/ExpandableSlider.cs | 123 ++++++++++++++++++++++++ osu.Game/Overlays/IExpandableControl.cs | 16 +++ 2 files changed, 139 insertions(+) create mode 100644 osu.Game/Overlays/ExpandableSlider.cs create mode 100644 osu.Game/Overlays/IExpandableControl.cs diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs new file mode 100644 index 0000000000..38faf44148 --- /dev/null +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -0,0 +1,123 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays +{ + /// + /// An implementation for the UI slider bar control. + /// + public class ExpandableSlider : CompositeDrawable, IExpandableControl, IHasCurrentValue + where T : struct, IEquatable, IComparable, IConvertible + where TSlider : OsuSliderBar, new() + { + private readonly OsuSpriteText label; + private readonly TSlider slider; + + private LocalisableString contractedLabelText; + + /// + /// The label text to display when this slider is in a contracted state. + /// + public LocalisableString ContractedLabelText + { + get => contractedLabelText; + set + { + if (value == contractedLabelText) + return; + + contractedLabelText = value; + + if (!Expanded.Value) + label.Text = value; + } + } + + private LocalisableString expandedLabelText; + + /// + /// The label text to display when this slider is in an expanded state. + /// + public LocalisableString ExpandedLabelText + { + get => expandedLabelText; + set + { + if (value == expandedLabelText) + return; + + expandedLabelText = value; + + if (Expanded.Value) + label.Text = value; + } + } + + public Bindable Current + { + get => slider.Current; + set => slider.Current = value; + } + + public BindableBool Expanded { get; } = new BindableBool(); + + public bool IsControlDragged => slider.IsDragged; + + public override bool HandlePositionalInput => true; + + public ExpandableSlider() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + label = new OsuSpriteText(), + slider = new TSlider(), + } + }; + } + + [Resolved(canBeNull: true)] + private IExpandingContainer expandingContainer { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + expandingContainer?.Expanded.BindValueChanged(containerExpanded => + { + Expanded.Value = containerExpanded.NewValue; + }, true); + + Expanded.BindValueChanged(v => + { + label.Text = v.NewValue ? expandedLabelText : contractedLabelText; + slider.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + slider.BypassAutoSizeAxes = v.NewValue ? Axes.Y : Axes.None; + }, true); + } + } + + /// + /// An implementation for the UI slider bar control. + /// + public class ExpandableSlider : ExpandableSlider> + where T : struct, IEquatable, IComparable, IConvertible + { + } +} diff --git a/osu.Game/Overlays/IExpandableControl.cs b/osu.Game/Overlays/IExpandableControl.cs new file mode 100644 index 0000000000..fae07ae23b --- /dev/null +++ b/osu.Game/Overlays/IExpandableControl.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. + +namespace osu.Game.Overlays +{ + /// + /// An interface for UI controls with the ability to expand/contract. + /// + public interface IExpandableControl : IExpandable + { + /// + /// Returns whether the UI control is currently in a dragged state. + /// + bool IsControlDragged { get; } + } +} From eb83b7fe0a912130641da4e56a9808ed18ce099b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 10:18:17 +0300 Subject: [PATCH 009/306] Update existing implementation with changes --- .../TestSceneExpandingControlContainer.cs | 15 +++++++-------- osu.Game/Overlays/ExpandingControlContainer.cs | 3 +-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs index d75089ceac..48089566cc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections; using osuTK; using osuTK.Input; @@ -20,8 +19,8 @@ namespace osu.Game.Tests.Visual.UserInterface private TestExpandingContainer container; private SettingsToolboxGroup toolboxGroup; - private SettingsSlider slider1; - private SettingsSlider slider2; + private ExpandableSlider slider1; + private ExpandableSlider slider2; [SetUp] public void SetUp() => Schedule(() => @@ -37,7 +36,7 @@ namespace osu.Game.Tests.Visual.UserInterface Width = 1, Children = new Drawable[] { - slider1 = new SettingsSlider + slider1 = new ExpandableSlider { Current = new BindableFloat { @@ -47,7 +46,7 @@ namespace osu.Game.Tests.Visual.UserInterface Precision = 0.01f, }, }, - slider2 = new SettingsSlider + slider2 = new ExpandableSlider { Current = new BindableDouble { @@ -63,13 +62,13 @@ namespace osu.Game.Tests.Visual.UserInterface slider1.Current.BindValueChanged(v => { - slider1.LabelText = $"Slider One ({v.NewValue:0.##x})"; + slider1.ExpandedLabelText = $"Slider One ({v.NewValue:0.##x})"; slider1.ContractedLabelText = $"S. 1. ({v.NewValue:0.##x})"; }, true); slider2.Current.BindValueChanged(v => { - slider2.LabelText = $"Slider Two ({v.NewValue:N2})"; + slider2.ExpandedLabelText = $"Slider Two ({v.NewValue:N2})"; slider2.ContractedLabelText = $"S. 2. ({v.NewValue:N2})"; }, true); }); @@ -172,7 +171,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("container still expanded", () => container.Expanded.Value); } - private class TestExpandingContainer : ExpandingControlContainer + private class TestExpandingContainer : ExpandingControlContainer { public TestExpandingContainer() : base(120, 250) diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs index 2accd63fb9..fb6a71ba97 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -9,7 +9,6 @@ using osu.Framework.Input.Events; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Containers; -using osu.Game.Overlays.Settings; namespace osu.Game.Overlays { @@ -119,6 +118,6 @@ namespace osu.Game.Overlays /// /// Whether the given control is currently active, by checking whether it's hovered or dragged. /// - private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is ISettingsItem item && item.IsControlDragged); + private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is IExpandableControl expandable && expandable.IsControlDragged); } } From 161ff45f8cb5e93d7cf2a02c5601344d9d35064c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 10:35:42 +0300 Subject: [PATCH 010/306] Resolve further UI-related issues --- osu.Game/Overlays/ExpandableSlider.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs index 38faf44148..524485d806 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Overlays { @@ -84,10 +85,14 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), Children = new Drawable[] { label = new OsuSpriteText(), - slider = new TSlider(), + slider = new TSlider + { + RelativeSizeAxes = Axes.X, + }, } }; } @@ -108,7 +113,7 @@ namespace osu.Game.Overlays { label.Text = v.NewValue ? expandedLabelText : contractedLabelText; slider.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); - slider.BypassAutoSizeAxes = v.NewValue ? Axes.Y : Axes.None; + slider.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; }, true); } } From 2cc69d6b19b2682db3a7c6ec8dd79ccc1421a0d1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 27 Jan 2022 15:57:56 +0300 Subject: [PATCH 011/306] Replace `IsControlDragged` with an abstract `ShouldBeExpanded` --- osu.Game/Overlays/ExpandableSlider.cs | 2 +- osu.Game/Overlays/ExpandingControlContainer.cs | 8 +++++++- osu.Game/Overlays/IExpandable.cs | 11 +++++++++++ osu.Game/Overlays/IExpandableControl.cs | 4 ---- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs index 524485d806..d346b9b22c 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays public BindableBool Expanded { get; } = new BindableBool(); - public bool IsControlDragged => slider.IsDragged; + bool IExpandable.ShouldBeExpanded => IsHovered || slider.IsDragged; public override bool HandlePositionalInput => true; diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs index fb6a71ba97..859e4bcd25 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -118,6 +118,12 @@ namespace osu.Game.Overlays /// /// Whether the given control is currently active, by checking whether it's hovered or dragged. /// - private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is IExpandableControl expandable && expandable.IsControlDragged); + private bool isControlActive(TControl control) + { + if (control is IExpandable expandable) + return expandable.ShouldBeExpanded; + + return control.IsHovered || control.IsDragged; + } } } diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Overlays/IExpandable.cs index 770ac97847..f998fc7b9f 100644 --- a/osu.Game/Overlays/IExpandable.cs +++ b/osu.Game/Overlays/IExpandable.cs @@ -3,6 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays { @@ -15,5 +16,15 @@ namespace osu.Game.Overlays /// Whether this drawable is in an expanded state. /// BindableBool Expanded { get; } + + /// + /// Whether this drawable should be/stay expanded by a parenting . + /// By default, this is when this drawable is in a hovered or dragged state. + /// + /// + /// This is defined for certain controls which may have a child handling dragging instead. + /// (e.g. in which dragging is handled by their underlying control). + /// + bool ShouldBeExpanded => IsHovered || IsDragged; } } diff --git a/osu.Game/Overlays/IExpandableControl.cs b/osu.Game/Overlays/IExpandableControl.cs index fae07ae23b..2cda6f467b 100644 --- a/osu.Game/Overlays/IExpandableControl.cs +++ b/osu.Game/Overlays/IExpandableControl.cs @@ -8,9 +8,5 @@ namespace osu.Game.Overlays /// public interface IExpandableControl : IExpandable { - /// - /// Returns whether the UI control is currently in a dragged state. - /// - bool IsControlDragged { get; } } } From bbef12e72c7bbaaf41c9a9b34b9d90ce5b4cfc2e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Feb 2022 05:45:12 +0300 Subject: [PATCH 012/306] Refactor `ExpandingControlContainer` to no longer rely on controls --- ...iner.cs => TestSceneExpandingContainer.cs} | 40 ++---------- osu.Game/Overlays/ExpandableSlider.cs | 4 +- osu.Game/Overlays/ExpandingButtonContainer.cs | 6 +- ...trolContainer.cs => ExpandingContainer.cs} | 64 ++++++------------- osu.Game/Overlays/IExpandable.cs | 11 ---- osu.Game/Overlays/IExpandableControl.cs | 12 ---- 6 files changed, 28 insertions(+), 109 deletions(-) rename osu.Game.Tests/Visual/UserInterface/{TestSceneExpandingControlContainer.cs => TestSceneExpandingContainer.cs} (74%) rename osu.Game/Overlays/{ExpandingControlContainer.cs => ExpandingContainer.cs} (54%) delete mode 100644 osu.Game/Overlays/IExpandableControl.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs similarity index 74% rename from osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs index 48089566cc..f63591311f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs @@ -1,20 +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.Linq; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections; using osuTK; -using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneExpandingControlContainer : OsuManualInputManagerTestScene + public class TestSceneExpandingContainer : OsuManualInputManagerTestScene { private TestExpandingContainer container; private SettingsToolboxGroup toolboxGroup; @@ -84,44 +80,22 @@ namespace osu.Game.Tests.Visual.UserInterface } /// - /// Tests hovering over controls expands the parenting container appropriately and does not contract until hover is lost from container. + /// Tests hovering expands the container and does not contract until hover is lost. /// [Test] - public void TestHoveringControlExpandsContainer() + public void TestHoveringExpandsContainer() { AddAssert("ensure container contracted", () => !container.Expanded.Value); - AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); + AddStep("hover container", () => InputManager.MoveMouseTo(container)); AddAssert("container expanded", () => container.Expanded.Value); AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); - AddStep("hover group top", () => InputManager.MoveMouseTo(toolboxGroup.ScreenSpaceDrawQuad.TopLeft + new Vector2(5))); - AddAssert("container still expanded", () => container.Expanded.Value); - AddAssert("controls still expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); - AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); AddAssert("container contracted", () => !container.Expanded.Value); AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); } - /// - /// Tests dragging a UI control (e.g. ) outside its parenting container does not contract it until dragging is finished. - /// - [Test] - public void TestDraggingControlOutsideDoesntContractContainer() - { - AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); - AddAssert("container expanded", () => container.Expanded.Value); - - AddStep("hover slider nub", () => InputManager.MoveMouseTo(slider1.ChildrenOfType().Single())); - AddStep("hold slider nub", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag outside container", () => InputManager.MoveMouseTo(Vector2.Zero)); - AddAssert("container still expanded", () => container.Expanded.Value); - - AddStep("release slider nub", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("container contracted", () => !container.Expanded.Value); - } - /// /// Tests expanding a container will expand underlying groups if contracted. /// @@ -157,21 +131,21 @@ namespace osu.Game.Tests.Visual.UserInterface } /// - /// Tests expanding a container via does not get contracted by losing hover. + /// Tests expanding a container via does not get contracted by losing hover. /// [Test] public void TestExpandingContainerDoesntGetContractedByHover() { AddStep("expand container", () => container.Expanded.Value = true); - AddStep("hover control", () => InputManager.MoveMouseTo(slider1)); + AddStep("hover container", () => InputManager.MoveMouseTo(container)); AddAssert("container still expanded", () => container.Expanded.Value); AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); AddAssert("container still expanded", () => container.Expanded.Value); } - private class TestExpandingContainer : ExpandingControlContainer + private class TestExpandingContainer : ExpandingContainer { public TestExpandingContainer() : base(120, 250) diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs index d346b9b22c..062de98659 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays /// /// An implementation for the UI slider bar control. /// - public class ExpandableSlider : CompositeDrawable, IExpandableControl, IHasCurrentValue + public class ExpandableSlider : CompositeDrawable, IExpandable, IHasCurrentValue where T : struct, IEquatable, IComparable, IConvertible where TSlider : OsuSliderBar, new() { @@ -72,8 +72,6 @@ namespace osu.Game.Overlays public BindableBool Expanded { get; } = new BindableBool(); - bool IExpandable.ShouldBeExpanded => IsHovered || slider.IsDragged; - public override bool HandlePositionalInput => true; public ExpandableSlider() diff --git a/osu.Game/Overlays/ExpandingButtonContainer.cs b/osu.Game/Overlays/ExpandingButtonContainer.cs index d7ff285707..8fb3e1b550 100644 --- a/osu.Game/Overlays/ExpandingButtonContainer.cs +++ b/osu.Game/Overlays/ExpandingButtonContainer.cs @@ -1,17 +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.Graphics.UserInterface; - namespace osu.Game.Overlays { /// - /// An with a long hover expansion delay for buttons. + /// An with a long hover expansion delay. /// /// /// Mostly used for buttons with explanatory labels, in which the label would display after a "long hover". /// - public class ExpandingButtonContainer : ExpandingControlContainer + public class ExpandingButtonContainer : ExpandingContainer { protected ExpandingButtonContainer(float contractedWidth, float expandedWidth) : base(contractedWidth, expandedWidth) diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingContainer.cs similarity index 54% rename from osu.Game/Overlays/ExpandingControlContainer.cs rename to osu.Game/Overlays/ExpandingContainer.cs index 859e4bcd25..ea3fffcb78 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingContainer.cs @@ -1,23 +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 System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Containers; namespace osu.Game.Overlays { /// - /// Represents a with the ability to expand/contract when hovering the controls within it. + /// Represents a with the ability to expand/contract on hover. /// - /// The type of UI control to lookup for hover expansion. - public class ExpandingControlContainer : Container, IExpandingContainer - where TControl : class, IDrawable + public class ExpandingContainer : Container, IExpandingContainer { private readonly float contractedWidth; private readonly float expandedWidth; @@ -33,7 +29,7 @@ namespace osu.Game.Overlays protected FillFlowContainer FillFlow { get; } - protected ExpandingControlContainer(float contractedWidth, float expandedWidth) + protected ExpandingContainer(float contractedWidth, float expandedWidth) { this.contractedWidth = contractedWidth; this.expandedWidth = expandedWidth; @@ -57,7 +53,6 @@ namespace osu.Game.Overlays } private ScheduledDelegate hoverExpandEvent; - private TControl activeControl; protected override void LoadComplete() { @@ -69,61 +64,38 @@ namespace osu.Game.Overlays }, true); } - protected override void Update() - { - base.Update(); - - // if the container was expanded from hovering over a control, we have to check per-frame whether we can contract it back. - // that's because contracting the container depends not only on whether it's no longer hovered, - // but also on whether the hovered control is no longer in a dragged state (if it was). - if (hoverExpandEvent != null && !IsHovered && (activeControl == null || !isControlActive(activeControl))) - { - hoverExpandEvent?.Cancel(); - - Expanded.Value = false; - hoverExpandEvent = null; - activeControl = null; - } - } - protected override bool OnHover(HoverEvent e) { - queueExpandIfHovering(); + updateHoverExpansion(); return true; } protected override bool OnMouseMove(MouseMoveEvent e) { - queueExpandIfHovering(); + updateHoverExpansion(); return base.OnMouseMove(e); } - private void queueExpandIfHovering() + protected override void OnHoverLost(HoverLostEvent e) { - // if the same control is hovered or dragged, let the scheduled expand play out.. - if (activeControl != null && isControlActive(activeControl)) + if (hoverExpandEvent != null) + { + hoverExpandEvent?.Cancel(); + hoverExpandEvent = null; + + Expanded.Value = false; return; + } - // ..otherwise check whether a new control is hovered, and if so, queue a new hover operation. - hoverExpandEvent?.Cancel(); - - // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way - // to handle cases like the editor where the controls may be nested within a child hierarchy. - activeControl = FillFlow.ChildrenOfType().FirstOrDefault(isControlActive); - - if (activeControl != null && !Expanded.Value) - hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); + base.OnHoverLost(e); } - /// - /// Whether the given control is currently active, by checking whether it's hovered or dragged. - /// - private bool isControlActive(TControl control) + private void updateHoverExpansion() { - if (control is IExpandable expandable) - return expandable.ShouldBeExpanded; + hoverExpandEvent?.Cancel(); - return control.IsHovered || control.IsDragged; + if (IsHovered && !Expanded.Value) + hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); } } } diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Overlays/IExpandable.cs index f998fc7b9f..770ac97847 100644 --- a/osu.Game/Overlays/IExpandable.cs +++ b/osu.Game/Overlays/IExpandable.cs @@ -3,7 +3,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays { @@ -16,15 +15,5 @@ namespace osu.Game.Overlays /// Whether this drawable is in an expanded state. /// BindableBool Expanded { get; } - - /// - /// Whether this drawable should be/stay expanded by a parenting . - /// By default, this is when this drawable is in a hovered or dragged state. - /// - /// - /// This is defined for certain controls which may have a child handling dragging instead. - /// (e.g. in which dragging is handled by their underlying control). - /// - bool ShouldBeExpanded => IsHovered || IsDragged; } } diff --git a/osu.Game/Overlays/IExpandableControl.cs b/osu.Game/Overlays/IExpandableControl.cs deleted file mode 100644 index 2cda6f467b..0000000000 --- a/osu.Game/Overlays/IExpandableControl.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. - -namespace osu.Game.Overlays -{ - /// - /// An interface for UI controls with the ability to expand/contract. - /// - public interface IExpandableControl : IExpandable - { - } -} From 176bb4a4e22901a37761ab6e1812663d3ec6c7dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Feb 2022 15:25:54 +0900 Subject: [PATCH 013/306] Update desktop projects to target .NET 6 --- .run/osu! (Second Client).run.xml | 8 ++++---- .../osu.Game.Rulesets.EmptyFreeform.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.EmptyScrolling.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 4 ++-- osu.Desktop/osu.Desktop.csproj | 2 +- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Mania.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Osu.Tests.csproj | 4 ++-- .../osu.Game.Rulesets.Taiko.Tests.csproj | 4 ++-- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- .../osu.Game.Tournament.Tests.csproj | 4 ++-- osu.iOS.props | 2 +- 14 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.run/osu! (Second Client).run.xml b/.run/osu! (Second Client).run.xml index 599b4b986b..9a471df902 100644 --- a/.run/osu! (Second Client).run.xml +++ b/.run/osu! (Second Client).run.xml @@ -1,8 +1,8 @@ - - \ No newline at end of file + diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index 3c6aaa39ca..cb922c5a58 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -20,7 +20,7 @@ WinExe - net5.0 + net6.0 osu.Game.Rulesets.EmptyFreeform.Tests - \ No newline at end of file + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 0719dd30df..5ecd9cc675 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -20,7 +20,7 @@ WinExe - net5.0 + net6.0 osu.Game.Rulesets.Pippidon.Tests - \ No newline at end of file + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index d0db43cc81..33ad0ac4f7 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -20,7 +20,7 @@ WinExe - net5.0 + net6.0 osu.Game.Rulesets.EmptyScrolling.Tests - \ No newline at end of file + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 0719dd30df..5ecd9cc675 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -20,7 +20,7 @@ WinExe - net5.0 + net6.0 osu.Game.Rulesets.Pippidon.Tests - \ No newline at end of file + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 89b9ffb94b..5e203af1f2 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 WinExe true A free-to-win rhythm game. Rhythm is just a *click* away! diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 57b914bee6..434c0e0367 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 Exe false diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 13f2e25f05..fc6d900567 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -9,9 +9,9 @@ WinExe - net5.0 + net6.0 - \ No newline at end of file + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index d51a6da4f9..ddad2adfea 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -9,9 +9,9 @@ WinExe - net5.0 + net6.0 - \ No newline at end of file + 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 fea2e408f6..bd4c3d3345 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 @@ -10,9 +10,9 @@ WinExe - net5.0 + net6.0 - \ No newline at end of file + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index ad3713e047..a6b8eb8651 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -9,9 +9,9 @@ WinExe - net5.0 + net6.0 - \ No newline at end of file + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 3b115d43e5..acf1e8470b 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -12,7 +12,7 @@ WinExe - net5.0 + net6.0 tests.ruleset diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 130fcfaca1..c7314a4969 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -11,7 +11,7 @@ WinExe - net5.0 + net6.0 @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/osu.iOS.props b/osu.iOS.props index 5978f6d685..7e5ab37257 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -63,7 +63,7 @@ - + $(NoWarn);NU1605 From 9cd88ec2b84d8460af461bd91deec5fc355ddced Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Feb 2022 21:23:38 +0300 Subject: [PATCH 014/306] Update API models with score pinning changes --- osu.Game/Online/API/Requests/GetUserScoresRequest.cs | 3 ++- osu.Game/Online/API/Requests/Responses/APIUser.cs | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs index 653abf7427..5d39799f6b 100644 --- a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs @@ -39,6 +39,7 @@ namespace osu.Game.Online.API.Requests { Best, Firsts, - Recent + Recent, + Pinned } } diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index e4a432b074..2b64e5de06 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -151,6 +151,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"scores_recent_count")] public int ScoresRecentCount; + [JsonProperty(@"scores_pinned_count")] + public int ScoresPinnedCount; + [JsonProperty(@"beatmap_playcounts_count")] public int BeatmapPlayCountsCount; From 4f7003928acb9a14a12adc02c343cb9329181ec5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 10 Feb 2022 21:32:07 +0300 Subject: [PATCH 015/306] Add score container for pinned scores in ranks section --- .../Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs | 3 +++ osu.Game/Overlays/Profile/Sections/RanksSection.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 5532e35cc5..5c67da1911 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -46,6 +46,9 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks case ScoreType.Recent: return user.ScoresRecentCount; + case ScoreType.Pinned: + return user.ScoresPinnedCount; + default: return 0; } diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 00a68d5bf9..02d8bd8c52 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -18,6 +18,8 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { + // todo: update to use UsersStrings.ShowExtraTopRanksPinnedTitle once that exists. + new PaginatedScoreContainer(ScoreType.Pinned, User, "Pinned Scores"), new PaginatedScoreContainer(ScoreType.Best, User, UsersStrings.ShowExtraTopRanksBestTitle), new PaginatedScoreContainer(ScoreType.Firsts, User, UsersStrings.ShowExtraTopRanksFirstTitle) }; From 9574bc13820665f5e6721915b3c05367ac9dccb7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:00:40 +0300 Subject: [PATCH 016/306] Allow `IRulesetInfo`s of same type to be comparable At first I was planning on making `CompareTo` implemented at `IRulesetInfo` itself and shared across classes, but turns out it only implements it explicitly and not allow direct `IRulesetInfo.Equals` calls. It messed with my head enough that I decided to just let each class have its own implementation and only allow same type. --- osu.Game/Rulesets/IRulesetInfo.cs | 2 +- osu.Game/Rulesets/RulesetInfo.cs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index 44731a2495..60a02212fc 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets /// /// A representation of a ruleset's metadata. /// - public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable + public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable { /// /// The user-exposed name of this ruleset. diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 0a0941d1ff..88e3988431 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] [MapTo("Ruleset")] - public class RulesetInfo : RealmObject, IEquatable, IRulesetInfo + public class RulesetInfo : RealmObject, IEquatable, IComparable, IRulesetInfo { [PrimaryKey] public string ShortName { get; set; } = string.Empty; @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets return ShortName == other.ShortName; } - public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b); + public bool Equals(IRulesetInfo? other) => other is RulesetInfo r && Equals(r); public int CompareTo(RulesetInfo other) { @@ -63,6 +63,14 @@ namespace osu.Game.Rulesets return string.Compare(ShortName, other.ShortName, StringComparison.Ordinal); } + public int CompareTo(IRulesetInfo other) + { + if (!(other is RulesetInfo ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(RulesetInfo)}.", nameof(other)); + + return CompareTo(ruleset); + } + public override int GetHashCode() { // Importantly, ignore the underlying realm hash code, as it will usually not match. From 1b729e891d53e8134efcd5ef8f09d5655c5bf20f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:01:10 +0300 Subject: [PATCH 017/306] Update pointless `CompareTo` implementation once again --- osu.Game/Rulesets/EFRulesetInfo.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index ba56adac49..4174aa773c 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] [Table(@"RulesetInfo")] - public sealed class EFRulesetInfo : IEquatable, IRulesetInfo + public sealed class EFRulesetInfo : IEquatable, IComparable, IRulesetInfo { public int? ID { get; set; } @@ -42,7 +42,15 @@ namespace osu.Game.Rulesets public bool Equals(EFRulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; - public int CompareTo(RulesetInfo other) => OnlineID.CompareTo(other.OnlineID); + public int CompareTo(EFRulesetInfo other) => OnlineID.CompareTo(other.OnlineID); + + public int CompareTo(IRulesetInfo other) + { + if (!(other is EFRulesetInfo ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(EFRulesetInfo)}.", nameof(other)); + + return CompareTo(ruleset); + } public override bool Equals(object obj) => obj is EFRulesetInfo rulesetInfo && Equals(rulesetInfo); From 26839f6ad8f3c49c3d1c63016b3af47e9afd2602 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:17:24 +0300 Subject: [PATCH 018/306] Consider `OnlineID`s during ruleset equality if available Required for `APIBeatmap`s, which provide `Ruleset` instances with `OnlineID` available only. Also consistent with the comparer implementation. --- osu.Game/Rulesets/RulesetInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 88e3988431..ba7c8d191d 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -44,6 +44,9 @@ namespace osu.Game.Rulesets if (ReferenceEquals(this, other)) return true; if (other == null) return false; + if (OnlineID >= 0 && other.OnlineID >= 0) + return OnlineID == other.OnlineID; + return ShortName == other.ShortName; } From 6f0e32826c6d90c92c1130cd7aa07894e3b7f38f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:06:28 +0300 Subject: [PATCH 019/306] Standardise ordering/grouping of `IRulesetInfo`/`RulesetInfo`s --- .../Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs | 2 +- osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs | 6 ++---- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Select/Carousel/SetPanelContent.cs | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs index 7753d8480a..eeb86f4702 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards bool firstGroup = true; - foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key)) + foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { if (!firstGroup) { diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 5b211084ab..5b467d67e2 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -62,10 +62,8 @@ namespace osu.Game.Beatmaps.Drawables // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 bool collapsed = beatmapSet.Beatmaps.Count() > 12; - foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key)) - { - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key, rulesetGrouping, collapsed)); - } + foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) + flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed)); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 5503a62ba2..2aec63fa65 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -851,7 +851,7 @@ namespace osu.Game.Screens.Edit var difficultyItems = new List(); - foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset.ShortName).OrderBy(group => group.Key)) + foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset).OrderBy(group => group.Key)) { if (difficultyItems.Count > 0) difficultyItems.Add(new EditorMenuItemSpacer()); diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 82523c9d9d..760915b528 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Select.Carousel var beatmaps = carouselSet.Beatmaps.ToList(); return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset.ShortName) + ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset) .Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset)) : beatmaps.Select(b => new FilterableDifficultyIcon(b)); } From c29cc78853f9cfb5efc4e923c7d9fca6fda8fbf7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 04:44:54 +0300 Subject: [PATCH 020/306] Fix failing test case --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index b429619044..9083415a78 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -457,10 +457,12 @@ namespace osu.Game.Tests.Visual.UserInterface public override ModType Type => ModType.Conversion; } - private class TestUnimplementedModOsuRuleset : OsuRuleset + private class TestUnimplementedModOsuRuleset : OsuRuleset, ILegacyRuleset { public override string ShortName => "unimplemented"; + int ILegacyRuleset.LegacyID => -1; + public override IEnumerable GetModsFor(ModType type) { if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); From 92e22c57a77a2860e2ddc78ee85da46f2e5f43d4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 08:02:51 +0300 Subject: [PATCH 021/306] Introduce private `APIRuleset` for online ID equality comparison --- .../API/Requests/Responses/APIBeatmap.cs | 26 ++++++++++++++++++- osu.Game/Rulesets/RulesetInfo.cs | 3 --- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index ebbac0dcab..dca60e54cb 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -98,7 +98,7 @@ namespace osu.Game.Online.API.Requests.Responses public string MD5Hash => Checksum; - public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID }; + public IRulesetInfo Ruleset => new APIRuleset { OnlineID = RulesetID }; [JsonIgnore] public string Hash => throw new NotImplementedException(); @@ -106,5 +106,29 @@ namespace osu.Game.Online.API.Requests.Responses #endregion public bool Equals(IBeatmapInfo? other) => other is APIBeatmap b && this.MatchesOnlineID(b); + + private class APIRuleset : IRulesetInfo + { + public int OnlineID { get; set; } = -1; + + public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})"; + public string ShortName => nameof(APIRuleset); + public string InstantiationInfo => string.Empty; + + public Ruleset CreateInstance() => throw new NotImplementedException(); + + public bool Equals(IRulesetInfo? other) => other is APIRuleset r && this.MatchesOnlineID(r); + + public int CompareTo(IRulesetInfo other) + { + if (!(other is APIRuleset ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(APIRuleset)}.", nameof(other)); + + return OnlineID.CompareTo(ruleset.OnlineID); + } + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => OnlineID; + } } } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index ba7c8d191d..88e3988431 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -44,9 +44,6 @@ namespace osu.Game.Rulesets if (ReferenceEquals(this, other)) return true; if (other == null) return false; - if (OnlineID >= 0 && other.OnlineID >= 0) - return OnlineID == other.OnlineID; - return ShortName == other.ShortName; } From b06caf2bf7b3a8705d26101f5e1fba464542a701 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 09:12:02 +0300 Subject: [PATCH 022/306] 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 147f576c55..1a2859c851 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 dd10807ec2..a9c0226951 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6fbc468586..5e0b264834 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From f049f175d5d2d3a971bce6825d8cdbd2fedea42d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 15:19:55 +0900 Subject: [PATCH 023/306] Revert "Fix failing test case" This reverts commit c29cc78853f9cfb5efc4e923c7d9fca6fda8fbf7. --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 9083415a78..b429619044 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -457,12 +457,10 @@ namespace osu.Game.Tests.Visual.UserInterface public override ModType Type => ModType.Conversion; } - private class TestUnimplementedModOsuRuleset : OsuRuleset, ILegacyRuleset + private class TestUnimplementedModOsuRuleset : OsuRuleset { public override string ShortName => "unimplemented"; - int ILegacyRuleset.LegacyID => -1; - public override IEnumerable GetModsFor(ModType type) { if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); From f012f64fd1f441c96c589035afd63010b41d0b1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Feb 2022 16:45:57 +0900 Subject: [PATCH 024/306] Add test coverage checking carousel panel visual state after ruleset filter change --- .../SongSelect/TestSceneBeatmapCarousel.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 4e46901e08..540b820250 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -41,6 +41,68 @@ namespace osu.Game.Tests.Visual.SongSelect this.rulesets = rulesets; } + [Test] + public void TestExternalRulesetChange() + { + createCarousel(new List()); + + AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(0), + AllowConvertedBeatmaps = true, + }, false)); + + AddStep("add mixed ruleset beatmapset", () => + { + var testMixed = TestResources.CreateTestBeatmapSetInfo(3); + + for (int i = 0; i <= 2; i++) + { + testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i); + } + + carousel.UpdateBeatmapSet(testMixed); + }); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1; + }); + + AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(1), + AllowConvertedBeatmaps = true, + }, false)); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 2 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 1) == 1; + }); + + AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(2), + AllowConvertedBeatmaps = true, + }, false)); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 2 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 2) == 1; + }); + } + [Test] public void TestScrollPositionMaintainedOnAdd() { From ccd664896185a0aff852fb8eaf6c47cb1b7bf125 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 11 Feb 2022 09:22:09 +0300 Subject: [PATCH 025/306] Update pinned score container header to use localised title --- osu.Game/Overlays/Profile/Sections/RanksSection.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 02d8bd8c52..f48e33dc12 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -18,8 +18,7 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - // todo: update to use UsersStrings.ShowExtraTopRanksPinnedTitle once that exists. - new PaginatedScoreContainer(ScoreType.Pinned, User, "Pinned Scores"), + new PaginatedScoreContainer(ScoreType.Pinned, User, UsersStrings.ShowExtraTopRanksPinnedTitle), new PaginatedScoreContainer(ScoreType.Best, User, UsersStrings.ShowExtraTopRanksBestTitle), new PaginatedScoreContainer(ScoreType.Firsts, User, UsersStrings.ShowExtraTopRanksFirstTitle) }; From beb3731c0b079c6b2bdaf1d051bd51b5350899f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 15:53:45 +0900 Subject: [PATCH 026/306] Standardise and combine base implementation of score submission requests These share too much yet have very different constructor signatures and property exposure. Just a clean-up pass as I begin to look at replay submission. --- .../Online/Rooms/SubmitRoomScoreRequest.cs | 32 ++------------- osu.Game/Online/Rooms/SubmitScoreRequest.cs | 41 +++++++++++++++++++ .../Online/Solo/SubmitSoloScoreRequest.cs | 33 ++------------- osu.Game/Screens/Play/RoomSubmittingPlayer.cs | 2 +- osu.Game/Screens/Play/SoloPlayer.cs | 2 +- 5 files changed, 51 insertions(+), 59 deletions(-) create mode 100644 osu.Game/Online/Rooms/SubmitScoreRequest.cs diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index e24d113822..39193be1af 100644 --- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -1,46 +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.Net.Http; -using Newtonsoft.Json; -using osu.Framework.IO.Network; -using osu.Game.Online.API; -using osu.Game.Online.Solo; using osu.Game.Scoring; namespace osu.Game.Online.Rooms { - public class SubmitRoomScoreRequest : APIRequest + public class SubmitRoomScoreRequest : SubmitScoreRequest { - private readonly long scoreId; private readonly long roomId; private readonly long playlistItemId; - private readonly SubmittableScore score; - public SubmitRoomScoreRequest(long scoreId, long roomId, long playlistItemId, ScoreInfo scoreInfo) + public SubmitRoomScoreRequest(ScoreInfo scoreInfo, long scoreId, long roomId, long playlistItemId) + : base(scoreInfo, scoreId) { - this.scoreId = scoreId; this.roomId = roomId; this.playlistItemId = playlistItemId; - score = new SubmittableScore(scoreInfo); } - protected override WebRequest CreateWebRequest() - { - var req = base.CreateWebRequest(); - - req.ContentType = "application/json"; - req.Method = HttpMethod.Put; - req.Timeout = 30000; - - req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - })); - - return req; - } - - protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{scoreId}"; + protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{ScoreId}"; } } diff --git a/osu.Game/Online/Rooms/SubmitScoreRequest.cs b/osu.Game/Online/Rooms/SubmitScoreRequest.cs new file mode 100644 index 0000000000..14f858f007 --- /dev/null +++ b/osu.Game/Online/Rooms/SubmitScoreRequest.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 System.Net.Http; +using Newtonsoft.Json; +using osu.Framework.IO.Network; +using osu.Game.Online.API; +using osu.Game.Online.Solo; +using osu.Game.Scoring; + +namespace osu.Game.Online.Rooms +{ + public abstract class SubmitScoreRequest : APIRequest + { + public readonly SubmittableScore Score; + + protected readonly long ScoreId; + + protected SubmitScoreRequest(ScoreInfo scoreInfo, long scoreId) + { + Score = new SubmittableScore(scoreInfo); + this.ScoreId = scoreId; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.ContentType = "application/json"; + req.Method = HttpMethod.Put; + req.Timeout = 30000; + + req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + })); + + return req; + } + } +} diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs index 78ebddb2e6..77fd7b813b 100644 --- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -1,46 +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.Net.Http; -using Newtonsoft.Json; -using osu.Framework.IO.Network; -using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; namespace osu.Game.Online.Solo { - public class SubmitSoloScoreRequest : APIRequest + public class SubmitSoloScoreRequest : SubmitScoreRequest { - public readonly SubmittableScore Score; - - private readonly long scoreId; - private readonly int beatmapId; - public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo) + public SubmitSoloScoreRequest(ScoreInfo scoreInfo, long scoreId, int beatmapId) + : base(scoreInfo, scoreId) { this.beatmapId = beatmapId; - this.scoreId = scoreId; - Score = new SubmittableScore(scoreInfo); } - protected override WebRequest CreateWebRequest() - { - var req = base.CreateWebRequest(); - - req.ContentType = "application/json"; - req.Method = HttpMethod.Put; - req.Timeout = 30000; - - req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - })); - - return req; - } - - protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{scoreId}"; + protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{ScoreId}"; } } diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs index 1002e7607f..fc96dfa965 100644 --- a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs +++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateSubmissionRequest(Score score, long token) { Debug.Assert(Room.RoomID.Value != null); - return new SubmitRoomScoreRequest(token, Room.RoomID.Value.Value, PlaylistItem.ID, score.ScoreInfo); + return new SubmitRoomScoreRequest(score.ScoreInfo, token, Room.RoomID.Value.Value, PlaylistItem.ID); } } } diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index eced2d142b..824c0072e3 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play Debug.Assert(beatmap.OnlineID > 0); - return new SubmitSoloScoreRequest(beatmap.OnlineID, token, score.ScoreInfo); + return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID); } } } From 908c31c68764370e44e9e3a6055aa881a7ce1971 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 16:02:25 +0900 Subject: [PATCH 027/306] Update stream read operations to use new helper methods --- osu.Game/Database/ImportTask.cs | 5 ++--- osu.Game/IO/Archives/ArchiveReader.cs | 14 +++----------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game/Database/ImportTask.cs b/osu.Game/Database/ImportTask.cs index cd9e396d13..d75c1a73e6 100644 --- a/osu.Game/Database/ImportTask.cs +++ b/osu.Game/Database/ImportTask.cs @@ -4,6 +4,7 @@ #nullable enable using System.IO; +using osu.Framework.Extensions; using osu.Game.IO.Archives; using osu.Game.Stores; using osu.Game.Utils; @@ -63,9 +64,7 @@ namespace osu.Game.Database if (!(stream is MemoryStream memoryStream)) { // This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out). - byte[] buffer = new byte[stream.Length]; - stream.Read(buffer, 0, (int)stream.Length); - memoryStream = new MemoryStream(buffer); + memoryStream = new MemoryStream(stream.ReadAllBytesToArray()); } if (ZipUtils.IsZipArchive(memoryStream)) diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs index 1d8da16c72..dab70eaf70 100644 --- a/osu.Game/IO/Archives/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using osu.Framework.Extensions; using osu.Framework.IO.Stores; namespace osu.Game.IO.Archives @@ -35,14 +36,7 @@ namespace osu.Game.IO.Archives public virtual byte[] Get(string name) { using (Stream input = GetStream(name)) - { - if (input == null) - return null; - - byte[] buffer = new byte[input.Length]; - input.Read(buffer); - return buffer; - } + return input?.ReadAllBytesToArray(); } public async Task GetAsync(string name, CancellationToken cancellationToken = default) @@ -52,9 +46,7 @@ namespace osu.Game.IO.Archives if (input == null) return null; - byte[] buffer = new byte[input.Length]; - await input.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - return buffer; + return await input.ReadAllBytesToArrayAsync(cancellationToken).ConfigureAwait(false); } } } From 6005daeba8bda373622123167bc533e2738704d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 16:02:42 +0900 Subject: [PATCH 028/306] Fix fire-and-forget async calls to use `WaitSafely` --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8f6ba6375f..7661cf671b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -829,7 +829,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID - }))); + })).WaitSafely()); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); @@ -860,11 +860,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID - }))); + })).WaitSafely()); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); - AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2)); + AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2).WaitSafely()); AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1); AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); From 28bbf34b1495908b1e145f8575f6ffaaf0979bd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Feb 2022 16:21:49 +0900 Subject: [PATCH 029/306] Remove unnecessary `this.` prefix --- osu.Game/Online/Rooms/SubmitScoreRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/SubmitScoreRequest.cs b/osu.Game/Online/Rooms/SubmitScoreRequest.cs index 14f858f007..b263262d2b 100644 --- a/osu.Game/Online/Rooms/SubmitScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitScoreRequest.cs @@ -19,7 +19,7 @@ namespace osu.Game.Online.Rooms protected SubmitScoreRequest(ScoreInfo scoreInfo, long scoreId) { Score = new SubmittableScore(scoreInfo); - this.ScoreId = scoreId; + ScoreId = scoreId; } protected override WebRequest CreateWebRequest() From 2ed3d5853144436f76260feb38f0d4d1c3a56e52 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 12 Feb 2022 08:51:09 +0800 Subject: [PATCH 030/306] Ignore short spinners for relax mod --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 5d191119b9..905d55c64e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -87,8 +87,9 @@ namespace osu.Game.Rulesets.Osu.Mods requiresHold |= slider.Ball.IsHovered || h.IsHovered; break; - case DrawableSpinner _: - requiresHold = true; + case DrawableSpinner spinner: + if (spinner.HitObject.SpinsRequired > 0) + requiresHold = true; break; } } From 053f41d755999ec120c1674c958cfadadb7898f7 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 12 Feb 2022 10:06:43 +0800 Subject: [PATCH 031/306] Simplify code --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 905d55c64e..10abd24e80 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -88,8 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSpinner spinner: - if (spinner.HitObject.SpinsRequired > 0) - requiresHold = true; + requiresHold = spinner.HitObject.SpinsRequired > 0; break; } } From 639d813d06fe2275b6220fc41b6133e0390de5f5 Mon Sep 17 00:00:00 2001 From: PercyDan <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 12 Feb 2022 11:15:03 +0800 Subject: [PATCH 032/306] Don't override previous value Co-authored-by: Salman Ahmed --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 10abd24e80..1bf63ef6d4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSpinner spinner: - requiresHold = spinner.HitObject.SpinsRequired > 0; + requiresHold |= spinner.HitObject.SpinsRequired > 0; break; } } From f1535b74beb185c83f76d7e7e3a54e55b6c32a81 Mon Sep 17 00:00:00 2001 From: Kaleb Date: Sun, 13 Feb 2022 02:16:06 -0500 Subject: [PATCH 033/306] Give Spun Out mod dynamic spin rate --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 098c639949..b900fa3274 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -45,7 +45,8 @@ namespace osu.Game.Rulesets.Osu.Mods // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; - spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f)); + float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / spinner.HitObject.Duration / 1.01); + spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); } } } From 585bd541f319ce316c8c4eef80aeb4247449b232 Mon Sep 17 00:00:00 2001 From: Kaleb Date: Sun, 13 Feb 2022 02:38:49 -0500 Subject: [PATCH 034/306] Add missing parentheses to RPM calculation --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index b900fa3274..4725a43a77 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Mods // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; - float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / spinner.HitObject.Duration / 1.01); + float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / (spinner.HitObject.Duration / 1.01)); spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); } } From a2c2b2bbb3dcbd87f9c4c08582b00b98f1096e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 17:52:59 +0100 Subject: [PATCH 035/306] Add flow for copying existing difficulty content --- osu.Game/Beatmaps/BeatmapManager.cs | 34 ++++++++------ .../Screens/Edit/CreateNewDifficultyDialog.cs | 45 +++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 20 ++++++++- osu.Game/Screens/Edit/EditorLoader.cs | 7 ++- .../Edit/NewDifficultyCreationParameters.cs | 36 +++++++++++++++ osu.Game/Tests/Visual/EditorTestScene.cs | 2 +- 6 files changed, 125 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs create mode 100644 osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 633eb8f15e..bd9cdba9fb 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -20,6 +20,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; +using osu.Game.Screens.Edit; using osu.Game.Skinning; using osu.Game.Stores; @@ -112,29 +113,36 @@ namespace osu.Game.Beatmaps /// The new difficulty will be backed by a model /// and represented by the returned . /// - public virtual WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + public virtual WorkingBeatmap CreateNewBlankDifficulty(NewDifficultyCreationParameters creationParameters) { - // fetch one of the existing difficulties to copy timing points and metadata from, - // so that the user doesn't have to fill all of that out again. - // this silently assumes that all difficulties have the same timing points and metadata, - // but cases where this isn't true seem rather rare / pathological. - var referenceBeatmap = GetWorkingBeatmap(beatmapSetInfo.Beatmaps.First()); + var referenceBeatmap = creationParameters.ReferenceBeatmap; + var targetBeatmapSet = creationParameters.BeatmapSet; - var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); + var newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); // populate circular beatmap set info <-> beatmap info references manually. // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` // rely on them being freely traversable in both directions for correct operation. - beatmapSetInfo.Beatmaps.Add(newBeatmapInfo); - newBeatmapInfo.BeatmapSet = beatmapSetInfo; + targetBeatmapSet.Beatmaps.Add(newBeatmapInfo); + newBeatmapInfo.BeatmapSet = targetBeatmapSet; - var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; - foreach (var timingPoint in referenceBeatmap.Beatmap.ControlPointInfo.TimingPoints) - newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); + IBeatmap newBeatmap; + + if (creationParameters.ClearAllObjects) + { + newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; + foreach (var timingPoint in referenceBeatmap.ControlPointInfo.TimingPoints) + newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); + } + else + { + newBeatmap = referenceBeatmap.Clone(); + newBeatmap.BeatmapInfo = newBeatmapInfo; + } beatmapModelManager.Save(newBeatmapInfo, newBeatmap); - workingBeatmapCache.Invalidate(beatmapSetInfo); + workingBeatmapCache.Invalidate(targetBeatmapSet); return GetWorkingBeatmap(newBeatmap.BeatmapInfo); } diff --git a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs new file mode 100644 index 0000000000..472f0e8948 --- /dev/null +++ b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs @@ -0,0 +1,45 @@ +// 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.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public class CreateNewDifficultyDialog : PopupDialog + { + /// + /// Delegate used to create new difficulties. + /// A value of in the clearAllObjects parameter + /// indicates that the new difficulty should have its hitobjects cleared; + /// otherwise, the new difficulty should be an exact copy of an existing one. + /// + public delegate void CreateNewDifficulty(bool clearAllObjects); + + public CreateNewDifficultyDialog(CreateNewDifficulty createNewDifficulty) + { + HeaderText = "Would you like to clear all objects?"; + + Icon = FontAwesome.Regular.Clone; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = "Yeah, let's start from scratch!", + Action = () => createNewDifficulty.Invoke(true) + }, + new PopupDialogCancelButton + { + Text = "No, create an exact copy of this difficulty", + Action = () => createNewDifficulty.Invoke(false) + }, + new PopupDialogCancelButton + { + Text = "I changed my mind, I want to keep editing this difficulty", + Action = () => { } + } + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2aec63fa65..c5578287e3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -841,7 +841,25 @@ namespace osu.Game.Screens.Edit } protected void CreateNewDifficulty(RulesetInfo rulesetInfo) - => loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo.BeatmapSet, rulesetInfo, GetState()); + { + if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset)) + { + switchToNewDifficulty(rulesetInfo, true); + return; + } + + dialogOverlay.Push(new CreateNewDifficultyDialog(clearAllObjects => switchToNewDifficulty(rulesetInfo, clearAllObjects))); + } + + private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool clearAllObjects) + => loader?.ScheduleSwitchToNewDifficulty(new NewDifficultyCreationParameters + { + BeatmapSet = editorBeatmap.BeatmapInfo.BeatmapSet, + Ruleset = rulesetInfo, + ReferenceBeatmap = playableBeatmap, + ClearAllObjects = clearAllObjects, + EditorState = GetState() + }); private EditorMenuItem createDifficultySwitchMenu() { diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index de47411fdc..169b601a94 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -11,7 +11,6 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -80,12 +79,12 @@ namespace osu.Game.Screens.Edit } } - public void ScheduleSwitchToNewDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo, EditorState editorState) + public void ScheduleSwitchToNewDifficulty(NewDifficultyCreationParameters creationParameters) => scheduleDifficultySwitch(() => { try { - return beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo); + return beatmapManager.CreateNewBlankDifficulty(creationParameters); } catch (Exception ex) { @@ -94,7 +93,7 @@ namespace osu.Game.Screens.Edit Logger.Error(ex, ex.Message); return Beatmap.Value; } - }, editorState); + }, creationParameters.EditorState); public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState) => scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState); diff --git a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs new file mode 100644 index 0000000000..dd03fd3644 --- /dev/null +++ b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.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 osu.Game.Beatmaps; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.Edit +{ + public class NewDifficultyCreationParameters + { + /// + /// The that should contain the newly-created difficulty. + /// + public BeatmapSetInfo BeatmapSet { get; set; } + + /// + /// The that the new difficulty should be playable for. + /// + public RulesetInfo Ruleset { get; set; } + + /// + /// A reference upon which the new difficulty should be based. + /// + public IBeatmap ReferenceBeatmap { get; set; } + + /// + /// Whether all objects should be cleared from the new difficulty. + /// + public bool ClearAllObjects { get; set; } + + /// + /// The saved state of the previous which should be restored upon opening the newly-created difficulty. + /// + public EditorState EditorState { get; set; } + } +} diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 331bf04644..ff09598eef 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); } - public override WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + public override WorkingBeatmap CreateNewBlankDifficulty(NewDifficultyCreationParameters creationParameters) { // don't actually care about properly creating a difficulty for this context. return TestBeatmap; From 0d1171b7fae94ea769598bb2b5f0dff194c6c32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 17:56:51 +0100 Subject: [PATCH 036/306] Adjust existing test coverage to pass --- .../Editing/TestSceneEditorBeatmapCreation.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index a14c9aded3..89a9307f9f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -10,6 +10,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; @@ -92,7 +93,7 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestCreateNewDifficulty() + public void TestCreateNewDifficulty([Values] bool sameRuleset) { string firstDifficultyName = Guid.NewGuid().ToString(); string secondDifficultyName = Guid.NewGuid().ToString(); @@ -111,7 +112,14 @@ namespace osu.Game.Tests.Visual.Editing }); AddAssert("can save again", () => Editor.Save()); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(sameRuleset ? new OsuRuleset().RulesetInfo : new CatchRuleset().RulesetInfo)); + + if (sameRuleset) + { + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction()); + } + AddUntilStep("wait for created", () => { string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; @@ -154,7 +162,7 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestCreateNewBeatmapFailsWithSameNamedDifficulties() + public void TestCreateNewBeatmapFailsWithSameNamedDifficulties([Values] bool sameRuleset) { Guid setId = Guid.Empty; const string duplicate_difficulty_name = "duplicate"; @@ -168,7 +176,14 @@ namespace osu.Game.Tests.Visual.Editing return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); }); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(sameRuleset ? new OsuRuleset().RulesetInfo : new CatchRuleset().RulesetInfo)); + + if (sameRuleset) + { + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction()); + } + AddUntilStep("wait for created", () => { string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; From eb939547a9978e0d4e47a65f90c3b5b902b7d988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 18:03:54 +0100 Subject: [PATCH 037/306] Add test coverage for difficulty copy flow --- .../Editing/TestSceneEditorBeatmapCreation.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 89a9307f9f..91f667d8b0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -9,13 +9,17 @@ using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; using osu.Game.Storyboards; using osu.Game.Tests.Resources; +using osuTK; using SharpCompress.Archives; using SharpCompress.Archives.Zip; @@ -99,6 +103,21 @@ namespace osu.Game.Tests.Visual.Editing string secondDifficultyName = Guid.NewGuid().ToString(); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] + { + new HitCircle + { + Position = new Vector2(0), + StartTime = 0 + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE, + StartTime = 1000 + } + })); + AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => { @@ -126,6 +145,80 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != firstDifficultyName; }); + AddAssert("created difficulty has timing point", () => + { + var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); + return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; + }); + AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); + var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + + return beatmap != null + && beatmap.DifficultyName == secondDifficultyName + && set != null + && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); + }); + } + + [Test] + public void TestCopyDifficulty() + { + string firstDifficultyName = Guid.NewGuid().ToString(); + string secondDifficultyName = Guid.NewGuid().ToString(); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] + { + new HitCircle + { + Position = new Vector2(0), + StartTime = 0 + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE, + StartTime = 1000 + } + })); + + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName); + var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + + return beatmap != null + && beatmap.DifficultyName == firstDifficultyName + && set != null + && set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID); + }); + AddAssert("can save again", () => Editor.Save()); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick()); + + AddUntilStep("wait for created", () => + { + string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != firstDifficultyName; + }); + + AddAssert("created difficulty has timing point", () => + { + var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); + return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; + }); + AddAssert("created difficulty has objects", () => EditorBeatmap.HitObjects.Count == 2); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => From fd1c8c361444f1bd7077379d64a1c03e5576c0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 18:26:19 +0100 Subject: [PATCH 038/306] Add failing test coverage for correct beatmap difficulty copy --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 91f667d8b0..672e643e60 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -187,6 +187,7 @@ namespace osu.Game.Tests.Visual.Editing StartTime = 1000 } })); + AddStep("set approach rate", () => EditorBeatmap.Difficulty.ApproachRate = 4); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => @@ -218,6 +219,7 @@ namespace osu.Game.Tests.Visual.Editing return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; }); AddAssert("created difficulty has objects", () => EditorBeatmap.HitObjects.Count == 2); + AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); From 1bf5375e746f6be78a992594fda76b821b72cb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 18:40:51 +0100 Subject: [PATCH 039/306] Fix `BeatmapInfo`-associated member not copying --- osu.Game/Beatmaps/BeatmapManager.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index bd9cdba9fb..c350cfc111 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -118,18 +118,12 @@ namespace osu.Game.Beatmaps var referenceBeatmap = creationParameters.ReferenceBeatmap; var targetBeatmapSet = creationParameters.BeatmapSet; - var newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); - - // populate circular beatmap set info <-> beatmap info references manually. - // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` - // rely on them being freely traversable in both directions for correct operation. - targetBeatmapSet.Beatmaps.Add(newBeatmapInfo); - newBeatmapInfo.BeatmapSet = targetBeatmapSet; - + BeatmapInfo newBeatmapInfo; IBeatmap newBeatmap; if (creationParameters.ClearAllObjects) { + newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; foreach (var timingPoint in referenceBeatmap.ControlPointInfo.TimingPoints) newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); @@ -137,9 +131,19 @@ namespace osu.Game.Beatmaps else { newBeatmap = referenceBeatmap.Clone(); - newBeatmap.BeatmapInfo = newBeatmapInfo; + newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone(); + // assign a new ID to the clone. + newBeatmapInfo.ID = Guid.NewGuid(); + // clear difficulty name to avoid clashes on save. + newBeatmapInfo.DifficultyName = string.Empty; } + // populate circular beatmap set info <-> beatmap info references manually. + // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` + // rely on them being freely traversable in both directions for correct operation. + targetBeatmapSet.Beatmaps.Add(newBeatmapInfo); + newBeatmapInfo.BeatmapSet = targetBeatmapSet; + beatmapModelManager.Save(newBeatmapInfo, newBeatmap); workingBeatmapCache.Invalidate(targetBeatmapSet); From 1292722a00e331933852f1842455a3e25108c4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 18:44:12 +0100 Subject: [PATCH 040/306] Add failing test coverage for correct combo colour copy --- .../Editing/TestSceneEditorBeatmapCreation.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 672e643e60..90b1d3a6f9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -6,6 +6,8 @@ using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -188,6 +190,16 @@ namespace osu.Game.Tests.Visual.Editing } })); AddStep("set approach rate", () => EditorBeatmap.Difficulty.ApproachRate = 4); + AddStep("set combo colours", () => + { + var beatmapSkin = EditorBeatmap.BeatmapSkin.AsNonNull(); + beatmapSkin.ComboColours.Clear(); + beatmapSkin.ComboColours.AddRange(new[] + { + new Colour4(255, 0, 0, 255), + new Colour4(0, 0, 255, 255) + }); + }); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => @@ -220,6 +232,7 @@ namespace osu.Game.Tests.Visual.Editing }); AddAssert("created difficulty has objects", () => EditorBeatmap.HitObjects.Count == 2); AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); + AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); From a144d6f8d67b0fe6c90021b3309682f323da9322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 18:50:11 +0100 Subject: [PATCH 041/306] Fix beatmap skin properties not copying --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 16 +++++---- .../Edit/NewDifficultyCreationParameters.cs | 34 ++++++++++++++++--- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c350cfc111..a9d0576696 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -144,7 +144,7 @@ namespace osu.Game.Beatmaps targetBeatmapSet.Beatmaps.Add(newBeatmapInfo); newBeatmapInfo.BeatmapSet = targetBeatmapSet; - beatmapModelManager.Save(newBeatmapInfo, newBeatmap); + beatmapModelManager.Save(newBeatmapInfo, newBeatmap, creationParameters.ReferenceBeatmapSkin); workingBeatmapCache.Invalidate(targetBeatmapSet); return GetWorkingBeatmap(newBeatmap.BeatmapInfo); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c5578287e3..37d4dce60e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -9,6 +9,7 @@ using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -853,13 +854,14 @@ namespace osu.Game.Screens.Edit private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool clearAllObjects) => loader?.ScheduleSwitchToNewDifficulty(new NewDifficultyCreationParameters - { - BeatmapSet = editorBeatmap.BeatmapInfo.BeatmapSet, - Ruleset = rulesetInfo, - ReferenceBeatmap = playableBeatmap, - ClearAllObjects = clearAllObjects, - EditorState = GetState() - }); + ( + editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull(), + rulesetInfo, + playableBeatmap, + editorBeatmap.BeatmapSkin, + clearAllObjects, + GetState() + )); private EditorMenuItem createDifficultySwitchMenu() { diff --git a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs index dd03fd3644..aa7dac609b 100644 --- a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs +++ b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs @@ -1,8 +1,11 @@ // 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 osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Skinning; namespace osu.Game.Screens.Edit { @@ -11,26 +14,47 @@ namespace osu.Game.Screens.Edit /// /// The that should contain the newly-created difficulty. /// - public BeatmapSetInfo BeatmapSet { get; set; } + public BeatmapSetInfo BeatmapSet { get; } /// /// The that the new difficulty should be playable for. /// - public RulesetInfo Ruleset { get; set; } + public RulesetInfo Ruleset { get; } /// /// A reference upon which the new difficulty should be based. /// - public IBeatmap ReferenceBeatmap { get; set; } + public IBeatmap ReferenceBeatmap { get; } + + /// + /// A reference that the new difficulty should base its own skin upon. + /// + public ISkin? ReferenceBeatmapSkin { get; } /// /// Whether all objects should be cleared from the new difficulty. /// - public bool ClearAllObjects { get; set; } + public bool ClearAllObjects { get; } /// /// The saved state of the previous which should be restored upon opening the newly-created difficulty. /// - public EditorState EditorState { get; set; } + public EditorState EditorState { get; } + + public NewDifficultyCreationParameters( + BeatmapSetInfo beatmapSet, + RulesetInfo ruleset, + IBeatmap referenceBeatmap, + ISkin? referenceBeatmapSkin, + bool clearAllObjects, + EditorState editorState) + { + BeatmapSet = beatmapSet; + Ruleset = ruleset; + ReferenceBeatmap = referenceBeatmap; + ReferenceBeatmapSkin = referenceBeatmapSkin; + ClearAllObjects = clearAllObjects; + EditorState = editorState; + } } } From 6fd663a718b30de3b3064edc4a986d4cc61dfa2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 19:51:02 +0100 Subject: [PATCH 042/306] Apply some renames to convey difference between creation options better --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs | 2 +- osu.Game/Screens/Edit/EditorLoader.cs | 2 +- .../Screens/Edit/NewDifficultyCreationParameters.cs | 13 +++++++++---- osu.Game/Tests/Visual/EditorTestScene.cs | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a9d0576696..87051ef650 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -113,7 +113,7 @@ namespace osu.Game.Beatmaps /// The new difficulty will be backed by a model /// and represented by the returned . /// - public virtual WorkingBeatmap CreateNewBlankDifficulty(NewDifficultyCreationParameters creationParameters) + public virtual WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters) { var referenceBeatmap = creationParameters.ReferenceBeatmap; var targetBeatmapSet = creationParameters.BeatmapSet; @@ -121,7 +121,7 @@ namespace osu.Game.Beatmaps BeatmapInfo newBeatmapInfo; IBeatmap newBeatmap; - if (creationParameters.ClearAllObjects) + if (creationParameters.CreateBlank) { newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; diff --git a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs index 472f0e8948..138e13bda1 100644 --- a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs +++ b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Edit public CreateNewDifficultyDialog(CreateNewDifficulty createNewDifficulty) { - HeaderText = "Would you like to clear all objects?"; + HeaderText = "Would you like to create a blank difficulty?"; Icon = FontAwesome.Regular.Clone; diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 169b601a94..be3e68c857 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.Edit { try { - return beatmapManager.CreateNewBlankDifficulty(creationParameters); + return beatmapManager.CreateNewDifficulty(creationParameters); } catch (Exception ex) { diff --git a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs index aa7dac609b..a6458a9456 100644 --- a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs +++ b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs @@ -32,9 +32,14 @@ namespace osu.Game.Screens.Edit public ISkin? ReferenceBeatmapSkin { get; } /// - /// Whether all objects should be cleared from the new difficulty. + /// Whether the new difficulty should be blank. /// - public bool ClearAllObjects { get; } + /// + /// A blank difficulty will have no objects, no control points other than timing points taken from + /// and will not share values with , + /// but it will share metadata and timing information with . + /// + public bool CreateBlank { get; } /// /// The saved state of the previous which should be restored upon opening the newly-created difficulty. @@ -46,14 +51,14 @@ namespace osu.Game.Screens.Edit RulesetInfo ruleset, IBeatmap referenceBeatmap, ISkin? referenceBeatmapSkin, - bool clearAllObjects, + bool createBlank, EditorState editorState) { BeatmapSet = beatmapSet; Ruleset = ruleset; ReferenceBeatmap = referenceBeatmap; ReferenceBeatmapSkin = referenceBeatmapSkin; - ClearAllObjects = clearAllObjects; + CreateBlank = createBlank; EditorState = editorState; } } diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index ff09598eef..8c8a106791 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); } - public override WorkingBeatmap CreateNewBlankDifficulty(NewDifficultyCreationParameters creationParameters) + public override WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters) { // don't actually care about properly creating a difficulty for this context. return TestBeatmap; From 90c48de9f8d68d2cdafcdde863ea671320449e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 20:01:47 +0100 Subject: [PATCH 043/306] Add failing test coverage for save of copied beatmap keeping old beatmap file --- .../Editing/TestSceneEditorBeatmapCreation.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 90b1d3a6f9..0025e88e62 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -12,6 +12,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; @@ -236,16 +237,24 @@ namespace osu.Game.Tests.Visual.Editing AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); + + BeatmapInfo refetchedBeatmap = null; + Live refetchedBeatmapSet = null; + + AddStep("refetch from database", () => + { + refetchedBeatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); + refetchedBeatmapSet = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + }); + AddAssert("new beatmap persisted", () => { - var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); - var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); - - return beatmap != null - && beatmap.DifficultyName == secondDifficultyName - && set != null - && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); + return refetchedBeatmap != null + && refetchedBeatmap.DifficultyName == secondDifficultyName + && refetchedBeatmapSet != null + && refetchedBeatmapSet.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); }); + AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2)); } [Test] From ecd6a68c6f0814cbd9a646f4dca4abf758162276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Feb 2022 20:05:32 +0100 Subject: [PATCH 044/306] Clear hash when creating copy of existing difficulty --- osu.Game/Beatmaps/BeatmapManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 87051ef650..4fa07ff518 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -136,6 +136,8 @@ namespace osu.Game.Beatmaps newBeatmapInfo.ID = Guid.NewGuid(); // clear difficulty name to avoid clashes on save. newBeatmapInfo.DifficultyName = string.Empty; + // also clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. + newBeatmapInfo.Hash = string.Empty; } // populate circular beatmap set info <-> beatmap info references manually. From 13abc392bd139305ac876df335139580ce2f4035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Feb 2022 18:54:52 +0100 Subject: [PATCH 045/306] Add failing test coverage for not copying online properties --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 0025e88e62..b4559ac9fa 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -201,6 +201,11 @@ namespace osu.Game.Tests.Visual.Editing new Colour4(0, 0, 255, 255) }); }); + AddStep("set status & online ID", () => + { + EditorBeatmap.BeatmapInfo.OnlineID = 123456; + EditorBeatmap.BeatmapInfo.Status = BeatmapOnlineStatus.WIP; + }); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => @@ -235,6 +240,9 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); + AddAssert("status not copied", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None); + AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); From 5dabc9282c0e1e3be5538cd4c7996d67eae6197e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Feb 2022 19:04:11 +0100 Subject: [PATCH 046/306] Change `BeatmapInfo` copy logic to be opt-in rather than opt-out --- osu.Game/Beatmaps/BeatmapManager.cs | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4fa07ff518..4dd0e08dab 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -131,13 +131,28 @@ namespace osu.Game.Beatmaps else { newBeatmap = referenceBeatmap.Clone(); - newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone(); - // assign a new ID to the clone. - newBeatmapInfo.ID = Guid.NewGuid(); - // clear difficulty name to avoid clashes on save. - newBeatmapInfo.DifficultyName = string.Empty; - // also clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. - newBeatmapInfo.Hash = string.Empty; + + var referenceBeatmapInfo = referenceBeatmap.BeatmapInfo; + newBeatmap.BeatmapInfo = newBeatmapInfo = new BeatmapInfo(referenceBeatmapInfo.Ruleset, referenceBeatmapInfo.Difficulty.Clone(), referenceBeatmapInfo.Metadata.DeepClone()) + { + // Only selected appropriate properties are copied over. + // Things like database ID, online status/ID, MD5 hash, star rating, etc. are omitted + // because they should not be copied over and/or they will be recomputed on save. + AudioLeadIn = referenceBeatmapInfo.AudioLeadIn, + StackLeniency = referenceBeatmapInfo.StackLeniency, + SpecialStyle = referenceBeatmapInfo.SpecialStyle, + LetterboxInBreaks = referenceBeatmapInfo.LetterboxInBreaks, + WidescreenStoryboard = referenceBeatmapInfo.WidescreenStoryboard, + EpilepsyWarning = referenceBeatmapInfo.EpilepsyWarning, + SamplesMatchPlaybackRate = referenceBeatmapInfo.SamplesMatchPlaybackRate, + DistanceSpacing = referenceBeatmapInfo.DistanceSpacing, + BeatDivisor = referenceBeatmapInfo.BeatDivisor, + GridSize = referenceBeatmapInfo.GridSize, + TimelineZoom = referenceBeatmapInfo.TimelineZoom, + Countdown = referenceBeatmapInfo.Countdown, + CountdownOffset = referenceBeatmapInfo.CountdownOffset, + Bookmarks = (int[])referenceBeatmapInfo.Bookmarks.Clone() + }; } // populate circular beatmap set info <-> beatmap info references manually. From df9535d195700205380bc624f2a9999cfa9e228c Mon Sep 17 00:00:00 2001 From: Kaleb Date: Sun, 13 Feb 2022 14:28:40 -0500 Subject: [PATCH 047/306] Update RPM calculation for readability Multiply the 1.01 factor to the resulting RPM, not to the duration. --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 4725a43a77..9be0dc748a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -45,7 +45,10 @@ namespace osu.Game.Rulesets.Osu.Mods // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; - float rotationSpeed = (float)(spinner.HitObject.SpinsRequired / (spinner.HitObject.Duration / 1.01)); + + // multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact, + // some spinners may not complete due to very minor decimal loss during calculation + float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f)); } } From c1777f20e114da75a51c6e3fc38ad5f637b4900d Mon Sep 17 00:00:00 2001 From: Kaleb Date: Mon, 14 Feb 2022 03:11:44 -0500 Subject: [PATCH 048/306] Fix Spun Out tests Change 'unaffected by mods' test to use dynamic RPM value instead of a fixed value --- .../Mods/TestSceneOsuModSpunOut.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 24e69703a6..29d7e7b4d6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -48,7 +48,19 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = () => { var counter = Player.ChildrenOfType().SingleOrDefault(); - return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1); + var spinner = Player.ChildrenOfType().FirstOrDefault(); + + if (counter == null || spinner == null) + return false; + + // ignore cases where the spinner hasn't started as these lead to false-positives + if (Precision.AlmostEquals(counter.Result.Value, 0, 1)) + return false; + + double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; + float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); + + return Precision.AlmostEquals(counter.Result.Value, rotationSpeed * 1000 * 60, 1); } }); } From 95b1bffffeceff678762ccb32e329a0b9277af6a Mon Sep 17 00:00:00 2001 From: Kaleb Date: Mon, 14 Feb 2022 03:45:02 -0500 Subject: [PATCH 049/306] Add test to ensure spinners only complete No bonus or a non-300 judgement --- .../Mods/TestSceneOsuModSpunOut.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 29d7e7b4d6..e71377a505 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -57,7 +58,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods if (Precision.AlmostEquals(counter.Result.Value, 0, 1)) return false; - double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); return Precision.AlmostEquals(counter.Result.Value, rotationSpeed * 1000 * 60, 1); @@ -65,6 +65,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods }); } + [Test] + public void TestSpinnerOnlyComplete() => CreateModTest(new ModTestData + { + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => + { + var spinner = Player.ChildrenOfType().SingleOrDefault(); + var gameplayClockContainer = Player.ChildrenOfType().SingleOrDefault(); + + if (spinner == null || gameplayClockContainer == null) + return false; + + if (!Precision.AlmostEquals(gameplayClockContainer.CurrentTime, spinner.HitObject.StartTime + spinner.HitObject.Duration, 200.0f)) + return false; + + return Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); + } + }); + private Beatmap singleSpinnerBeatmap => new Beatmap { HitObjects = new List From b9d9fc56afa5f579389201f8d06bfa41b3c3980b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 17:51:39 +0900 Subject: [PATCH 050/306] Move files to UI namespace --- .../Visual/UserInterface/TestSceneExpandingContainer.cs | 2 ++ .../Containers}/ExpandingButtonContainer.cs | 2 +- .../{Overlays => Graphics/Containers}/ExpandingContainer.cs | 3 +-- osu.Game/{Overlays => Graphics/Containers}/IExpandable.cs | 2 +- .../{Overlays => Graphics/Containers}/IExpandingContainer.cs | 3 ++- .../{Overlays => Graphics/UserInterface}/ExpandableSlider.cs | 4 ++-- osu.Game/Overlays/Settings/SettingsSidebar.cs | 1 + osu.Game/Overlays/SettingsToolboxGroup.cs | 1 + osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 9 files changed, 12 insertions(+), 8 deletions(-) rename osu.Game/{Overlays => Graphics/Containers}/ExpandingButtonContainer.cs (94%) rename osu.Game/{Overlays => Graphics/Containers}/ExpandingContainer.cs (97%) rename osu.Game/{Overlays => Graphics/Containers}/IExpandable.cs (93%) rename osu.Game/{Overlays => Graphics/Containers}/IExpandingContainer.cs (88%) rename osu.Game/{Overlays => Graphics/UserInterface}/ExpandableSlider.cs (97%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs index f63591311f..f4920b4412 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs @@ -4,6 +4,8 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections; using osuTK; diff --git a/osu.Game/Overlays/ExpandingButtonContainer.cs b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs similarity index 94% rename from osu.Game/Overlays/ExpandingButtonContainer.cs rename to osu.Game/Graphics/Containers/ExpandingButtonContainer.cs index 8fb3e1b550..b79af22bd2 100644 --- a/osu.Game/Overlays/ExpandingButtonContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs @@ -1,7 +1,7 @@ // 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 +namespace osu.Game.Graphics.Containers { /// /// An with a long hover expansion delay. diff --git a/osu.Game/Overlays/ExpandingContainer.cs b/osu.Game/Graphics/Containers/ExpandingContainer.cs similarity index 97% rename from osu.Game/Overlays/ExpandingContainer.cs rename to osu.Game/Graphics/Containers/ExpandingContainer.cs index ea3fffcb78..b50e008362 100644 --- a/osu.Game/Overlays/ExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingContainer.cs @@ -6,9 +6,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Threading; -using osu.Game.Graphics.Containers; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// Represents a with the ability to expand/contract on hover. diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Graphics/Containers/IExpandable.cs similarity index 93% rename from osu.Game/Overlays/IExpandable.cs rename to osu.Game/Graphics/Containers/IExpandable.cs index 770ac97847..593564a2f9 100644 --- a/osu.Game/Overlays/IExpandable.cs +++ b/osu.Game/Graphics/Containers/IExpandable.cs @@ -4,7 +4,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// An interface for drawables with ability to expand/contract. diff --git a/osu.Game/Overlays/IExpandingContainer.cs b/osu.Game/Graphics/Containers/IExpandingContainer.cs similarity index 88% rename from osu.Game/Overlays/IExpandingContainer.cs rename to osu.Game/Graphics/Containers/IExpandingContainer.cs index ec5f0c90f4..a82faa3cd1 100644 --- a/osu.Game/Overlays/IExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/IExpandingContainer.cs @@ -3,8 +3,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// A target expanding container that should be resolved by children s to propagate state changes. diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs similarity index 97% rename from osu.Game/Overlays/ExpandableSlider.cs rename to osu.Game/Graphics/UserInterface/ExpandableSlider.cs index 062de98659..60e83f9c81 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs @@ -8,11 +8,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osuTK; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.UserInterface { /// /// An implementation for the UI slider bar control. diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index e6ce90c33e..4e6a1eb914 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Settings { diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index 9e7223df9d..08321f68fe 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osuTK; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 92ea2db338..39783cc8bb 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -13,7 +13,7 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Overlays; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; From 3aa5908de8d0bd691c5a5839130e3ce66fabe107 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 18:01:56 +0900 Subject: [PATCH 051/306] Remove unused using statement --- osu.Game/Graphics/Containers/IExpandingContainer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/IExpandingContainer.cs b/osu.Game/Graphics/Containers/IExpandingContainer.cs index a82faa3cd1..eb186c96a8 100644 --- a/osu.Game/Graphics/Containers/IExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/IExpandingContainer.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; -using osu.Game.Overlays; namespace osu.Game.Graphics.Containers { From e324287f796128a722a4d94435b02b90f356076e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 18:08:16 +0900 Subject: [PATCH 052/306] Reduce expansion delay on `ExpandingButtonContainer` Felt too long. --- osu.Game/Graphics/Containers/ExpandingButtonContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs index b79af22bd2..859850e771 100644 --- a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs @@ -16,6 +16,6 @@ namespace osu.Game.Graphics.Containers { } - protected override double HoverExpansionDelay => 750; + protected override double HoverExpansionDelay => 400; } } From e304c031dcdffdcbd66f0f896ee160f79ee7a86d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 21:53:56 +0900 Subject: [PATCH 053/306] 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 1a2859c851..6b3142fbdc 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 a9c0226951..847832f8ac 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 4a1dc53281..00c9653146 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From c5019fefb0c9ed0a212167851acc082fe5f4e618 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 22:35:08 +0900 Subject: [PATCH 054/306] Update CI runs to target net6.0 --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c52802cf6..ec3816d541 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Install .NET 5.0.x + - name: Install .NET 6.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: "5.0.x" + dotnet-version: "6.0.x" # FIXME: libavformat is not included in Ubuntu. Let's fix that. # https://github.com/ppy/osu-framework/issues/4349 @@ -65,10 +65,10 @@ jobs: run: | $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2 - - name: Install .NET 5.0.x + - name: Install .NET 6.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: "5.0.x" + dotnet-version: "6.0.x" # Contrary to seemingly any other msbuild, msbuild running on macOS/Mono # cannot accept .sln(f) files as arguments. @@ -84,10 +84,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Install .NET 5.0.x + - name: Install .NET 6.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: "5.0.x" + dotnet-version: "6.0.x" # Contrary to seemingly any other msbuild, msbuild running on macOS/Mono # cannot accept .sln(f) files as arguments. @@ -102,17 +102,17 @@ jobs: - 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. + # FIXME: Tools won't run in .NET 6.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 + - name: Install .NET 6.0.x uses: actions/setup-dotnet@v1 with: - dotnet-version: "5.0.x" + dotnet-version: "6.0.x" - name: Restore Tools run: dotnet tool restore From 70ba6fb7dc2e48712886a6ae3ad4eb22079e4136 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 22:35:30 +0900 Subject: [PATCH 055/306] Update .NET version in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1dfcab416..7ace47a74f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir Please make sure you have the following prerequisites: -- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) installed. +- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed. - When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). - When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). - When running on Linux, please have a system-wide FFmpeg installation available to support video decoding. From 9b7d9d42bcd01f84375cc38d5f6bf4ab74f996a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 22:48:04 +0900 Subject: [PATCH 056/306] Update reference to `NetAnalyzers` --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 894ea25c8b..6e7015fa26 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ - + $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset From 9ad7b5d51caab9433c2eaaecbce6a1e4dc687516 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 22:52:30 +0900 Subject: [PATCH 057/306] Remove no longer required `NoWarn` spec --- Directory.Build.props | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6e7015fa26..c1682638c2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -32,13 +32,8 @@ NU1701: 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 - - 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 false From b581ca14cca29e7c8fb28bcfbb55e0aec0ae7389 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 02:32:38 +0900 Subject: [PATCH 058/306] Update usages in line with `BorderColour` type change --- osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs index deb2e6baf6..c6477d1781 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs @@ -114,7 +114,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 private class CircularBorderContainer : CircularContainer { - public void TransformBorderTo(SRGBColour colour) + public void TransformBorderTo(ColourInfo colour) => this.TransformTo(nameof(BorderColour), colour, 250, Easing.OutQuint); } } From 02f58a82fc4682df861a6069a12316d3bf474084 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 02:35:08 +0900 Subject: [PATCH 059/306] Use `WaitSafely()` in tests where it was not already being used --- .../Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs | 3 ++- .../Multiplayer/TestSceneMultiplayerPlaylist.cs | 10 +++++----- .../Multiplayer/TestSceneMultiplayerQueueList.cs | 12 ++++++------ .../Visual/Multiplayer/TestSceneTeamVersus.cs | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index c7eeff81fe..cd14a98751 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; @@ -71,7 +72,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers - })); + }).WaitSafely()); AddUntilStep("api room updated", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 936798e6b4..5dd9fb9fe2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Multiplayer importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); }); - AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); } [Test] @@ -97,19 +97,19 @@ namespace osu.Game.Tests.Visual.Multiplayer addItemStep(); addItemStep(); - AddStep("finish current item", () => Client.FinishCurrentItem()); + AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(1, 0); assertItemInQueueListStep(2, 0); assertItemInQueueListStep(3, 1); - AddStep("finish current item", () => Client.FinishCurrentItem()); + AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(2, 0); assertItemInHistoryListStep(1, 1); assertItemInQueueListStep(3, 0); - AddStep("finish current item", () => Client.FinishCurrentItem()); + AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(3, 0); assertItemInHistoryListStep(2, 1); @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestListsClearedWhenRoomLeft() { addItemStep(); - AddStep("finish current item", () => Client.FinishCurrentItem()); + AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); AddStep("leave room", () => RoomManager.PartRoom()); AddUntilStep("wait for room part", () => !RoomJoined); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index ddf794b437..80b20ed59f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -65,13 +65,13 @@ namespace osu.Game.Tests.Visual.Multiplayer importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); }); - AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); } [Test] public void TestDeleteButtonAlwaysVisibleForHost() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); addPlaylistItem(() => API.LocalUser.Value.OnlineID); @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); AddStep("join other user", () => Client.AddUser(new APIUser { Id = 1234 })); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestCurrentItemDoesNotHaveDeleteButton() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers })); + AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); addPlaylistItem(() => API.LocalUser.Value.OnlineID); @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Multiplayer assertDeleteButtonVisibility(0, false); assertDeleteButtonVisibility(1, true); - AddStep("finish current item", () => Client.FinishCurrentItem()); + AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); AddUntilStep("wait for next item to be selected", () => Client.Room?.Settings.PlaylistItemId == 2); AddUntilStep("wait for two items in playlist", () => playlist.ChildrenOfType().Count() == 2); @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapID = importedBeatmap.OnlineID, }); - Client.AddUserPlaylistItem(userId(), item); + Client.AddUserPlaylistItem(userId(), item).WaitSafely(); itemId = item.ID; }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 781f0a1824..2837d75553 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change match type", () => client.ChangeSettings(new MultiplayerRoomSettings { MatchType = MatchType.TeamVersus - })); + }).WaitSafely()); AddUntilStep("api room updated to team versus", () => client.APIRoom?.Type.Value == MatchType.TeamVersus); } From 8da0800d7fc70d213ab5401e97d537f308d75cf5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 02:47:35 +0900 Subject: [PATCH 060/306] Update `ChangeFocus` usage in line with framework changes --- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 3fd56ece58..27743e709f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -246,7 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { base.LoadComplete(); - Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextBox)); + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(passwordTextBox)); passwordTextBox.OnCommit += (_, __) => performJoin(); } From 4bd58cfde160398726aaffb80e93d630a34029f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 18:52:19 +0100 Subject: [PATCH 061/306] Update one more custom transform with `BorderColour` type change --- osu.Game/Overlays/Volume/MuteButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs index bcc9394aba..e9d3b31207 100644 --- a/osu.Game/Overlays/Volume/MuteButton.cs +++ b/osu.Game/Overlays/Volume/MuteButton.cs @@ -79,13 +79,13 @@ namespace osu.Game.Overlays.Volume protected override bool OnHover(HoverEvent e) { - Content.TransformTo, SRGBColour>("BorderColour", hoveredColour, 500, Easing.OutQuint); + Content.TransformTo, ColourInfo>("BorderColour", hoveredColour, 500, Easing.OutQuint); return false; } protected override void OnHoverLost(HoverLostEvent e) { - Content.TransformTo, SRGBColour>("BorderColour", unhoveredColour, 500, Easing.OutQuint); + Content.TransformTo, ColourInfo>("BorderColour", unhoveredColour, 500, Easing.OutQuint); } } } From db74a226c0b3e5d59493075b124b4dc1e8fdb0a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 02:54:45 +0900 Subject: [PATCH 062/306] Fix test regression due to mouse overlapping settings overlay --- osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs index e34ec6c46a..bbab6380ba 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs @@ -19,6 +19,10 @@ namespace osu.Game.Tests.Visual.Menus base.SetUpSteps(); AddAssert("no screen offset applied", () => Game.ScreenOffsetContainer.X == 0f); + + // avoids mouse interacting with settings overlay. + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for overlays", () => Game.Settings.IsLoaded && Game.Notifications.IsLoaded); } From 7e75fa7117ebabf0e5cb1c8ad1b5e5d79349047a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 19:52:08 +0100 Subject: [PATCH 063/306] Revert "Change `BeatmapInfo` copy logic to be opt-in rather than opt-out" This reverts commit 5dabc9282c0e1e3be5538cd4c7996d67eae6197e. --- osu.Game/Beatmaps/BeatmapManager.cs | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4dd0e08dab..4fa07ff518 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -131,28 +131,13 @@ namespace osu.Game.Beatmaps else { newBeatmap = referenceBeatmap.Clone(); - - var referenceBeatmapInfo = referenceBeatmap.BeatmapInfo; - newBeatmap.BeatmapInfo = newBeatmapInfo = new BeatmapInfo(referenceBeatmapInfo.Ruleset, referenceBeatmapInfo.Difficulty.Clone(), referenceBeatmapInfo.Metadata.DeepClone()) - { - // Only selected appropriate properties are copied over. - // Things like database ID, online status/ID, MD5 hash, star rating, etc. are omitted - // because they should not be copied over and/or they will be recomputed on save. - AudioLeadIn = referenceBeatmapInfo.AudioLeadIn, - StackLeniency = referenceBeatmapInfo.StackLeniency, - SpecialStyle = referenceBeatmapInfo.SpecialStyle, - LetterboxInBreaks = referenceBeatmapInfo.LetterboxInBreaks, - WidescreenStoryboard = referenceBeatmapInfo.WidescreenStoryboard, - EpilepsyWarning = referenceBeatmapInfo.EpilepsyWarning, - SamplesMatchPlaybackRate = referenceBeatmapInfo.SamplesMatchPlaybackRate, - DistanceSpacing = referenceBeatmapInfo.DistanceSpacing, - BeatDivisor = referenceBeatmapInfo.BeatDivisor, - GridSize = referenceBeatmapInfo.GridSize, - TimelineZoom = referenceBeatmapInfo.TimelineZoom, - Countdown = referenceBeatmapInfo.Countdown, - CountdownOffset = referenceBeatmapInfo.CountdownOffset, - Bookmarks = (int[])referenceBeatmapInfo.Bookmarks.Clone() - }; + newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone(); + // assign a new ID to the clone. + newBeatmapInfo.ID = Guid.NewGuid(); + // clear difficulty name to avoid clashes on save. + newBeatmapInfo.DifficultyName = string.Empty; + // also clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. + newBeatmapInfo.Hash = string.Empty; } // populate circular beatmap set info <-> beatmap info references manually. From 40cfee34211387866835612abf142b893b5498c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 19:54:40 +0100 Subject: [PATCH 064/306] Explicitly reset online ID and beatmap status on copy --- osu.Game/Beatmaps/BeatmapManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4fa07ff518..884209ee56 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -136,8 +136,11 @@ namespace osu.Game.Beatmaps newBeatmapInfo.ID = Guid.NewGuid(); // clear difficulty name to avoid clashes on save. newBeatmapInfo.DifficultyName = string.Empty; - // also clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. + // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. newBeatmapInfo.Hash = string.Empty; + // clear online properties. + newBeatmapInfo.OnlineID = -1; + newBeatmapInfo.Status = BeatmapOnlineStatus.None; } // populate circular beatmap set info <-> beatmap info references manually. From 1685e214d36463e225971c6ffef3fad73aae0735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 19:59:54 +0100 Subject: [PATCH 065/306] Adjust test coverage to cover desired copy naming scheme --- .../Editing/TestSceneEditorBeatmapCreation.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index b4559ac9fa..0a2f622da1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -172,10 +172,10 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestCopyDifficulty() { - string firstDifficultyName = Guid.NewGuid().ToString(); - string secondDifficultyName = Guid.NewGuid().ToString(); + string originalDifficultyName = Guid.NewGuid().ToString(); + string copyDifficultyName = $"{originalDifficultyName} (copy)"; - AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = originalDifficultyName); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { @@ -210,11 +210,11 @@ namespace osu.Game.Tests.Visual.Editing AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => { - var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName); + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == originalDifficultyName); var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); return beatmap != null - && beatmap.DifficultyName == firstDifficultyName + && beatmap.DifficultyName == originalDifficultyName && set != null && set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID); }); @@ -228,9 +228,10 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("wait for created", () => { string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; - return difficultyName != null && difficultyName != firstDifficultyName; + return difficultyName != null && difficultyName != originalDifficultyName; }); + AddAssert("created difficulty has copy suffix in name", () => EditorBeatmap.BeatmapInfo.DifficultyName == copyDifficultyName); AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -243,7 +244,6 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("status not copied", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None); AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1); - AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); BeatmapInfo refetchedBeatmap = null; @@ -251,16 +251,19 @@ namespace osu.Game.Tests.Visual.Editing AddStep("refetch from database", () => { - refetchedBeatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); + refetchedBeatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == copyDifficultyName); refetchedBeatmapSet = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); }); AddAssert("new beatmap persisted", () => { return refetchedBeatmap != null - && refetchedBeatmap.DifficultyName == secondDifficultyName + && refetchedBeatmap.DifficultyName == copyDifficultyName && refetchedBeatmapSet != null - && refetchedBeatmapSet.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); + && refetchedBeatmapSet.PerformRead(s => + s.Beatmaps.Count == 2 + && s.Beatmaps.Any(b => b.DifficultyName == originalDifficultyName) + && s.Beatmaps.Any(b => b.DifficultyName == copyDifficultyName)); }); AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2)); } From 62214471647382ff03bf1449db2c03bde8cfa9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 20:19:12 +0100 Subject: [PATCH 066/306] Append copy suffix on creating copy of difficulty --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Edit/EditorLoader.cs | 9 ++++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 884209ee56..d1b8e88743 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -134,8 +134,8 @@ namespace osu.Game.Beatmaps newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone(); // assign a new ID to the clone. newBeatmapInfo.ID = Guid.NewGuid(); - // clear difficulty name to avoid clashes on save. - newBeatmapInfo.DifficultyName = string.Empty; + // add "(copy)" suffix to difficulty name to avoid clashes on save. + newBeatmapInfo.DifficultyName += " (copy)"; // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. newBeatmapInfo.Hash = string.Empty; // clear online properties. diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 37d4dce60e..7a3c4f2a19 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -857,7 +857,7 @@ namespace osu.Game.Screens.Edit ( editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull(), rulesetInfo, - playableBeatmap, + editorBeatmap, editorBeatmap.BeatmapSkin, clearAllObjects, GetState() diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index be3e68c857..505a57f157 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -84,7 +84,14 @@ namespace osu.Game.Screens.Edit { try { - return beatmapManager.CreateNewDifficulty(creationParameters); + var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(creationParameters.ReferenceBeatmap.BeatmapInfo); + return beatmapManager.CreateNewDifficulty(new NewDifficultyCreationParameters( + refetchedBeatmap.BeatmapSetInfo, + refetchedBeatmap.BeatmapInfo.Ruleset, + refetchedBeatmap.Beatmap, + refetchedBeatmap.Skin, + creationParameters.CreateBlank, + creationParameters.EditorState)); } catch (Exception ex) { From e45a2ae0fc9b24af51454589be31923cb80543ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Feb 2022 20:56:05 +0100 Subject: [PATCH 067/306] Restructure difficulty copy flow to adapt to latest changes --- osu.Game/Beatmaps/BeatmapManager.cs | 83 +++++++++++-------- .../Screens/Edit/CreateNewDifficultyDialog.cs | 12 +-- osu.Game/Screens/Edit/Editor.cs | 27 ++---- osu.Game/Screens/Edit/EditorLoader.cs | 21 ++--- .../Edit/NewDifficultyCreationParameters.cs | 65 --------------- osu.Game/Tests/Visual/EditorTestScene.cs | 8 +- 6 files changed, 83 insertions(+), 133 deletions(-) delete mode 100644 osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d1b8e88743..777d5db2ad 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -20,7 +20,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using osu.Game.Screens.Edit; using osu.Game.Skinning; using osu.Game.Stores; @@ -109,55 +108,73 @@ namespace osu.Game.Beatmaps } /// - /// Add a new difficulty to the beatmap set represented by the provided . + /// Add a new difficulty to the provided based on the provided . /// The new difficulty will be backed by a model /// and represented by the returned . /// - public virtual WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters) + /// + /// Contrary to , this method does not preserve hitobjects and beatmap-level settings from . + /// The created beatmap will have zero hitobjects and will have default settings (including difficulty settings), but will preserve metadata and existing timing points. + /// + /// The to add the new difficulty to. + /// The to use as a baseline reference when creating the new difficulty. + /// The ruleset with which the new difficulty should be created. + public virtual WorkingBeatmap CreateNewDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap, RulesetInfo rulesetInfo) { - var referenceBeatmap = creationParameters.ReferenceBeatmap; - var targetBeatmapSet = creationParameters.BeatmapSet; + var playableBeatmap = referenceWorkingBeatmap.GetPlayableBeatmap(rulesetInfo); + var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), playableBeatmap.Metadata.DeepClone()); + var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; + foreach (var timingPoint in playableBeatmap.ControlPointInfo.TimingPoints) + newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); + + return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin); + } + + /// + /// Add a copy of the provided to the provided . + /// The new difficulty will be backed by a model + /// and represented by the returned . + /// + /// + /// Contrary to , this method creates a nearly-exact copy of + /// (with the exception of a few key properties that cannot be copied under any circumstance, like difficulty name, beatmap hash, or online status). + /// + /// The to add the copy to. + /// The to be copied. + public virtual WorkingBeatmap CopyExistingDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap) + { + var newBeatmap = referenceWorkingBeatmap.GetPlayableBeatmap(referenceWorkingBeatmap.BeatmapInfo.Ruleset).Clone(); BeatmapInfo newBeatmapInfo; - IBeatmap newBeatmap; - if (creationParameters.CreateBlank) - { - newBeatmapInfo = new BeatmapInfo(creationParameters.Ruleset, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); - newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; - foreach (var timingPoint in referenceBeatmap.ControlPointInfo.TimingPoints) - newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); - } - else - { - newBeatmap = referenceBeatmap.Clone(); - newBeatmap.BeatmapInfo = newBeatmapInfo = referenceBeatmap.BeatmapInfo.Clone(); - // assign a new ID to the clone. - newBeatmapInfo.ID = Guid.NewGuid(); - // add "(copy)" suffix to difficulty name to avoid clashes on save. - newBeatmapInfo.DifficultyName += " (copy)"; - // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. - newBeatmapInfo.Hash = string.Empty; - // clear online properties. - newBeatmapInfo.OnlineID = -1; - newBeatmapInfo.Status = BeatmapOnlineStatus.None; - } + newBeatmap.BeatmapInfo = newBeatmapInfo = referenceWorkingBeatmap.BeatmapInfo.Clone(); + // assign a new ID to the clone. + newBeatmapInfo.ID = Guid.NewGuid(); + // add "(copy)" suffix to difficulty name to avoid clashes on save. + newBeatmapInfo.DifficultyName += " (copy)"; + // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. + newBeatmapInfo.Hash = string.Empty; + // clear online properties. + newBeatmapInfo.OnlineID = -1; + newBeatmapInfo.Status = BeatmapOnlineStatus.None; + return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin); + } + + private WorkingBeatmap addDifficultyToSet(BeatmapSetInfo targetBeatmapSet, IBeatmap newBeatmap, ISkin beatmapSkin) + { // populate circular beatmap set info <-> beatmap info references manually. // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` // rely on them being freely traversable in both directions for correct operation. - targetBeatmapSet.Beatmaps.Add(newBeatmapInfo); - newBeatmapInfo.BeatmapSet = targetBeatmapSet; + targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo); + newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet; - beatmapModelManager.Save(newBeatmapInfo, newBeatmap, creationParameters.ReferenceBeatmapSkin); + beatmapModelManager.Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin); workingBeatmapCache.Invalidate(targetBeatmapSet); return GetWorkingBeatmap(newBeatmap.BeatmapInfo); } - // TODO: add back support for making a copy of another difficulty - // (likely via a separate `CopyDifficulty()` method). - /// /// Delete a beatmap difficulty. /// diff --git a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs index 138e13bda1..aa6ca280ee 100644 --- a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs +++ b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs @@ -10,11 +10,11 @@ namespace osu.Game.Screens.Edit { /// /// Delegate used to create new difficulties. - /// A value of in the clearAllObjects parameter - /// indicates that the new difficulty should have its hitobjects cleared; - /// otherwise, the new difficulty should be an exact copy of an existing one. + /// A value of in the createCopy parameter + /// indicates that the new difficulty should be an exact copy of an existing one; + /// otherwise, the new difficulty should have its hitobjects and beatmap-level settings cleared. /// - public delegate void CreateNewDifficulty(bool clearAllObjects); + public delegate void CreateNewDifficulty(bool createCopy); public CreateNewDifficultyDialog(CreateNewDifficulty createNewDifficulty) { @@ -27,12 +27,12 @@ namespace osu.Game.Screens.Edit new PopupDialogOkButton { Text = "Yeah, let's start from scratch!", - Action = () => createNewDifficulty.Invoke(true) + Action = () => createNewDifficulty.Invoke(false) }, new PopupDialogCancelButton { Text = "No, create an exact copy of this difficulty", - Action = () => createNewDifficulty.Invoke(false) + Action = () => createNewDifficulty.Invoke(true) }, new PopupDialogCancelButton { diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 7a3c4f2a19..c2775ae101 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -9,7 +9,6 @@ using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -359,14 +358,14 @@ namespace osu.Game.Screens.Edit /// /// Creates an instance representing the current state of the editor. /// - /// - /// The next beatmap to be shown, in the case of difficulty switch. + /// + /// The ruleset of the next beatmap to be shown, in the case of difficulty switch. /// indicates that the beatmap will not be changing. /// - public EditorState GetState([CanBeNull] BeatmapInfo nextBeatmap = null) => new EditorState + public EditorState GetState([CanBeNull] RulesetInfo nextRuleset = null) => new EditorState { Time = clock.CurrentTimeAccurate, - ClipboardContent = nextBeatmap == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextBeatmap.Ruleset.ShortName ? Clipboard.Content.Value : string.Empty + ClipboardContent = nextRuleset == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextRuleset.ShortName ? Clipboard.Content.Value : string.Empty }; /// @@ -845,23 +844,15 @@ namespace osu.Game.Screens.Edit { if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset)) { - switchToNewDifficulty(rulesetInfo, true); + switchToNewDifficulty(rulesetInfo, false); return; } - dialogOverlay.Push(new CreateNewDifficultyDialog(clearAllObjects => switchToNewDifficulty(rulesetInfo, clearAllObjects))); + dialogOverlay.Push(new CreateNewDifficultyDialog(createCopy => switchToNewDifficulty(rulesetInfo, createCopy))); } - private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool clearAllObjects) - => loader?.ScheduleSwitchToNewDifficulty(new NewDifficultyCreationParameters - ( - editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull(), - rulesetInfo, - editorBeatmap, - editorBeatmap.BeatmapSkin, - clearAllObjects, - GetState() - )); + private void switchToNewDifficulty(RulesetInfo rulesetInfo, bool createCopy) + => loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo, rulesetInfo, createCopy, GetState(rulesetInfo)); private EditorMenuItem createDifficultySwitchMenu() { @@ -886,7 +877,7 @@ namespace osu.Game.Screens.Edit return new EditorMenuItem("Change difficulty") { Items = difficultyItems }; } - protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap)); + protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); private void cancelExit() { diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 505a57f157..0a2b8437fa 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -4,6 +4,7 @@ using System; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -11,6 +12,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -79,19 +81,18 @@ namespace osu.Game.Screens.Edit } } - public void ScheduleSwitchToNewDifficulty(NewDifficultyCreationParameters creationParameters) + public void ScheduleSwitchToNewDifficulty(BeatmapInfo referenceBeatmapInfo, RulesetInfo rulesetInfo, bool createCopy, EditorState editorState) => scheduleDifficultySwitch(() => { try { - var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(creationParameters.ReferenceBeatmap.BeatmapInfo); - return beatmapManager.CreateNewDifficulty(new NewDifficultyCreationParameters( - refetchedBeatmap.BeatmapSetInfo, - refetchedBeatmap.BeatmapInfo.Ruleset, - refetchedBeatmap.Beatmap, - refetchedBeatmap.Skin, - creationParameters.CreateBlank, - creationParameters.EditorState)); + // fetch a fresh detached reference from database to avoid polluting model instances attached to cached working beatmaps. + var targetBeatmapSet = beatmapManager.QueryBeatmap(b => b.ID == referenceBeatmapInfo.ID).AsNonNull().BeatmapSet.AsNonNull(); + var referenceWorkingBeatmap = beatmapManager.GetWorkingBeatmap(referenceBeatmapInfo); + + return createCopy + ? beatmapManager.CopyExistingDifficulty(targetBeatmapSet, referenceWorkingBeatmap) + : beatmapManager.CreateNewDifficulty(targetBeatmapSet, referenceWorkingBeatmap, rulesetInfo); } catch (Exception ex) { @@ -100,7 +101,7 @@ namespace osu.Game.Screens.Edit Logger.Error(ex, ex.Message); return Beatmap.Value; } - }, creationParameters.EditorState); + }, editorState); public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState) => scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState); diff --git a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs b/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs deleted file mode 100644 index a6458a9456..0000000000 --- a/osu.Game/Screens/Edit/NewDifficultyCreationParameters.cs +++ /dev/null @@ -1,65 +0,0 @@ -// 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 osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Skinning; - -namespace osu.Game.Screens.Edit -{ - public class NewDifficultyCreationParameters - { - /// - /// The that should contain the newly-created difficulty. - /// - public BeatmapSetInfo BeatmapSet { get; } - - /// - /// The that the new difficulty should be playable for. - /// - public RulesetInfo Ruleset { get; } - - /// - /// A reference upon which the new difficulty should be based. - /// - public IBeatmap ReferenceBeatmap { get; } - - /// - /// A reference that the new difficulty should base its own skin upon. - /// - public ISkin? ReferenceBeatmapSkin { get; } - - /// - /// Whether the new difficulty should be blank. - /// - /// - /// A blank difficulty will have no objects, no control points other than timing points taken from - /// and will not share values with , - /// but it will share metadata and timing information with . - /// - public bool CreateBlank { get; } - - /// - /// The saved state of the previous which should be restored upon opening the newly-created difficulty. - /// - public EditorState EditorState { get; } - - public NewDifficultyCreationParameters( - BeatmapSetInfo beatmapSet, - RulesetInfo ruleset, - IBeatmap referenceBeatmap, - ISkin? referenceBeatmapSkin, - bool createBlank, - EditorState editorState) - { - BeatmapSet = beatmapSet; - Ruleset = ruleset; - ReferenceBeatmap = referenceBeatmap; - ReferenceBeatmapSkin = referenceBeatmapSkin; - CreateBlank = createBlank; - EditorState = editorState; - } - } -} diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 8c8a106791..24015590e2 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -136,7 +136,13 @@ namespace osu.Game.Tests.Visual return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); } - public override WorkingBeatmap CreateNewDifficulty(NewDifficultyCreationParameters creationParameters) + public override WorkingBeatmap CreateNewDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap, RulesetInfo rulesetInfo) + { + // don't actually care about properly creating a difficulty for this context. + return TestBeatmap; + } + + public override WorkingBeatmap CopyExistingDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap) { // don't actually care about properly creating a difficulty for this context. return TestBeatmap; From f5d0eb41cb1041c999921ada5c271411de00513f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 15 Feb 2022 02:42:37 +0300 Subject: [PATCH 068/306] Update further `ChangeFocus` usages --- osu.Game/Overlays/Login/LoginPanel.cs | 3 ++- osu.Game/Overlays/LoginOverlay.cs | 2 +- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index d1e5bfe809..481abd48ab 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -183,7 +183,8 @@ namespace osu.Game.Overlays.Login break; } - if (form != null) GetContainingInputManager()?.ChangeFocus(form); + if (form != null) + ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form)); }); public override bool AcceptsFocus => true; diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index f3562aa6d9..9b2d7ca1ee 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays panel.Bounding = true; this.FadeIn(transition_time, Easing.OutQuint); - GetContainingInputManager().ChangeFocus(panel); + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(panel)); } protected override void PopOut() diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index f0ca3e1bbc..571dfb3f6f 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Edit.Setup base.LoadComplete(); if (string.IsNullOrEmpty(ArtistTextBox.Current.Value)) - GetContainingInputManager().ChangeFocus(ArtistTextBox); + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(ArtistTextBox)); ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); From 60153bb69df75819e7e2830596884bbab8b04624 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 13:22:14 +0900 Subject: [PATCH 069/306] Update nuget packages to highest usable versions EF packages are intentionally pinned to 5.0.14 as higher versions no longer support `netstandard2.1`, which we require for xamarin projects. --- ...u.Game.Rulesets.EmptyFreeform.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- ....Game.Rulesets.EmptyScrolling.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- osu.Desktop/osu.Desktop.csproj | 9 +++++--- .../osu.Game.Benchmarks.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- .../osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- .../osu.Game.Tournament.Tests.csproj | 2 +- osu.Game/osu.Game.csproj | 22 +++++++++---------- 13 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index cb922c5a58..c305872288 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 5ecd9cc675..ec90885cd9 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index 33ad0ac4f7..f2e143a9c5 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 5ecd9cc675..ec90885cd9 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 5e203af1f2..b1117bf796 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,10 +26,13 @@ - + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 434c0e0367..2bdb6a650c 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index fc6d900567..a6c614f22f 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index ddad2adfea..ff49d6d4dd 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -4,7 +4,7 @@ - + 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 bd4c3d3345..a2e54f5cdc 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 @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index a6b8eb8651..debddae037 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index acf1e8470b..40969c8b29 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index c7314a4969..9fd73f2c1b 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 847832f8ac..7b50c804ff 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,27 +18,27 @@ - + - - + + - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + From 28b45fa8999ba897fa93c0f4a4a5fc1e3700a71e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 13:40:58 +0900 Subject: [PATCH 070/306] Add assertions against null reference for connection usages --- .../Multiplayer/OnlineMultiplayerClient.cs | 29 +++++++++++++++++++ .../Online/Spectator/OnlineSpectatorClient.cs | 11 +++++++ 2 files changed, 40 insertions(+) diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 3794bec228..ad898759ff 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -4,6 +4,7 @@ #nullable enable using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -79,6 +80,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.FromCanceled(new CancellationToken(true)); + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty); } @@ -87,6 +90,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.FromCanceled(new CancellationToken(true)); + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); } @@ -95,6 +100,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId); } @@ -103,6 +110,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.KickUser), userId); } @@ -111,6 +120,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings); } @@ -119,6 +130,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState); } @@ -127,6 +140,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability); } @@ -135,6 +150,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserMods), newMods); } @@ -143,6 +160,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.SendMatchRequest), request); } @@ -151,6 +170,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); } @@ -159,6 +180,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay)); } @@ -167,6 +190,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item); } @@ -175,6 +200,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.EditPlaylistItem), item); } @@ -183,6 +210,8 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 753796158e..ddde69c627 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; @@ -51,6 +52,8 @@ namespace osu.Game.Online.Spectator if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), state); } @@ -59,6 +62,8 @@ namespace osu.Game.Online.Spectator if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); } @@ -67,6 +72,8 @@ namespace osu.Game.Online.Spectator if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), state); } @@ -75,6 +82,8 @@ namespace osu.Game.Online.Spectator if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); } @@ -83,6 +92,8 @@ namespace osu.Game.Online.Spectator if (!IsConnected.Value) return Task.CompletedTask; + Debug.Assert(connection != null); + return connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId); } } From 8ec28dc8bcc1ab691af17e3124b046a6a27a1037 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 13:41:10 +0900 Subject: [PATCH 071/306] Update `OsuDbContext` in line with EF changes --- osu.Game/Database/OsuDbContext.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 441b090a6e..79183b6f0e 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -3,7 +3,6 @@ using System; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using osu.Framework.Logging; using osu.Framework.Statistics; @@ -12,8 +11,9 @@ using osu.Game.Configuration; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Scoring; -using LogLevel = Microsoft.Extensions.Logging.LogLevel; using osu.Game.Skinning; +using SQLitePCL; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace osu.Game.Database { @@ -40,10 +40,10 @@ namespace osu.Game.Database static OsuDbContext() { // required to initialise native SQLite libraries on some platforms. - SQLitePCL.Batteries_V2.Init(); + Batteries_V2.Init(); // https://github.com/aspnet/EntityFrameworkCore/issues/9994#issuecomment-508588678 - SQLitePCL.raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/); + raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/); } /// @@ -116,7 +116,6 @@ namespace osu.Game.Database optionsBuilder // this is required for the time being due to the way we are querying in places like BeatmapStore. // if we ever move to having consumers file their own .Includes, or get eager loading support, this could be re-enabled. - .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning)) .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10)) .UseLoggerFactory(logger.Value); } From 334fe1f1203ac02f507132bd1d64f00139dfb81d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 15:23:14 +0900 Subject: [PATCH 072/306] Add `AsSplitQuery` specification to avoid optimisation recommendation log messages --- osu.Game/Database/EFToRealmMigrator.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 0bb5388d55..c9deee19fe 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -215,7 +215,8 @@ namespace osu.Game.Database .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) .Include(s => s.Files).ThenInclude(f => f.FileInfo) - .Include(s => s.Metadata); + .Include(s => s.Metadata) + .AsSplitQuery(); log("Beginning beatmaps migration to realm"); @@ -344,7 +345,8 @@ namespace osu.Game.Database .Include(s => s.Ruleset) .Include(s => s.BeatmapInfo) .Include(s => s.Files) - .ThenInclude(f => f.FileInfo); + .ThenInclude(f => f.FileInfo) + .AsSplitQuery(); log("Beginning scores migration to realm"); @@ -434,6 +436,7 @@ namespace osu.Game.Database var existingSkins = db.SkinInfo .Include(s => s.Files) .ThenInclude(f => f.FileInfo) + .AsSplitQuery() .ToList(); // previous entries in EF are removed post migration. From 2c3e50a450359d76c95fc4628b391733dc354f4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 15:46:25 +0900 Subject: [PATCH 073/306] Update package references in xamarin `props` files --- osu.Android.props | 2 +- osu.iOS.props | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 6b3142fbdc..eab2be1b72 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -56,6 +56,6 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 00c9653146..03a105673c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -79,15 +79,15 @@ - - - + + + - + - - + + From efeba30b9fe0976fd99c89841029323f88c2f243 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 16:01:14 +0900 Subject: [PATCH 074/306] Remove ruleset and mod bindables from PlaylistItem --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 2 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 2 +- .../TestSceneDrawableRoomPlaylist.cs | 41 +++++++------- .../TestSceneMatchBeatmapDetailArea.cs | 11 ++-- .../Multiplayer/TestSceneMultiplayer.cs | 49 ++++++++-------- .../TestSceneMultiplayerMatchSubScreen.cs | 11 ++-- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 2 +- .../TestSceneMultiplayerPlaylist.cs | 4 +- .../TestSceneMultiplayerReadyButton.cs | 2 +- .../TestSceneMultiplayerSpectateButton.cs | 2 +- .../TestScenePlaylistsRoomSettingsPlaylist.cs | 11 ++-- .../TestScenePlaylistsSongSelect.cs | 19 ++++++- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 8 +-- .../TestScenePlaylistsResultsScreen.cs | 2 +- .../TestScenePlaylistsRoomCreation.cs | 8 +-- .../Online/Multiplayer/MultiplayerClient.cs | 34 ++++------- .../Online/Rooms/MultiplayerPlaylistItem.cs | 4 +- osu.Game/Online/Rooms/MultiplayerScore.cs | 6 +- osu.Game/Online/Rooms/PlaylistItem.cs | 56 ++----------------- .../OnlinePlay/Components/ModeTypeInfo.cs | 11 +++- .../OnlinePlay/Components/RoomManager.cs | 6 +- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 19 ++++--- .../Lounge/Components/RoomsContainer.cs | 3 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 +- .../Multiplayer/MultiplayerMatchSongSelect.cs | 5 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- .../OnlinePlay/OnlinePlaySongSelect.cs | 27 +++++---- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 6 +- .../Playlists/PlaylistsSongSelect.cs | 24 +++----- .../Multiplayer/MultiplayerTestScene.cs | 2 +- .../Visual/OnlinePlay/TestRoomManager.cs | 2 +- 31 files changed, 178 insertions(+), 207 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 343fc7e6e0..9aa04dda92 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Online selectedItem.Value = new PlaylistItem { Beatmap = { Value = testBeatmapInfo }, - Ruleset = { Value = testBeatmapInfo.Ruleset }, + RulesetID = testBeatmapInfo.Ruleset.OnlineID, }; Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index c79395b343..36d6c6a306 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = InitialBeatmap }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } })); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 5c8c90e166..659cc22350 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -215,25 +216,25 @@ namespace osu.Game.Tests.Visual.Multiplayer { ID = 0, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, Expired = true, - RequiredMods = + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }, new PlaylistItem { ID = 1, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } } } @@ -314,12 +315,12 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapSet = new BeatmapSetInfo() } }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } @@ -348,12 +349,12 @@ namespace osu.Game.Tests.Visual.Multiplayer ID = index++, OwnerID = 2, Beatmap = { Value = b }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 1d61a5d496..6144824ba0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -35,12 +36,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { ID = SelectedRoom.Value.Playlist.Count, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8f6ba6375f..41715f6cfb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -19,6 +19,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -99,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -235,7 +236,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -257,7 +258,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -287,7 +288,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -318,7 +319,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -340,7 +341,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -373,7 +374,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -393,7 +394,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -415,7 +416,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -454,7 +455,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -493,7 +494,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -532,7 +533,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -566,7 +567,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -606,7 +607,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -626,8 +627,8 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - AllowedMods = { new OsuModHidden() } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod(new OsuModHidden()) } } } }); @@ -666,7 +667,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -697,7 +698,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -715,7 +716,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { ID = 2, Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -743,7 +744,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -779,7 +780,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -818,7 +819,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -849,7 +850,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -882,7 +883,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 869fb17317..a6151198cf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -72,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -89,8 +90,8 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new TaikoRuleset().RulesetInfo }, - AllowedMods = { new TaikoModSwap() } + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod(new TaikoModSwap()) } }); }); @@ -112,7 +113,7 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -127,7 +128,7 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 73f2ed5b39..010e9dc078 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset } + RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }, Client.Room?.Users.ToArray())); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 936798e6b4..361178bfe4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -146,12 +146,12 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } + RulesetID = Ruleset.Value.OnlineID, }, new PlaylistItem { Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value }, + RulesetID = Ruleset.Value.OnlineID, Expired = true } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 9867e5225e..7834226f15 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer selectedItem.Value = new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }, + RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID }; if (button != null) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 42ae279667..70d4d9dd55 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer selectedItem.Value = new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }, + RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }; Child = new FillFlowContainer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index e63e58824f..8bfdda29d5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -161,12 +162,12 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapSet = new BeatmapSetInfo() } }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index d933491ab6..3333afc88b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -115,8 +115,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2); AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value)); - AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item 1 has rate 1.5", () => + { + var mod = (OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + return Precision.AlmostEquals(1.5, mod.SpeedChange.Value); + }); + + AddAssert("item 2 has rate 2", () => + { + var mod = (OsuModDoubleTime)SelectedRoom.Value.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset()); + return Precision.AlmostEquals(2, mod.SpeedChange.Value); + }); } /// @@ -138,7 +147,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2); - AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item has rate 1.5", () => + { + var m = (OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + return Precision.AlmostEquals(1.5, m.SpeedChange.Value); + }); } private class TestPlaylistsSongSelect : PlaylistsSongSelect diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 781f0a1824..513c1413fa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 11df115b1a..a05d01613c 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -168,7 +168,7 @@ namespace osu.Game.Tests.Visual.Playlists LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID })); }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 68225f6d64..578ea63b4e 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Playlists room.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedBeatmap.Beatmaps.First() }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Playlists room.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedBeatmap.Beatmaps.First() }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.Playlists room.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedBeatmap.Beatmaps.First() }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Playlists } } }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 903aaa89e3..9d45229961 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -727,30 +727,18 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); } - private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) + private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem { - var ruleset = Rulesets.GetRuleset(item.RulesetID); - - Debug.Assert(ruleset != null); - - var rulesetInstance = ruleset.CreateInstance(); - - var playlistItem = new PlaylistItem - { - ID = item.ID, - BeatmapID = item.BeatmapID, - OwnerID = item.OwnerID, - Ruleset = { Value = ruleset }, - Expired = item.Expired, - PlaylistOrder = item.PlaylistOrder, - PlayedAt = item.PlayedAt - }; - - playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); - playlistItem.AllowedMods.AddRange(item.AllowedMods.Select(m => m.ToMod(rulesetInstance))); - - return playlistItem; - } + ID = item.ID, + BeatmapID = item.BeatmapID, + OwnerID = item.OwnerID, + RulesetID = item.RulesetID, + Expired = item.Expired, + PlaylistOrder = item.PlaylistOrder, + PlayedAt = item.PlayedAt, + RequiredMods = item.RequiredMods.ToArray(), + AllowedMods = item.AllowedMods.ToArray() + }; /// /// Retrieves a from an online source. diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 8ec073ff1e..d74cdd8c34 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -66,8 +66,8 @@ namespace osu.Game.Online.Rooms BeatmapID = item.BeatmapID; BeatmapChecksum = item.Beatmap.Value?.MD5Hash ?? string.Empty; RulesetID = item.RulesetID; - RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(); - AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray(); + RequiredMods = item.RequiredMods.ToArray(); + AllowedMods = item.AllowedMods.ToArray(); Expired = item.Expired; PlaylistOrder = item.PlaylistOrder ?? 0; PlayedAt = item.PlayedAt; diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index f1bb57bd9d..85327be037 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -65,7 +65,11 @@ namespace osu.Game.Online.Rooms public ScoreInfo CreateScoreInfo(RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap) { - var rulesetInstance = playlistItem.Ruleset.Value.CreateInstance(); + var ruleset = rulesets.GetRuleset(playlistItem.RulesetID); + if (ruleset == null) + throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {playlistItem.RulesetID}"); + + var rulesetInstance = ruleset.CreateInstance(); var scoreInfo = new ScoreInfo { diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 83a70c405b..c082babb01 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; @@ -10,8 +9,6 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; namespace osu.Game.Online.Rooms { @@ -49,68 +46,25 @@ namespace osu.Game.Online.Rooms [JsonIgnore] public readonly Bindable Beatmap = new Bindable(); - [JsonIgnore] - public readonly Bindable Ruleset = new Bindable(); - - [JsonIgnore] - public readonly BindableList AllowedMods = new BindableList(); - - [JsonIgnore] - public readonly BindableList RequiredMods = new BindableList(); - [JsonProperty("beatmap")] private APIBeatmap apiBeatmap { get; set; } - private APIMod[] allowedModsBacking; - [JsonProperty("allowed_mods")] - private APIMod[] allowedMods - { - get => AllowedMods.Select(m => new APIMod(m)).ToArray(); - set => allowedModsBacking = value; - } - - private APIMod[] requiredModsBacking; + public APIMod[] AllowedMods { get; set; } = Array.Empty(); [JsonProperty("required_mods")] - private APIMod[] requiredMods - { - get => RequiredMods.Select(m => new APIMod(m)).ToArray(); - set => requiredModsBacking = value; - } + public APIMod[] RequiredMods { get; set; } = Array.Empty(); public PlaylistItem() { Beatmap.BindValueChanged(beatmap => BeatmapID = beatmap.NewValue?.OnlineID ?? -1); - Ruleset.BindValueChanged(ruleset => RulesetID = ruleset.NewValue?.OnlineID ?? 0); } public void MarkInvalid() => valid.Value = false; - public void MapObjects(IRulesetStore rulesets) + public void MapObjects() { Beatmap.Value ??= apiBeatmap; - Ruleset.Value ??= rulesets.GetRuleset(RulesetID); - - Debug.Assert(Ruleset.Value != null); - - Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); - - if (allowedModsBacking != null) - { - AllowedMods.Clear(); - AllowedMods.AddRange(allowedModsBacking.Select(m => m.ToMod(rulesetInstance))); - - allowedModsBacking = null; - } - - if (requiredModsBacking != null) - { - RequiredMods.Clear(); - RequiredMods.AddRange(requiredModsBacking.Select(m => m.ToMod(rulesetInstance))); - - requiredModsBacking = null; - } } #region Newtonsoft.Json implicit ShouldSerialize() methods @@ -133,7 +87,7 @@ namespace osu.Game.Online.Rooms && BeatmapID == other.BeatmapID && RulesetID == other.RulesetID && Expired == other.Expired - && allowedMods.SequenceEqual(other.allowedMods) - && requiredMods.SequenceEqual(other.requiredMods); + && AllowedMods.SequenceEqual(other.AllowedMods) + && RequiredMods.SequenceEqual(other.RequiredMods); } } diff --git a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs index 2026106c42..d534a1e374 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.Drawables; +using osu.Game.Rulesets; using osuTK; namespace osu.Game.Screens.OnlinePlay.Components @@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Components private const float height = 28; private const float transition_duration = 100; + [Resolved] + private RulesetStore rulesets { get; set; } + private Container drawableRuleset; public ModeTypeInfo() @@ -56,11 +60,14 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateBeatmap() { var item = Playlist.FirstOrDefault(); + var ruleset = item == null ? null : rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); - if (item?.Beatmap != null) + if (item?.Beatmap != null && ruleset != null) { + var mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); + drawableRuleset.FadeIn(transition_duration); - drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value, item.RequiredMods) { Size = new Vector2(height) }; + drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, ruleset.RulesetInfo, mods) { Size = new Vector2(height) }; } else drawableRuleset.FadeOut(transition_duration); diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 238aa4059d..21b64b61bb 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Components { @@ -27,9 +26,6 @@ namespace osu.Game.Screens.OnlinePlay.Components protected IBindable JoinedRoom => joinedRoom; private readonly Bindable joinedRoom = new Bindable(); - [Resolved] - private IRulesetStore rulesets { get; set; } - [Resolved] private IAPIProvider api { get; set; } @@ -117,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Components try { foreach (var pi in room.Playlist) - pi.MapObjects(rulesets); + pi.MapObjects(); var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); if (existing == null) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index e1f7ea5e92..dcf2a5a480 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -69,8 +69,9 @@ namespace osu.Game.Screens.OnlinePlay private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; private readonly IBindable valid = new Bindable(); private readonly Bindable beatmap = new Bindable(); - private readonly Bindable ruleset = new Bindable(); - private readonly BindableList requiredMods = new BindableList(); + + private IRulesetInfo ruleset; + private Mod[] requiredMods; private Container maskingContainer; private Container difficultyIconContainer; @@ -86,6 +87,9 @@ namespace osu.Game.Screens.OnlinePlay private PanelBackground panelBackground; private FillFlowContainer mainFillFlow; + [Resolved] + private RulesetStore rulesets { get; set; } + [Resolved] private OsuColour colours { get; set; } @@ -108,8 +112,6 @@ namespace osu.Game.Screens.OnlinePlay beatmap.BindTo(item.Beatmap); valid.BindTo(item.Valid); - ruleset.BindTo(item.Ruleset); - requiredMods.BindTo(item.RequiredMods); if (item.Expired) Colour = OsuColour.Gray(0.5f); @@ -119,6 +121,11 @@ namespace osu.Game.Screens.OnlinePlay private void load() { maskingContainer.BorderColour = colours.Yellow; + + ruleset = rulesets.GetRuleset(Item.RulesetID); + var rulesetInstance = ruleset?.CreateInstance(); + + requiredMods = Item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } protected override void LoadComplete() @@ -145,9 +152,7 @@ namespace osu.Game.Screens.OnlinePlay }, true); beatmap.BindValueChanged(_ => Scheduler.AddOnce(refresh)); - ruleset.BindValueChanged(_ => Scheduler.AddOnce(refresh)); valid.BindValueChanged(_ => Scheduler.AddOnce(refresh)); - requiredMods.CollectionChanged += (_, __) => Scheduler.AddOnce(refresh); onScreenLoader.DelayedLoadStarted += _ => { @@ -276,7 +281,7 @@ namespace osu.Game.Screens.OnlinePlay } if (Item.Beatmap.Value != null) - difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; + difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; else difficultyIconContainer.Clear(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index f4d7823fcc..9f917c978c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Extensions; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; @@ -78,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.Ruleset.Value.MatchesOnlineID(criteria.Ruleset)); + matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.RulesetID == criteria.Ruleset.OnlineID); if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 2d5225639f..02e1b115a0 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -386,7 +386,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList(); + var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } private void updateRuleset() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 073497e1ce..12caf1fde1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -11,7 +11,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -71,8 +70,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer BeatmapID = item.BeatmapID, BeatmapChecksum = item.Beatmap.Value.MD5Hash, RulesetID = item.RulesetID, - RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(), - AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray() + RequiredMods = item.RequiredMods.ToArray(), + AllowedMods = item.AllowedMods.ToArray() }; Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a397493bab..cb50a56052 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -247,7 +247,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // update local mods based on room's reported status for the local user (omitting the base call implementation). // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). var ruleset = Ruleset.Value.CreateInstance(); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); + Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(ruleset))).ToList(); } [Resolved(canBeNull: true)] diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 63957caee3..eab1f83967 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; @@ -37,6 +38,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] protected IBindable SelectedItem { get; private set; } + [Resolved] + private RulesetStore rulesets { get; set; } + protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); @@ -78,10 +82,15 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. - // Similarly, freeMods is currently empty but should only contain the allowed mods. - Mods.Value = SelectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); - FreeMods.Value = SelectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); + var rulesetInstance = SelectedItem?.Value?.RulesetID == null ? null : rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + + if (rulesetInstance != null) + { + // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. + // Similarly, freeMods is currently empty but should only contain the allowed mods. + Mods.Value = SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + FreeMods.Value = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); @@ -110,15 +119,11 @@ namespace osu.Game.Screens.OnlinePlay { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = - { - Value = Ruleset.Value - } + RulesetID = Ruleset.Value.OnlineID, + RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() }; - item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone())); - item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone())); - SelectItem(item); return true; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 35d417520e..2b071175d5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Extensions; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -36,10 +37,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(PlaylistItem.Beatmap.Value)) throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap"); - if (!ruleset.Value.MatchesOnlineID(PlaylistItem.Ruleset.Value)) + if (ruleset.Value.OnlineID != PlaylistItem.RulesetID) throw new InvalidOperationException("Current Ruleset does not match PlaylistItem's Ruleset"); - if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals))) + var localMods = Mods.Value.Select(m => new APIMod(m)).ToArray(); + if (!PlaylistItem.RequiredMods.All(m => localMods.Any(m.Equals))) throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods"); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 0fd76f7e25..3ac576b18e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -3,6 +3,7 @@ using System.Linq; using osu.Framework.Screens; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.Select; @@ -30,7 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists break; case 1: - populateItemFromCurrent(Playlist.Single()); + Playlist.Clear(); + createNewItem(); break; } @@ -41,24 +43,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { PlaylistItem item = new PlaylistItem { - ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1 + ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1, + Beatmap = { Value = Beatmap.Value.BeatmapInfo }, + RulesetID = Ruleset.Value.OnlineID, + RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() }; - populateItemFromCurrent(item); - Playlist.Add(item); } - - private void populateItemFromCurrent(PlaylistItem item) - { - item.Beatmap.Value = Beatmap.Value.BeatmapInfo; - item.Ruleset.Value = Ruleset.Value; - - item.RequiredMods.Clear(); - item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone())); - - item.AllowedMods.Clear(); - item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone())); - } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 7607122ef0..ed86d572b9 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } + RulesetID = Ruleset.Value.OnlineID } } }; diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 4cbc6174c9..73d0df2c36 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay { room.Playlist.Add(new PlaylistItem { - Ruleset = { Value = ruleset }, + RulesetID = ruleset.OnlineID, Beatmap = { Value = new BeatmapInfo From 5b765581d83b9d7613e507697d68392c034d8ebe Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 16:18:40 +0900 Subject: [PATCH 075/306] Fix free mod selection not showing allowed mods --- .../TestSceneMultiplayerMatchSubScreen.cs | 25 +++++++++++++++++++ .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 11 +++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index a6151198cf..4dd3427bee 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -14,11 +14,14 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; 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.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; @@ -150,5 +153,27 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); } + + [Test] + public void TestFreeModSelectionHasAllowedMods() + { + AddStep("add playlist item with allowed mod", () => + { + SelectedRoom.Value.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod(new OsuModDoubleTime()) } + }); + }); + + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + ClickButtonWhenEnabled(); + + AddAssert("mod select contains only double time mod", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Mod is OsuModDoubleTime); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 02e1b115a0..836629ada0 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -350,10 +351,12 @@ namespace osu.Game.Screens.OnlinePlay.Match if (selected == null) return; + var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + var allowedMods = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + // Remove any user mods that are no longer allowed. - UserMods.Value = UserMods.Value - .Where(m => selected.AllowedMods.Any(a => m.GetType() == a.GetType())) - .ToList(); + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); UpdateMods(); updateRuleset(); @@ -367,7 +370,7 @@ namespace osu.Game.Screens.OnlinePlay.Match else { UserModsSection?.Show(); - userModsSelectOverlay.IsValidMod = m => selected.AllowedMods.Any(a => a.GetType() == m.GetType()); + userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } } From f03de16ee5a46deac3b5f2ca1edfba5c4c5dca7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 16:27:10 +0900 Subject: [PATCH 076/306] Add a test EF database Created on d8a23aad4 (just before skins were migrated to realm). This contains: - 2 beatmap sets (intro and disco prince) - 1 score (set on disco prince using autopilot/DT) - 1 skin (haxwell) - 322 named files (from skin) - 5 named files (from beatmaps) - 270 total file infos --- osu.Game.Tests/Resources/client.db | Bin 0 -> 266240 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Tests/Resources/client.db diff --git a/osu.Game.Tests/Resources/client.db b/osu.Game.Tests/Resources/client.db new file mode 100644 index 0000000000000000000000000000000000000000..079d5af3b7454bcd1c3acfdd199ce1a15c33567f GIT binary patch literal 266240 zcmeEv31D1R)&F~$Br|Vj-g~9crlpilQ%W1Uy?vn+NSkSiU1?IFBF4A2L(?QAlR{O* zf`Ed!FDRlY?zkX|Ac*3Eii+X_;szq%f+&ax3L^jCy>BL&NhU4G=lB1_o3!W5+~wSR zfA`#T&%Kj(rsu>BrE*k=2Zwii<$}&lbd;|m3j*^Lnb6 zd2adAEiK4R$$utgbonXyarw{Li-p%`E^qx7-y;8Qs;d8s=@%cLO}8yrz`tW%+4K9O z)sa`;?G1Gg#Dg_2b8EI1J9~?T-pKPNWywx2*50tG8uq_r}hx+Y9T9+Y6oBdN+4(LL(cCn|hZf0y{^; z(%^>33%ds8&+*Is#1~NGZY0SufM0fw|{C}ddgm~YeO_piUxwcjj=_M z;c>;7s?6YU6s{f|+_l>qCOAz*y0^4DB13)g;Nb4YB2J2iM@oa3jEPlGE``xZFdRh# zJ>|jSz5bv#98NV6wTonJ-6E+jnDLSHP1g9eo3|Fb*KMNfXmR_b{0`{e+TzyYrZvT$ zv6VY65bKvL-HyWMO@*%Fh9VaEn$Dgzon6IbMh@J$%6h5xwPyKD=-I^O)v#xk>d(Jh zRVD7Lz93px-=mtGhJC5#H|WgPBJ5iCnu@L34@gy&heheDzMj;P@r?R@RMSzpK_4pf zl ztx<)VmqPt?*5p86p8Z_b)jQBccJA>?{lrz6%AU1%50KrjT=L4WjbtyKY#%pyyHS74 zYGSyrOsCuC&*#ruLx;OQ+Pk_$Rb~8Drhc&F8P92~wbc$d)PRg&%5g$egI%KvW@9Bh z$r~;Kn;Mx=Pz--{Yr1XjT>jjX>G0Q<`Xd5dm8a{6UQK>Z10Sz+pkX)m3bsaZG>jcL zqV7-404h5;?2y)yZdK(1a+Fa zRBtKR)u2IGl5Kpt&9?b-yD9p8eZ{pKOFM?CEgm@mY7cj`VPaGLEHo@LHWOnayRn-l zN-gJ#Vx;NZV4G zMz0*~+|uvu^>HvG{&PbaD`%&A5*dzWsE(=VCXq3A?~f~?G8$~$fi<&IK~mXN4aL$d z-6fSf=?Sqr?5%#TQauPa?7?wusZ*;$RK-$kL#Ly_*eBhtG!)P|Gu4*}FjP%3Oj}U^ zUpyR*>^y0(e{^?Lxn3FR+<~J@LlN$b)Mg?=Q5DtIG(hQ)J`{&=0v;OKd$Kn?01z5h zJ?4q`IY;M0*U2RPTb8fJK;oQg>|rpaczj zp-)S#NyM0%iK&AM^%0%juhLj@b;ug21k|b0Y=d%lcO?xsY|NGD9I1|~>dM=;jYe5djb)r<|?MC zVz^2BxJ|(|3BhU2AZ|R#6+pRhH&&!mE0d9HP)=Afm5!W1-ME6;np&4szyd&vnnI^_ z<7m0MZX5P>MN73ERfF--bwlafui72koZ3#>v0)=zMF&6%os!DubWnpHTx*rdZEeE_ zyHlybq&_AF8f5F#*j83*axdTAl>jvkQbSQIo0569Ved^SmXWCA#?r_LtV}mK_0yZx zhLsIIQ7u$PKMdGir>}dUge&O&HTV#83NxR@el;g|2PgkZzFt08K3QHV&z7E+z9(HF zoh6+t>C!auLGhd7N5oNay|_64O#Y|&tMcdMH|H1S{)TGohatcaUjYn@sw%yu1(+t$w!m6qz}xl}RrrUrD6rr`9ETQ{6taj@6!g(a>^KYCEa@ zEbbJV&D3C0^toIgq$>Cw=%%1)UaBb2j!bbWAj;R_Z}VTr$q&j`$gfL&|9_h=);>dk zA;1t|2rvW~0t^9$07HNwzz|>vFa%z(2+U~Ba6Ep}d~0)S>v50A^rvY?OT%QqplA3@ zh8J3z@OlxciMlOKX*|Bqac%S+pK57JjY&F$uSgPF@Ka3|vFa#I^3;~7!Lx3T`5MT%}1Q-JK2pp78Eo>ss zDO96}@TP-lUo~qdB#LpA_Vwb!H}|R+I5iLbbGj2AlIjYF;9Q zJbEIhQwypk$@`XrTT)A^3Dw7@G8{L(HPul~CJ%w2Xo}0_QgemsLo9L=Bvk{lkn9|v znwzOU5+O<#am`XQCx1=8Kw2l^W%64O0ei;~U#EBWV9nZ)LAmLDW)yo$!=e*{kgERpV)F3&$IorGuo z!~ERbow+yWPRQ}u8?wXMCBpr}<-$oBH}kE`K0!`Dn!YL>rsuZa+4{!T6I%I}8(KzM zmNq}o{NCo1o2R92O`V%s+4NY`C!6A?dHh}coB0>_w<7e|H{I8Njt5{<7prXJX8=m);0TB;VQMVE?Ebj>v* zQ&Vi+b$lnZ^~iTLKh)IFveZabV>NOj-3S!RvK%u&A3Zm;Ayf-I&(Mnw72|4#;;F6^ zy1E-iu4cNvVVa)psgCa%u>(nlquXxen@$*_9o6xa$W%<*_5;ONiZ&Hv=)UUtjvtvZ zD(IoF`o3p5f#P_s8z~M1S)OBt2xAhO9(n2~LVg4=5hlcQjG~ncDz;H3a7`l!BFBIj zV4*mW9lBoNJ9v(X;A5F_XgR(gMWKfWgaRiD1N170iY65UsDj9F70=W3*fAo60WmRa zj&BB0i2eh5Jq$zDx3x%z5Je9Gt%G@W;!uywqCv%&wxb((EXVg<$F-5EMv-Qq1CAGX zw(ACwijf9>V1|$z1e#`hfu+O>!stY?;}>-*#*N)r^KCUYW8YL`UkwdAHbTvdP0NKW zKP2$#ii;<@pc2048fIiWwh|kPjwXv56=RsXW1EqytDfoDks2$ir>g-T3c_rJp63B( zL&cRVpS{G0~1`s4`l(xvrCX7LNhvB+j2Z-Z7G(IHB($c#YBM{8I}^5w&CfRK+6Wenr+2)9QqLg`arlAyB3L-0>x1sEpmM; z3JeXZ6!}4MITfQ@P&muPY*~@7TA>a8WG`J?OBn#8}7<)EAoOgorbRxw0%*)0G$ulL{S*J=6A}sXFw-(uzx| z7$9iscHrBt76$=3V?wty4{KSoFw>YtOsyT+kna0o=!BkSYGDw27Q*IurdwQ+49bpm z{F*>IK$lDlF=K)NPY^~%;9x)+R;X-HR;Xx^q82--80cN7Mu88Q10DS| zHK?fJhq^=78s^KiG!;{VVmAm$(;8I7hiS2(^lFHK z`Iha)ZUn8>0@yUgb4{!igfy~r*ca$$@yLo8AFB={^Aybpyf8#Fo`HUNo(e0ddss$> zX9cbv#Ia&Uum)J_y5~6_R*Pa37g8|>N&`|19>SoD#3~zxO?C#79AZH$E}cV6HX0cmx#_X%_5+s|3&%MEC*m!x%}HYx%JsVDXzUNwMXGF)+7bZ=rv- z>-jJYM&ww<1yqcN2u3iFk!^*6x)=xu&m}u-7`YUU&3BF%4q2+`zU11*(Qc2goXb8e+HeuuCX*@o*}}4_sF#76iMM z5u+iP2OHBzR85II>|3$pV5wr)MyDOvSWKabUfV#}@||LViorG#LBX*Bz;cBKx(gc- zn~FiY;dq)K=`K-P&%qo=8s-j`9g41Lp^07DE*?h37=9eu2G+I@VAa6D%wxL>VD8M& zfnCNj#tso!Dh#V>LLIyq3KnZvJ#a*fusA0X0|oX24VDagVq)KjP({(<=6IMOG^#*x zFfKR|fE|7UEFx@}5@-Zb)h`}O#b9+{TfhdPnRWyl2^1U^%Owm9Y`X@w6Aj_AVgf46 zn~#kP?uL(!#AE@)VR1GU<6>hb1I3OK#hMvmxnm>4(m@xX3-Cuw1F@@MEL6*a1tCrt zQ7PR~qR4UFwX=vA-gVUgdo4!os4;9X42R{zEzo?OctejyF3JQJ=AAjPtSw*YHl=(eQ>ii_uR^X8!FdrO22Q~5Vy{V`$%4klv9kqA}6%+Z` zmMu6CXapwTQgsb}o9Dr-z?E=S7*)KE03Eh&4YmVYxC;Y~fMK2u6J9)+iXj7qzXOv1 z6?Ijp9neutJ5?JU?eb6nu(Da@O|KrSPs@9aWb&Unoe;V6@!)L#R1$bAG(a4E>_U72%8$#VW?pT zPt1jD`@Rx6umh%P`i^O<@Hb%X^|cZa!>jNCV51zU4t!f66{{Fp;99U6@RrF&fvA3V zsA4IYu?hbO))a;i4xZ(}b4?YqNq68<*>F7IHy9dRmJqS(f*6+GaE$<+$MS#%!exi| zZ235ESV641umSL%U_6wfP#G1%d}61AF2K`rjnK1kWN`Ed8=s0E1=#4Y_rgOVLFOFt zLpDG7y&=LwdTudO=?$C&2W|#-`oM+uz-z*>1_Ls&W<9uru=T_vgWB0fXkhJYh(QQH z34KQs#WWQ|_E|XYSj@!N_hV?O4`k4&?h#`fwbZl&ju&V)9salj1 zF~m7gEm%Cb8u0HN3p8|e4=8$pf)p448|es}7Xoa|mmkBmMtN9|z%Pna44JzK`&00UHSq6Nf1qn|-W%K6+-kMjTrqnKvXG zp0B_%;=FzIzL$YI`#M0)VGv^_iT{r;{bN520fqoWfFZyTU316@w34>8FhwaO&o7g&LZqapDUmsQa_KeXi&K|V zx*wS|pU4bI@pyQ&R37Q#S~=krGY+0Cb2EC5|ND`LTvVHn|65a24tJwBQ1X^jx*t(A zp9tGcic=!W3%MrgdQN_;bUhgRVF)k;7y=9dh5$o=A;1t|2rvW~0t^9$!2c-(cBQgh z29I$F8DS%NcI5dyJCVlsL~6?vZJBBo6#HmRJ=#>4<1rgNRk%b^j#dWnrBjb?+JhOtQVnRJg z?U~e?<}5c|JS1FcZ5L06CU643@%cU_@brYnl%q{;xvJ`hWhFxs^Y7uBayN7Gs@%=V z@BgW3W}RUOFa#I^3;~7!Lx3T`5MT%}1Zoi2x4mT^hi?MIK?uj8OlBj`pP9*|(_ChD zMo6bJnM`Zy`JRvPTtA{}xTc422_YV)4@?B}2{k;%AK;m9$Iq;jYkGhE*qZiXMAgAHVQ|e&h$w=%4=$dLjv_oCj}R>uqOTx=Pk>k}h(myQ6o~1A zNGu4~VcUoVfv2^@03jw2QvgAj0uomU;cgs+2SKa>#9F~a@jBwuc($YIcv!rZPazT= z26ce=bA`o8oH~rLdtk&Hi0a2U?e*9F`kt?JCt1e_ujbb+dUUyRFvsy3E&>PO+3y%pt`K5GL3ADsKfZwo zVm^7m9)SrE=mT+C49f!+h;4>v)Jae#JPO~!H$B%8p5P6K_2bLF#{TQRbMGM;)zR_C zo{QPNXaB=%YEm5pb1|GCj1j%TCqZNIz`PzJb_@+VhLbhIW>_X-c_5MzLJqkhB5NT8 z0Sagcy@OCU2vwxTi29-d5k!*G5c&bZk`UR!CSiD*dH%T$ujq~Jtsh>hcv6#*pGPuk zhPUr{9*a-l1wr7G#m8-@vnRA=c%q(}RP*ya!6RS-;_sM<@1xo@UXmRnj28kW7>Ejj z*hfuClt3uxSU|w>)karc-AIo6%Af!7+M6FahUPrYT_H*jaxKz@ocxIVRq5x_Ez*V6 z-x?#sUNQt20t^9$07HNwzz|>vFa#I^3;~7!L*Rb~fnyK_5jR&U;a|E*It0NFd45qU zQ}-1GB4Qcx{OlAy-Y4Pn(b2njeqXAo^|@XL)ZFJl$Xj{-oTirNDr4-%1QJdp5}Kc@ zgt1ErNRTnErd0Cv2ul%9P~f*e%Xx45pjwXkV(Z(-B64I2t;HgD?b-P(!tLi?8WeRcJ4eYml6>-NI> z;`T!4w%*P7u8FP1jm1s9OA~>eMF%?DH!c9?`_nDYQe}W zca`>(Mh1tc7KSpry`joiE$&Cyn!(Y5ayWSA)cxuzjg&onl||2x7nBBev=;#1hA~(w zeOfnM3VTW~kEZO<3Es%g_Cjy*lmyL6wG&E+yg7B#8=`VK8ukaz>K<4<9C^D&rWC)i z%WT|2^*bo*Ai#81IhF-7^#gC9l7yytjX9 zTzbl0uxmp!P>KeEy^XO&k>PR0n5qoETqImQIJgU6eL`@Wh;(mhcSMHz;=#e)jYXUk z4Ud!tF&Pu9o?Hr}kzhEA271ba!+ZTfT%I?t#gCJg~XLV)hqN-hwYwOlyb#0D=);D=^t=+t}*u8EOU9yYYSD!c3 zl5ShFguk$@)^t?0r?S|qZ(AFhkZSI~Vn)X69?+Bwm$4ux&r&B%-!@PR21BUZ#7U$U zhe zr@9ru&!~AR)ay)5&T~?jx_SrFmd-t1sh_w$Q!Yq2iP+7{C9e!yPduaQjEw8WCOn~9 zlS-CZ4^~roLVCx(SEkc#^XK#Dt)VEakM^!EQ8gWZm8r*IJmWb*P+RSQ2n1wcQw|@Z z8rYpG#*G)iN%%4`Kvp9YSYr-c(T7Y|^iFGbCcb6=54x2;&gpTDqH4aWVaiXx0BG}prS==b#%*KREB z7^WtA8Cwd2;b>5vaB>(kF5OM7 zZKv5hhg~5$tD<|}XnBynZ`?|s+OY8yiT{`9ujlyd#jlD+{=@ktxzFZ=>>0wZgi|t) zW%j1ukv_C_q~+cgzxju$`}p7H?#OM-4z>1GZwCHHdDCs&fIT07N2Gv*D zEXGsjR_3UCQ&;g6TrD&z-A=rW_VGHHv-aw)(@~n8ZZpxE(4e(Wy4#IcJZyiBC50Qc zRmthtJ1gDRIhWrzzuHuF1L~{aNvg?nCN)>hsaCk;*evyw-ktaem6gtJWa@nF`fCGr zBg1ZFlEd=^yE^Wn|KGcjDYO@daJeuvviD?fc%b1?b^PRw1I3tu41x( z)^59vZ?GnuZ!2#npK2?1bv(ae&nnqucbRaseU9C#o)7-L=j;Q5R6Vxg6x&yQJ?X$d zo>7lUHJu)TrvfDVZ`cyYPGe-xukM#*PU`pA_71---L?$7aeC5jOip{%nuk{Q;BA|_ zPuy0-#}-vLqgtRZIU7!Fjiz?&YnhpDD|YbvI;xG-t5VGj*JsS0wDs5L*HlbAj{0me z-l`T6UEC)fl5Sg%ewb7AqwXH8URK+Ix>8qns@~KW>ex5q;B?zYmEY%1?$8*r)fe4e z<2|aT&D>9~s%84BFUX1}6Es;20jj1{$G%0=({0<)uM_vvuW>QN(`wYa8PBG76UFSW zkCV!F>^UgiwrT;tZ}xA|%9_F!VghW(gS2))VguQwAm zvzp$>`|r(!ij8|Su~5frR_4-es>;6>LF~pqo5hw?)$Z{DR8#Vme$;vFa#I^3;~7!Lx3T`5MT%}1Q-Ggf&V20T2egU+JeTW zH#dXkQz_8arlwR%5ZL+ue`yj}GYkQS07HNwzz|>vFa#I^3;~7!Lx3T`5coGEK+gXu zdj4;s=l`a<^ZzJ5|6jq$H_9u(*bhU1A;1t|2rvW~0t^9$07HNwzz|>vFa#I^99L|{ z*8o1>@6#a1@3s8%{UX7pF7!oszF#kpl^@6X|Ig&_|No4a?1v%15MT%}1Q-Gg0fqoW zfFZyTUMe;+45CEu6)z7Ph<#=sC@2rvW~ z0t^9$07HNwzz|>vFa#I^41xbT1jwKN;|mh;_UC1e#MBp4F z`F{qV|KG#4$hVR6{}b|0vFa#I^3;~9~ z3kv}n=ue~m|KBC&|HtLKvFa#I^3;~9~3k3mkAHhf!)b=}iUv=_yiYx|*N zIa+APp6xa*NNp4}J9KT!v{X&C70)$QJ61e1a(u&5^;ofz@Be#*eE$E4{K(YfV>t`~ zh5$o=A;1t|2rvW~0t^9$07HNwzz|>v{O2G*{_;OfzW^XbpZ{;d2mUQmkqls^e693T zsVKcsUXc2G`oiY(TTT|1G|8#m@{O%erEe3?$o-c4jBuTFWz)T_9hvvF9K=1*{GHs4 z?DwTMepUV%{??ZJvaidXnq8AxB#(+k@dx6?`7a1hw_M(IS>}Pv_WZJRr@SKf{`^Su zajDP#%ZL@5_ldj_IjUhOapd|z?0TjX21aO^wqh%muB%Gq*me|IVeI>nVaH||gm$QC zu3>AAu6g<~RElk?n&sQ>R^j1-bJ+>yhu^8c9<_%Tgm%jn&ABbR$qK%W}*BJ@nkrhD6jw78Pj#IT*HB^PYNqQOrs>(9>iC`!I}l_zy6r|jAPdoq>Uc_IDyD7YR>@Z! zD#g%!)$<)cGGmn2LtpiM&vF9A@mx1j9LTXe#|#z44o%NdF$&~I08&*|&oS&|M6rz` zfomE;5IF{<00+f^=+N~7-?0*GEHe%*2lEt#o}xN|6NLf#6hs!40+fQta23zf^w=>X z1=TQLj&BB0h|U9bJq$w?H(`+u8HyePRR=Tb#GxLUCY55^xJFZTjKg(Y8+mFJX$Gdo z@dDhGxk032m;r9=LWm6lO|!kgQes6h>)GFjNH!qdFSatrZ1^1{I3@ zUAoeW9Fg(*;nvBSfb^MwDIY5U@OE)kV zimpUP;9xWw7Nu<&o{dXcSMjk@Y#0F@rohxx!&a71DV_(#bk)ekXmzq8J&W{T^CBxy zfrz8U#LmPn^wD%f1vkJRaQ0mb7B5oNqo@?9Tc}2X4}=39oisHlrs3m$nXE6&lxb(#j-f6{Mg%heqX|n4z13paeXPJ3 zM$vZ7P! zDy#v_Y#g`>`Fz1pv0$K>fgamX1w(b_QzF}G1KSD|tQnXB&o;5n4HIo44?V=r zGM7p*{5Z4?EM^~&s)2zS$F3B>wwa*=6O46>tst;e7*W%N8hH3{2WuWH2A+ly9+pUf z*7|`4vjz1qu|Gtpq3G~pJj@J0RG=>y6ubm<51s(bA1syY-Va$B&Pw*#vbR#BOABTrhDK7S2GHuv7@IjOrVSQt} z!fHVWfEfG@(@<;`#zD0#SPbG}5lzw^C5jx!ot;Q=)d2e`M(n7u=W8$&mXD2J^L66u zV16~sssmFEcS?t$vG5;lnXrboX`8dC6!*#%FN8iO=rZ5-IR#d?2Z4CVU_4<4%nIEhajH6=TD5GU2EIOe{3G z127JTZd+=gxQ?pCravHV{O5_>cm5g4yIDD z?krf8keF=^+Yt;{5F4%oBdn=$2!{b`q#N+ceI?K>EDHD$rVrhO$%z$bdSyg#k1zwU z70^VyP<>rjG~a_01S6ne7_dj0iD4P=Y~XKL4vYzLBCwH~&OuZPtc4c`aG`uCFt)Q; zLAN68T(BjfhCMnl5BOZv$C&^N-^9nVj%nkgRUc+upGKuXPhfB@tUqjZa4j4QG<0+i zrp5~tq(CLfN3sfBA#BnR<;QU7P#)?W_;S)0cuO|i3b+M^27e?}Rh$xF(GAxK&~dB? z=pDRpxWpEYEuj^}s*5!b&j{8}$yP?Bz<9%SM;e?c*9biuX8=c!u(_$8i~Ti@vAe?m zP_fZCswxDdbflr{} zi#Z178S0~=g-n7Ac2ca|i*Ku^gI*m0wgu7xI7W#_)$i_AjdgwEb4i+{tErRE0+4%I^)y{#WVj3c=MQR zXa*KQsB^bv#ZU55Hr<+iC;bM1yYU?WKbF5Qe_6gkzE-|kzDoYE{C@cz@|)$?%NNRL z%V){w;u(RRav;A{-Y)mbo8=eDMfrGnrEJNnyi{H!x65o z>Xg4ODbf<@NNK)wnA9dsmqaNerNn=Ve-|GU9~K`J9}s^f{#?9MepLL9_zm$I@uT9E z;(Nt+iEk6%C|)Ft+}t|ZpvMk`%Lbt+=p`S%e^c2*4!nz3v>H&ugHz&26LrckUK4Ra_+?3`dl%0 zT+Yquxn;RUx%s&{xfwY*C*)Gur?XFF|D63p_JQm@*`H>Al)Wwc&FojQH)gNRemeWH z?3LNevv1B`oIO8#PIhm0B)cPfYIa+8bGAFXCc7$YXVvVI?85B4?Ck8pSuvZIhqJrI zo5dTlp0rYaNHXQ$NL!?9rSFNi%eP8DmVPE*CjAQP#(o$A3;~7!Lx3T`5P0Ds(2^3G z`011$MCmk2WlAMVMN0FO<|xflihxt7i-1$0X-ZouZK1T8hMGE)zD~kUjnLOkl=75v zgmTYN`cF!qZb@aDxPQ>sPf_|LrGKaNZEo3CmD0y3{R^d!Qu=2~AEESNO8-RZ zA1Qr^(mzo8Af>;j^mmm0meSu)`T(W(Q+gkzzoztFN`FP^J(S)}=`Sh$1*Jcy^k{RyRaQhEoaKc@6Yl>U&?A5eNbrQfIYdz9Wr>31pp4yE6w^j1p0Md>#w{RXAC zQ2KRBzeed-Dg6qiH&gm$O20(uO_Y9-(iJlzx`dYbgB; zrB_q>X-Ypu=_e`u1f^F|`f*A>M(IZ>{RpKWru0LUevr~DDg6MY@2B(%O5aE6dnvt~ z()Un$8Kv*0^j(x*O6fZ(eFvp)r}S-1!#ykkSh%J)hFoPRJvX_vvq;GRAXOdTY z$SV@VZIse7NgF2Q4D!AuM_vvRGC;^~Li!2WMab!dlnB{L$PPkcLLx#!LIOg3LOeoV zMo1qaFD2wO(nw0CG*1#vCFI3~Y^RCa$jg%nIf;-{2FJA%_t%hmb=FnN7Psi@agbr6C zFSCRQgk%UIK>$f+Y8s^?NgE<0iT_XbvT1~{ABF%!fFZyTUvFa#I^3;~7!Lx3T`5TFQ<^MAAaCr*As z{u90d@F@lhLx3T`5MT%}1Q-Gg0fqoWfFZyTU675G2pNw6hYsRqBSvi;Bgm;`nFv^l(98&; z?1qRCj-ZPupdp~J($s|rfC7nijBvMz|BG0qh@g$C2#Kg6b~EDoB8<9&h=AxP0)is+ zBSQZf7`u;H(iVawBIK=^m4C}M$@h})|GQWIE&2Tq)e_cih5$o=A;1t|2rvW~0t^9$ z07HNwzz|>v{5K;&g8r>1w;wb36uG{T5cdx(jUa@n6elEq|NnDNzF&S^epvqbe=}5U z>vFa#I^3;~7!Lx3T`5cm&7Ad_kn%3f*LU^X?IkB3J~<&mb=)WMm- zkx?Pl#_#q9N?wY^|N9S|S=MES07HNwzz|>vFa#I^3;~7!Lx3T`5MT&AM+9ol|8t*X zRaTrKzz|>vFa#I^3;~7!Lx3T`5MT%}1Q-Ggf&UN$MD&xFcXRm1ei#A_0fqoWfFZyT zUx4H+mAgL!2>;CGdvG} z=D&QMw>w%+`lg=3|++X3CjUGHzyW`swst>8sOkPLHOyr;kk^krrAXX}z`e zs@4lzPj5Y;wWBrN@=(hiEnjJQf6Km>m$s~FQCbdfNjE>-d}s5Qnm^opVRL`;wr02a z@aC4(L#ZF7u1{T&IxqFo)G?{Unx1I7qvjmzPk*Gfk%B8`9WxEH%WpTeZva`RmV`uqz?X2ZP13TvLYem6MfxEN5;7+eU zE|)MxD09TV7L-{ha6f5WW=vFbBI>S*#fHP-!C+S+DwT-(Y2z{#QSJMhP;8CB{d_{P zQW!=9W9{)td-v5B8jkwCv&IB*iJ)K9ml+@s1cSqLd<)M(nQnpmb_A5DxBMHn=Amrp=s{H1qd{B`VF#ITM9UfqQadAtGVs z9%L>N_}s`4R&g}EYeU%`0d&;JUBWKlA1I7A#mBaKvIgP??N@8BUY=G2Zu;{ z+v!NhNTo`;KR7TtviwYM&&*O%L9S#_LA!_a89Ql@XVoU7__Q4;K2PAMjg-A%I<$jg zq|6uigGSCA930t6WX_I|gsEt)R9@x}mdk^5mga}Z>J<3qYL+)NG(6}9JI811@Bl@i zWKT_;{i+iB=-Dbk9_cTI(J<*pIjLO9pYEal0)cxvDd3ag&v+TqIt1>Ym9$FH*?kbD z3sRCA4TR&pD7+L|os+VN9`}2DiOjjDL8dEk&m^_u!G0)DjewUR`#6E;CTCY#pK~gj zu!Ve5pk^RMdi#r!Sro)cnKeB)Y&*)h!kHt3ad}x%@XVd1kuvlVv#{_Km!&!YIwEbc#$NsdngAw1u^ks>v*uV7!qx_eA6Y=O2#i1KCp|Z-h?6kz0_B zE$0XI+2P=*-ye~^q%zJ0n^6u1?)It55!4UgghJ50D`B?a73^5nbHcI_46VN+!`Gk1Gu1%tc&!D{}> zB>$7u{8Q>xVeZk$M_1oi&0kZeDRbPU?blYby&xDJhPO$2e7KXye%-{(O52Cp$Ol9h zRr6QZKx8HDUOXYMQuPoM*?{QvW7!lU1F3-MlCjikOM24M8>$(VQIJZ9Xo;vdjpdQl zIVw^C>ZR4xDsHqLC28l))yy^3c2*?qyrr7aRWqXH$ODpZosd@@(K2LXL~k3*rY$W^ zT6+6fYNhzBCCGp#zoVM5W7r$oNqc@2a?ta4*5y>H&FM%oKT*xBPB*DJV=*$ZXg)Qb zNs`+aAsO!C_v=kcP#TDKdlk8DN1_an|JF#wA0F~XMo8sD7a{|5{Phvy5c=NmvS6@( zaFqHbbB{nC5V>VSUPb1N1+b*Vo2b^@Rifi-N3va)9C`yg636SX`N(t|WYSH%t&sHc z8`WM`oy$Y#CB6J+ZAPUVhaR5f-CE5fL#yP?oQu3A0{7LiKKeUIA7>txq<_0ohqePF zKXXo!{+&uXMG5J14y9w7l_0zPsavOvW>N1`4PG_qCdFR_aba1X=Jy zzCyE>(R0tNgOLX1{PegYRZCA#N?(_h)`{?G2O({qz+F8Yc_B3%2Tw!FLV^1XO{r9E zlaUA&yJk3ocQOLsb~y2350#Js75i*$MzuW=dFb8elJ*qR-t>Huc5RZT(zINX_W2}@ zYRmL&l6HNPW{`5T1f*ePHz#S>7lUYko-C$klG0yDa!e|foDGt?@$p`<(?MsU-_j1$th!g&bGdQ1lar#^7?$$5xkvJp&j(l70Q@LZZ zU(dd*Y2?|Ri2wCc{2$x!hX}uGTe5_|kS}|Ff6Kl6&)VkBt-OnRqH-A?%1D)KYql0U zdy9qM&ea==h4yN4yO=H1{45r;Le1MkcUPf%Q*Uuyacf~|qaBgm4nvCVV5*b-U zZeY3x;=w9EV!~J|+3khJbTIARUF~Dt-n6;5uxZ`?E-kd5fa{K$B&2i$LvNs5^2nVC8EQ>-#|68VrQ7Ds<1bm5ph3(!g`@gn z$pk#A>CX+2@mdE$WGlSiDaYG8SoZpRh_ES(bXI(w_Ci;24Zz>9SY2v#6sl8I0dC8d z_5yTv1D(D~+D7lJHN**1LepDM63`c6PucU4pi5krRYUssoZ-`9A zek5)l=*Nan0jVnmovcXj^2%cxQqk5*PEQ#MF@lp$gFa1EqC{B_q?3hqGM2_2sa+v! zu|lc}(N{Cj+Sf$N+Ra;w-Rm~d#k07*rXVsqFnw!_TZ^046nhG_dC*I;Mmq|dH$ek7 z6k(RuboQ+2>?$VXp0Xp=`2*s8)z_0cF`iN1foeKkT~l=++aVvs-Q~{X_Z?Ik2&1#Q zPpQG7Dxqr(8nv?Wx;J$dPr)*7Sh}5zvwf@%8U2`~_KtnLmTv1rgDWOCSf@K<0+dRl z+ctHdxUC3FH@WS)ib?zX#0BZL_4E0CE356m(yqp97_V_K`%dzPORz-adF_*iIG#7T zUeeZhiTc69(pBWQcbq*x-L`50zi&~sJ5@}` zWK1kzmka5(ZnS?~wf!39?Mv{hrgT+bplgMxdQnqmV)djiH3djtCNzN_9WgiEwylHT zck+ZDVNWGCrd}#ZtoEx?VEO^NHnp6(4pxdy>LYEjz2oJFrQ2Nea^4iZBzCDX)QY5o zo@Kuhi$>N;eSOjsn%A*sPNFljt8LfJZ(rvg+;|grp(gS0O4H+Oq$YE`Mnh#8lh@u+ znw@Slv2=uLyWqs5l$NNWmGh-%)?+i!D|l#}#u z9MZEF!-y~B_sy?1L?BvQ>Zgv{n6ug4RhztcQiHX*HI>GJtx6;fS0{n&q-QVMU-Q*k z_02EZfAiy&8aH2)-*MjFBhzgQ7xEXHWBz(IyLLzmD=k*fAT<^)NHR)g(m*YW(yGURdR*Ftvb*4ol6z`l`*3y&z)h-KJ_0GLv;aQ#FH3H_d#O z4pVLWRW+&8r~3A*MJBah&6v9VYzL0857V>SlCilbOQTx#1$I{J!1Mq7>U4pVpAs}- ziExB4SA4VZbb7n^&TMmfhj_MlW_EfuC)_Ir+1crriC1O|>4EGb;qmP9>@nFj*%xI` zlzZh(;?dbS+m}5hYYH#Hr}sf-Tl#k4VSKuOx^RYYiEx&1j&OnSHsLbi1H#9ItA*=? zFA28@-xYo&{4BdWTh6{Bm(I<|9hO^=J1VE#h<=SsOD`EYrbJYAe8 z=JHSEAI!ZfcV=#5_J-`aIWGJ6>?gAC&Auu7mF#D;AIiQn`?~BOvfmPd^y2KJ+3yS6 zgiYByv%k#VpZ;q4*7Q%(cc&jnKa{>P{g(`%$zGx$c{3XKincFjWXCBBrl=(~MNr4wKnV$%<;0nhJ z>%{AXnc_{ETg9(sF34;WFVDOrb9&~C%vqUrnR7CiWZss!Ec1cP$1~StZpfXVyCnDa z+~v|)(mB!v(k0T{q|2laNM}eNldhA#B;6u?SNf6kGwELGYAKM8kX|A_B>qKwQsSkI zBug{J2c)^u5=oO>>3C_Kv`N|~{Z2YvdRRVNzF2;X{4V(l`6KeDE$X}GdE&o8i zOTI_`jr>RaUB}ntJ+dng%FS|4K0;n1JuPeU@$x$P#j+>wlpYuF7Jnk%p5K_?mOm{Y z<#*>t^JnLa`SbH{&tIPZVE(H7HTf^(Z_ZzmUzIoW%W@yeeKL1#?xx%=x!ZDgLlMt*L7Vg8%>@8$0hUm}*oA@Mx%BJpG5)#9zwfdVVn5*gl*;vgl*yH5q2VfIAL4)xrFubhY=R=a|jFhLkWxc*@V4< zpGDXnzKyV1{7k~mIe!pgujHo@HpRYSU>kq!gg~{6E?{GgRl|qDZ+BxlL`BK!v2=9CkPwj9w+P!?yrOmbB__$$^9i^ zj}kVE`!iw3bB_=*@yMwTUxE~XCF!v+EW^q3x>=5n;gw5b?Cu}D7eZtzf z?-4eOyDeegP1tt`Tf%*tu%+Csge~K~Mc69tn}i+5eS@%Lxm%KVUr*kBEqV9VivT*fHFP2wTN{Fkx3xRelZk0m{$k-cR`j+!d5x$i0v9-*WGz{CC{tl>eT4 z59JSXmr?!)?%k9>#J!91KXR8+{wMC8lt0Y9gYrkXw^ROS?roGm%Dt8Hzi@A%{4ws$ zl>e1`6XlO{Z>0PQ?hTaxhP#CF2e{W$em{3HnQ&*_gc#D;4Y;6PVNHA zf5M$l`CZ&=DE}#U9_2sh&ZYc5ZXe~p;9lLDYT~(HoMjc^8K*&&)XNZ;fKC~MhG)B5Qf zaTjIM>6FPOk~M25eKTzbWe3HSwMLXRhlFuY1*D+t(>G6hl>PH%Bu(n0^rbZEnbQd4 zxtEZ%{HcVBFDCTN?SxJ{g=X_7Q`U46NgLTlXt|fr(H@$S+DaJr!xJg{!4}GH-%Qz+ zn<#tFM#?VVK-qiOQ}(_WQFcW)W$!~#)h7ul3uY*F@lld?+;%HE(;_9l(8 zOI6C=tWfrr6_mYoIc0BKM%mk!QudA|l)dvP%0AIS*{2p$_Webaed|cdzP^yMTaF-b zu3A88JEik!#y1L-eRCdVw;oQ}SLagp?ZYVh&Ky#H)}fTnrb*wOMcHj_lznd|WnY^? z*;fvs?9&HRcHMN!u0Dve&rGB28kw@sN|b$0r0m)}WuMPcc72wzn+3|gkfH2`G-Wro zQuf6b%5G|=>`T}Vp(9^zYEJP&j-LNBk8$!p!9e4l)`{8RbI^7nCqyhXlQ zzES?X{2BQZI9Yx`zFfXkeyjWj`E@v9o+H0p9+ijWU2=>w(8`5-wjr{yN;Y3Xm$U!*@tznAWp?!h_t4(WD0U+@j-E7BKn{=G)} zr1VkgO6k4QyKpXkqjZsUzVvG871AD@kNc$^l8+}4PLXp@l!aBeo%a$ z_-^s-;+w>a#S6q&i!T?;;()kQ^l@fAN!%j7NbC}i6&+E-`SeJ!ARa0nB1&RLY|8&5 z|2WRC59aUB-<`iJ|HJ%u^WVV9_=f!F@}J6oH2;D8d+?VNZ^~bUXCYpdKPx|+@6X5i zm*roa@6B(@pO9ajKPGSGmHbipBl3smXXU5o^ZC|1mwPhzSnf}`-{pRt`$g`~-0it< z=f0l%QttZPHMvjZKAgKE_wL-=a&O4JHg|6BmAO5+q1@@YF!$2jDY>n=4Y(^gK6iA^ z$Suz;&b8wnWoB+#E}Lu4{xkbG+=D!n{Y~~)*`H;9oc&(*TiLH>znJ}e_Ui1%vmeCG z$)(x1WM7}XAp7d<%W)4gklmT}voFb>l-+`xov!S$StqMymu8R57P5zC56Mc|Otwk* zhw!-Yi146rzi_v3m+(W}<9tK7S-3&?obV~(qqyODkMIuRO~OUOYlK(f=4V*w7h>UM z!i$AoVUuuzuv$1qumlCqdK@7fF3b|93wfbc;4)7_3E2-rfFZyTUEV>lrSvdL;gzB!yi(BFl+L2G zjnbKv&Y<)VN)M)VI;96uI*n49Qi)QL(mbU(O0$#-lx8SRQ`$;t3#E7j6MaZg+C(W& zDMu*x45k01^l3`}LFrSJK1u1{Dg7IzPf+?erGKULF-reJ>7$hXnbJoneVEceQTj(p zAENXRls-u5?0HHU=6N9nIAy_eEoQF;%hcT@UHN`FD=&nf*Gr9Y+g zE=qqw>7A6`LFtbv{Sl=2;KTp3-Y6{T!vArSuv~KSSx& zlzy7hPf_|wN4z!(5Tzfa^h!!UKDwuN8>Mfh^evRWnbJ2=`o{m)-kZQ_a#eNz_4Ho7PBN3( zl1X}c5+*~^N$pEQNG6k-WC+<4NZ3NHGtJC&58XW^0RlV?LfAnCL6Jq?mqk<-K{gja zL=aI?B8t2U2!a~|uZjZd|9kGOr>A?8@bXXbE+42NR5EqzId$vYd(S=Rcb2QJFHzSQ ztLux@^@ZyC0(E`9y56I%&r{dus_Rdv>vPog+3LEYu3dF~)D)rF}WmzPTqBo9%H?XgJVGu9JALN5E{a=Bdg%H=Y-Tq>7` z%jFWeJWMVZ%jF`u#Bzz`63Qizi!T>XF0Nc0x!7{C>TMo1wXgSbwpyfczftCX;2U-ra9B4Vva-iiv%YpxE9O&;KsqOy{8dD$q zzt)ZR#VrR~4zwI-InZ*Ta$T&~l*VK+A#uZ5&v+yK|Z`(m!laPpy05X&vL5 zl}C3cMkrfz=cS+7`^tadpQ*<_Z6?hZ7v}FeI&;y?6*teEKeJRA%v|JLVBLN9`D=f$_xklexclzM7o4>-=va<3YdN#FKWzsWIqpSnctIG)u5F)Z z#TQxD(`K%jTUwgAD7Md^xu!e>Y4`k@>kiLcZhT)D7N%v@ygA5u+y<{~#bf9869 zyqLOulboV+Gq2G!_{Hj-tLAv#;rU0ViwDaS&)7V)dP_YN@{s!3<#K!#Kk7Zv`+IzM z>f2UW1wmMbo)g%9VcULQS-$VZUS7t56<4kwrCycC92S^YMUYo^oCkgt2bmjLrzrb> z>gxvow0~L-v>a$T&~l*VK+A!a11$$y4zwI-InZ*Ta$T&~l*Vz>gXSPSpQz{r^8|W7~dD%Yl{y zEeBc-v>a$T&~l*VK+A!a11$$y4zwJoIUxT3!PfuZ&WDx*EeBc-v>a$T&~l*VK+A!a z11$$y4zwI-Iq;*+f&PA@KL77~#?<$Iw7<3ex|Rbi2U-ra9B4Vva-iiv%Yl{yEeBc- zv>a$T@Z-vX{{9hD>i-)Xeq1@wes{}(mIEyZS`M@vXgSbwpyfczftCX;2U-ra9Qg6z zfY|@$$ZL#=?@in}{>|~@W8WO>nr}9@ajX5)a-iiv%Yl{yEeBc-v>f<9&Vi=|!&@%h z(dq7rCHzWt`dbMpu0tAzts+`g`1*S;H*y*DS*2d=#$xpn#hAD$Pu1c$DMp(erb6zots~tzW(Y}4#-pM9y_);cQjqRYx;0` z*K~S(d0}ooUo4N7^ULR-^vbJw;O6Akn^#|W@&l{i-s{@Q4?HS8a=cVD?%SdUhOI&da`>#k)E^crTS&z)JN zG_$AMafY|-<%JjC_l4KZADNpk^)IhjC1BMrz2({iH{8-&UhChz?uYmD{`Kv9o@(nZ z?7VLmF6TJ)qveb z8ZNm1F7%pD48(ye)^wuREQ7FyvJAt8!&~;zjdSk58@(T%)Qj2wMK4bL>;XEl=7BxW z>0B_pW%usR^Y&_?A~UAft~%TM%lY+Nd*Ws7gM68q&@=MgJ#ljFbvI96d&?sqIlb?? zYj3*w#=YF1o)H1E{+HL^c;K47H{LdVb#mME-dk?I?!dLY=$ho(o6pxId+)zL(rX@> zPmh)}TCDV5T(_=e`H9ZaG2Onlcl%H3-R-@Xul|ShV*hnFCI_y(R`vMYncgF`?3unX z*`M5)T)Qv1X?nFmhFvwXBCkjunXo(U+k4Z#y;meZKnGSkzkY75@51Us_vyoG%l&lX z#)MHhuJ6jO8>=u|J?V>eO2FCja7;LV}nL_qEot=m3y8OM^$d; zwil;WR(W|8$3d0Fj^~7F>Di?hcySiHUY>ffAIEkUM!u6pzCXBsgQAm$c?yNf%1S#A zy{xhdrz$-Jyi*j8Re6~oJCz-WS!R_{VdcK>^ZU^Dqs)p^w}1bHq9b?8!Y}|CFK=RYg(SL6rwq8aSaB z1zd~BO})}ibBjAp>WAF70>ALRkWOWlYnMD9@TRPP|G1)KJ6RBUt{+EP4kb5_ODpoD zs;r_g&9Xd9Z8uDdw4f`QWo1QR7k*WRMUh9IUpW2A#X`pnecK5#*GgTt%EQcY_~y`x zL&t`%wi8=k892Tl2euu!PMUjhoCc-qM?vA|RgoqaDKb%Bm0=#ZR^)kQSyn+>*hN_| zC_!jhewo7aG>@V(wHO2^@?BU{WO?S;fem|-Sdp<@Cx*w46}o&_RD^DsMNU>Yb`b=2 zY1@@uMkT{-1wjz`IekpyB7~@ur>W;Aks`zVuu?mYir9^dGIsnp^L#&rCsCGqRRkq3 za>KaH{HQ2+j~%5}>02-!Ze~^zDl(p%*=ZJKrC;#__-JQY8brAjrLnLZLO~k&g=K|> zpGGzxLVF1_ZQD*GFA4NVRUyyhvG3)18F`R_7ghxAB2M!x3N(KLzbb+#%gVAyEj!Ai zvdHOGUM9XG1FQ1Vi>)+u+$!=)i{CM7QRe4mLH}XBTNDLLz$smbFz~Q8VqBSXZs{kU zBIAdV>)9@!5=U`Jqg|Gc#{flXo`!Lpmo{ITXSrWMH_shb8k4WGEH7koWr?fE#8q55 zS!h>&mHBp+*@YKYUg4ybAH>kg3SqBn#W4f!!I#+c%P7ipTp=D-i(Q0TIhnKORO%*?{9;`K$D*osVLGtiOm*%gyCEUJvZ z@PTE;lyjpfD1)$!>1mJ#3<5lte!4*vxIt+#D2b)W*m<62WeWQ;7Ks~Isa1t#8M170 z$4Y%09@Cv7^sORv+$fJ_Jvc$g;>=hx$%Tqc$zt>ZL`mqSE&~{ZFxUx$Dy)jE46F#^ zP8A29XIo4hJ95f63(DMcm`Y`qCl@F(Zoq^Kd`2rMGdn2kJYbwG=1t&+h$o+p!sfs) zs|>jTUxM6aK01}>T7m5+vx*E4#Qdx%5K;)$iiM06MXWII^Uz_ef*^KWtKwe_Ds!kp z0;SAUm-!Jm$@z*59Q55V&qBl{V$aTf=9ZJPmYsl+W)vCPuteZNxhSF{4Sc6SGzOOE zrGA{8r#~vJT>j_7>6|Y4f$K3MmTQ#=cRt8rg@%EbhIUxRR>q74>``QxcLlOF zgUz{1KYfQO>Scu+$y#H){J^mp5^k5a6PAw4&fyoHr-W{pCFd$KNMV*@X|sF~eK66> z!aNW87*=!`k%Df@GV;%hj>Z03lwrX3=^=n!?xY2x&dON(9veherDerxE})ouh0W5fJSf5;&rW9a zM)puBE#DKd73X1)TkP*FqBQhbAfBJbaGE_ZKsNU!XQ}R>LR9P}vi}rJ&eEoL zVaZl+TeW^;75u_t2+N8&Agh3B$ZnUiOITrYrXrK&aqNl&VYl)sUW9lE89te6R++MI zRZ+xJW!I+D5i*t`^yzg7hl4CirWF~sk&+3{27u%$Ji3dFsC>(-SbYvnVyYy_+CGab^3 z3066*9uyI;NKV&en7~=?Ajy~~KKll$6q6P;CuM+msl~+MyHFxvJ9+{V5gBIXj&Rh@ zlI@BNtAlNU4Z`um5*Y~>BAew>proSgv7I;`Tdgn^@s_c1p>AY!1VuA&sv_B@$i!^S z@}cY~W##xK%bkskr9&5(3+N-hXN9)tdO?5$5hYBflp9%P8Ab8_twN@Qrkk_Z^3{=D zA%hVdL55o3WUgpK470=FMhI{;E*HrY@PA(BBOpUR^v9DesynDVXe~vFhQO3%>p{2_ z?0hM*kj2MxMbSg9r6?61T4j{Q9up~a(lGNqCrdVKGAKP6{Inb&^@wdgs@VD}nLMnY z5berGm4T%!JG2F)h3AHWom+8aTa}+Br)lQo5n~yJIT~bK6}BHEIT?#8LR3MOx1f9oT|vM3Ik*eR!5v+dDsZGyz=4*(d^h&fkMH2aXD8>Ql!KCCr?96p7Cri9CBhVMD( z8ij3R4j`hv*vok<^O~uHu8rCoWEcuTj*-RUMeSkLTZyU3&~ckBia|t0ioGHY(e)#@ zWXH1UQO*vX zQKgtKp;ve;S|{=tC?){bTa^qeGO|IUakC0VNzW>#SB4fC(=mqGmbFr*h(+c}DH0Gd z7lrI*2*jM_WhZ^QH%M3nEZYui0v!%U7pd!@IIx?b&_~#A9_n-)(>-=7n3QK|G65P7 zGS*Kzij2nsM*Tt457~(-H_fnygV?L8ppd?C+eS(27Yef=7EX+kM%#ZKkEBt z-!Jy%eP?v;?YyLOWydhy-`M^)jAT)LKYItpMH-?D!T*#M&gd~$yxh(r{)I4SAMgyOIN zhmdUr!j;`94$$t{BLOh%P?^@k8K#U|plfGrFE-+v*Qh1yr^!)8hF3A^_!3l0*e04z z#Xe#o7+DDwLQg;!L^_HL(=)2XB*Lmjgfs8bDmkLapmt!!vvDyl80W6<@f|E;Y+S4! zF_6&QSRXcfiW_EZ;<;$dSinUF#^EN16&W-+_IVZu3O5Ex8liN6tDw2C=8)4CYoARr zWG||O#hAjZM08M8Cg7Ved6FUnXUmLL2OkhdqKP;byA2=_(uHA@*?_8trXzZTRWc;# z10|*mtFnxoWKNOs3uG6YksDS3L4ZD1%vR(gbVGLBz{CEk_!(O`TSRGLq#%ruohWUX z2bh$H^hZ^ECvwI@fO-W}6r+%lWD^mq*#YLjhyhE=n7l}xBFrPu3QQ7q1<^#4gNh80 zLJk_iu2Dh(HOYd%83GKm1#KRbgQbB4M7CiyVV@31BD1eyOtEReV=Xx`;<)I)DTCU&ZDY$V-s7 z*g#-jB|B-6WQq)0V`PhdXa%C$;WRqL*aJ6BQYN6ZAVErpp38}a)yuLVjILx zvDom@L1js0u7hb@p;fVh!0j;YfJ|7t^cw`gOP-*}fOTf5?lazL@>a_c1CkssZajXCem-k)^>r z1SMjvqgt_)02pjLjx#rTobC9Zw#s$drBnd(k}TBia@)F@}4I7{ds`CS-U;D`EDdRBf|%EzQw^m=r)_3U?N6%Vc5~VCgvfZxJ#bfN*e{0OphcQV#0~ zAx{dK~ zXV_wxkQD|wb}rT@rm*0DEZXE&MaJ_A8Md2=iHKeC9WkAH(UP1H3 zc*Jr|Zc=2hQ5}Ri1}d6ljA0*w(fa_Kj8DmwV--|v8-TT#@M1DKD6N>)%t^Kpt4MBC zWI*HL6&5vma?bXg3RD#_-i60IV}8$QA^4fbVuGM$BFmA1fCyL+mYdw5$ONe1fW_!T zY-(a{$2^DhcfdM>)Whg!1z`FWm|beYfp>z|v6(Wdux66$6&WNRLdA(duvj8Mw2>v+ z4Jr&@$Umq6=(0eU6(0pERmjeY)J97|;YOxkr+LFN;Tv7{43;=50b&9~h9O5tB49x^ z3btn`Bg=eLS>~jJqX4zlu~^H=wTcYDj{_jhB=)gB1@1uKk9Zlv9-+v{K!Pn| z#DG;GF3@5Dk(h!7vrcS1EMw+5QUSM?%|0FR3Q=*G0~V7bIiScOdH`Y^*1UM6Jd`{j zGm&BdMQC(T=4s@E%k*UfwXnj0M@4-qK%{Ipxk`~i3#9{$X$X|aYXUyR#vy@pMWSHC zpj9I{JygEZLXXA9#^AKkco6$8!uU$f8z5v9()0GiB67F?J*Y)y<3>MveGuwNfU%5`%FM`k2=IqI$j z=n0y^HWeVYlcXl&!3|!4pkfhwA$nh6qn06;T_hfY6Z;=ogtTMs2#f>7g((Z<3}$HU zzf8z<=nQzIW7Cb0oz3?hbVEQnCZqsj^bMJe?SML#AygSOU`Wsaq@e}6ZVx0^sP0ts z7NLNaR0yPjw8cpUHXg!IKLxsp1su#}k!1+$5}OE35{HT>2x$-``xF_RipU4lV33D^ z2iSsesRDq23M!`vx|#1GVt{a6-@$}uP;l8~xD4>TImzXU4Erh@QX%_CR`9iDAwC0i z53$pkDIO|1YYv@80HKJz8z?YmJlV*=)x!OI6>~r$W%ZZh+QZ18$H;Zihad;C0{H`g z$i--88lWYBv7%;1fe1`i5BSQ!{)a0%^c3L%pduQ%i}wi`z_#T@F`WT_bAawZS>PJ_ z1pQxPTN<)oC^#uB4x0Fu+XU-JpX2;aG}d0wMO1XxwCHWU?3I z$-{(>iKxS1V$X=dPXHb8)_8P*F$b1I8kTH^=-4>BF@j4@NabT;X8({&Nus>IrMh}^PMyLqS2=h zy>aMqLz@TxuzzCk`Gflg9-!Lb{(S2{#B8| zJHx&Pd@FVq)0hr09}z$F0XRU!jVHcIbh{Kl0nH3tUQ`sB%S?LX>-wWg0Wl?xfX-H; zldv=3Y?CFyP9#__DlP;U(z3i{U755Ku2XwVL%B$!ZunU zL|HZkrgRAvfezVlUHovM4-S?Yng?4_Z2YArga3*ZjE^03nZ*=>h+}^^0U{w|^T(or z9oQf-Ryi0Nzh$Yhsl|XcU?s+1)V)EE5=X;*&zRL3u%A#>Pb70P;oKtI)7S zaYkUW_Ry$6H*mTbf3C=2H-KP^D^UDCfa3@yaipMgv5eW8SXF!q-->xFwj-DtKrhHE zMl6mzwLU|)0? z{W{OZZgcTvc^JF6D9}_`nygVgX6y~bY_Qd!r{LQLB;>Vt+#}=PbZ<)ZM3fKI5xh%4 zTP$m=dv@-iViPBHD8+ifHbjX7=mSu~C+cLV6L{hMFgISW1`1uMk!Esbk4htq?1=s( z^m&IRi=vJxfCA{D002V)ToV?B+K57nFpY4DqaXk}39KoIfY=QvhQ^0=XBsM^SL@Ei zgi~N%u+_yGrVJAZ_Z&tKmK{4=NH_)<6si{PC_q4_4Ksi-0_9{mS+4P0nhe%35Q`XJ zxcUmw3UN8BJs#|joty9+RyKyH2rOo(M;i_>>~f}i<{H1Pzp0^vc$e;sjq)s$oG2Sa zHPZ$i8Z#C97iR=BgUyw7z|<$yCKOjDn{o#3fa)UQBF4K^XHbAKbS)$>ZdxClEo4bJ zECb9Qphi{(Xe;`Cfi=Kf7xV^n#>2Gd%Q2Mi)>ImXqgXYSW}wUC6=M`uf!>2cgrbYq zOPB^eDn=Cp3>bpl2}PTQ;)#olt%Im3yx}ZCut}Df@jl&|hF0eFnhGP-#5J&3Q)w0_ z@sOHIv!IRLHI-)Q&uw~;Ffq_Hus9IRfO^<1#9g7d0sx|Y$Lwg>)tJZVtT>7Q1$_)N zjALL-oGXR#JDLo7u}E*?Ot8d|tQgVw0t%oBuy1j%3GhOIglysXGC(dd>tQE}Ng zjGxqNi4BAu9#{nRSX3K{4ntu>wZd{G77#cH#DWb3SQ{J6!ry{!k5^LA*~0F06l=gc zBp?BW=D~%}1)54jB;XoN1yhe8kAMJBG!Aw?)PHbFX1VD4z=j?`h3p^fOsGlNJea`Z zcqghb!)Lp!7z2(g2%Uh(XfhxQ2pG1vOdQdO6Sh_Ym0~bgv^5C~!<(8`U>tb=#M@mO zFRIC4fDut9&P}!8O1v9rmcU>xf)T(PO9(OSgA9QLV(PQO1D~Mii9J&p@6q4X(6w+- zcLux)vk&JzJ3ht^F%(P))Hu*Zz#WO`0?kD;Ml=E+u#3?*aA;svP?O9vUadRR5cYmz z-5DS3BW2%bSnvcBj)j4aO@`Qx5%9#};1x$n^no>fiF$zN;5R<|8R~mxJge?ZGw}R& z-5D$lFU*PTVL>sA@l+Dr1^7!a9UC6;MHnamM*>E<5}8OeDiK2fjv$9%aK^K1GWeWS zgf1>fFnR*AAcxN%OA5${twgkI9GNJD0;q|h#U^Y6+Tx)=MSFoJL*y;q1HKl|OC>55 zfoc)^Gu=ZKBLo>e8oPk#Qkj1NaT_t(1;HCASSaeoTQzGM20-o9oxv)@=E8G|pA_4g zCC(fHa|eSLYzX@g|G$gNKE{A%{a~wr2TF(;uu7I0zfhAQ6a-TMY$`&X%aJ&EuE0sa z1OWF4*RfzRhB~1N0N;EKa{||ShKVFzOXD~7H#H1Cj&*08N?cx;Cn~a2EOp=uWu#() zmIVB!xOoUX1OP=d7Ys~G7tAXeDJeFZyPFy&iuX+0P5TD zpkx3wA=nq_BdR=eo=_z$Rd4~|N4)i*_?Wz~1LqeGaG($|O)wX6S4YOTG#N3%nJEhA z$3z1v#-Gn7gMi5k%r`J=AU1UWNcks$@{oN1+aPQ3J~O_ohP|o7E>)euW=9bO<`OSR zNX&r;Mg~w$55U6E8F64>&;nR{EF@4qq*2ITmjm4q6=fM;9MHXK7$19{>WvFZDRw@% zgg8dfzc95hnVC@psp4n>gOz;;A00L|qL~<3fsMhqf!~7I_$&QUSoeUPKzNwI4~R44bGkP`8}JHQMXU@#*@y!?`N$6<9hn*IxDqXn zN6;f223r-3fH*KrA;NgDE(7DU^>{b)*F8pa821{^TtH0lVeuxgEb#$?{1qkc0}r62R0`N5ehOTnaFUpd7(g5xA19!Q z6RZz$OR$9k>Jwdu{cF5TlL3X6phQ#>e!YJETVpHLs0UZ&TP@+y!b{w=5vPLqzAY$$FeLjXh))?w3 zas-?LKQD2R#@7@X_FhrWn0bgR@frXz0%jw7*j50Xfgy8DLzEe1>k-xhSYiWlNHh>~ z-uN5MVMH0AAW{4*P!d##24^qJm>!ly`-mEe_Z_H$NJLy-pj1S6vDdRZBM;HpUTnV8 zcyS;8T!O>!h~))3BcU|l)u3`1sCW->HIw3jXOupmVGxQAs7}fZ-Vcc|z!PK}-&VcB zpWrd`O0ohF4&ak$shDtt4x+b{T7ohr0+X;e0+t9&A*%;B17N-=b3jIh<}lbHZhM(X zh!lwu1+_+*AhecgM5w2v8-Nd>h$7&HAjSk*Dv>_~@&r+5qUJDy2+syPCq8y)0@2{K z60b%IqAflW7Cu-j6CZ~gKtBDEm?t7+K|qSmkeW4MR`iz`B48eTII^+`?kBwjl%ar) zL}ig~BH^N9m*cd+0n1vV2|3Mvx}>NyP4}uli5PKqZ@d^b`Um1MB20!w4X96WE*cwN zU!Y5e5KC+@iRS=-#08rZ!H1lTjQ`XNp`pL|CQYSbI>`lk0)v$jx+-~Ze1IU(ght}e zK)7%d3xKFLyv7pCTH*2n!xK+ENp_f!#2#42ujWDubI(|Au&=yNR$HxHIA@3 z0WGljB(DIoO`;zNR&dZz2+BbDtcbHAR1cS$WO3Z78N)8Zq{qQZLJ(`7bx$G)!E3l$ zL!7h(-yrY4NAH*bZp!x$%~|GtJVV&e2pFy59F)f#=C=8WOI%(lF)H z(Nr2nWZkN%G|aFK^++S>h#6#}OI$cXK$uk$lTTDM7`2T-f;~oP1-Ka20zp-T$KwDc zas;nt7F5PhX)>%8g2@42Feb!$0-2Nq+8ocNgpI`@--0OM-XhBv2#@Hc5{Dv64Vp`l z8-Jq7;5;P6fVhvG} zjRJ=c>G=Pb8=bk%PUFv~-fz78L>++1sfmwIbjkkz=J;F3Q}O^lJ@)*uy`%p=`p(h0 z(Vga>n=dyHn4OXLk34x~&+r$AUp;)ousQVMp}U6c!M`1R!{BX$rwn{zpgR!vf3yFs z{b~QUzEAf(zi)5n-#hR8KGy&KJ9K;mAwlFAvw|_`3E5)nAybb~9Z44@*a$t97zVUP z{wlG*63Q(aH#<0C4Uyfq|G1)qECcqzl0kANJ@F;|m$%`*be2|1B5L!h71xR#}bah@TNVY6F zvRSY}Q=-MltC2ibQsfA=0X4u#!^i~(!1`whCHhf@Om-C3ah`N_Vk&NDQoDg2Bt=dV z%_V6bq(G(tnx0tWnEqs;l28YvD`pR_Pr@ru(6CnNkIqabAg+-KpLBI{Rj869Pqgg!qoc6N)W~^9cw7{nSB7qK+_pAkq9AvkgUssfL}7qK;rqx;i~o5>g4t zR|zHd4unT`2DcpYiXl8C_SCzzIR#PEm!T=_1Qb!%cE%6!NW18fDb z1cQ($AwpP43X}XWg2GeKbOFDaYlIn)))?aGA%}(7gE;A~Ca5;jyppcYP$l{hUlC4Z zRz1Um<|D~UB-4?(O^hxM0+1mzc#>;y>w}hO1>u^&`as5#D42A0iYk+eP;YXSBn?7R zjtQ$LAQW#if|PYfA|5Gp{I1L`@&B+oWpk8h7^x~yC0(7PN*q-qS5DGZNvb9&bpSTR zrx3^l^e1K^*#dYD$N}YJ*+>aBCO1_QpCkl~fg&Ubza!(0Low;$DZ~)MyDlbByDguhoe?*_i z9zv^#!628O3}dp4*;+_MU|T1lMgoh7olLqqPnE#fMxKJCTPLbERDY7L&Q!G8jjWllWftI|L5#EJPUzJ}TL};B3U(;g6A6La9JP3^xi!fZG8hj4)6{8ZJAF`J+k+ znK+CaVSq_jl>joEx{;HvDz#){A~kX~C0(5p%%0oGLz#4SQZS2%yb70?R>?ghDMUg! zW$Qv{qbVb?86Cofi8_%gEfTiI;KEc2VgU6re{@zb=q=fKEYy&7FTaN_dL?2Ke3A$^ zsd9prfOU+ozy>j8WLe4NMG)gl)Me7uX~CF_Iqp&kG-EWWMTG#s9OaFY#DnLOe+V{| z@H*rjP7DEZaXaDl#*=ENNq0!iXJU2n)`Bw&`YVD|!ng&B z6l5M?3qLs;Oo`kWRCEZ>vjITa({LVwJ`*^EWsKjQ`UeO#1TLYp#PLh)JuXy6MM6Lb z5GJrn>MkYS0Y!(n1tQYXuSwk|`6fbvCgV^-@&w`~f&eXn_*scsC34h}j209N?N@Y&B5&m2PP#g8T1rMp)+PER0Aqj&MZ_GRl}f${<49~Dp;LrMAWi%1+`^|qx*;Jk5})qDD)vO27L<)5c3NC=ls%#61X%E;h$5AM z;WA!^{w}tFCQ$^*GZh(Py-8OfLxp{Yj4R?S5rd%T{)CmtQg4s;&x5Uj;8;FUFe`R#! z2IB(b)t%0}JM#n9)a>L}CtolA{fSRaJa_Dd@$ZcN<@m3TA2D9t|Hl5ubso}xTHhb^ z-7_}YclpTZ@JENAF&qtjtN(9nkN#hc{`T0<41RX-<%5qH92oeGfjfv8SROy8^WD)) z&3`lBHDH;Ci81)Akymxz%}f7Re-ymYl-GADtVPnVh^avA6N-ilN(2Z~5HUtN4vu&R zo#{tN4~}QT5Sf3Nr-Vr1rk5B++xQnnMpOYJFsO?q8C$60R6Zdi8Pot#%LdPWgaSjB zAiDyn0x2$pBoNhxcL}V55Y%sI=RMKHji8M?RBxK@TgE+#43RKoJy17AQlqi2$s}eA z6$1+mnczT@yu<-T2&y=WQNbj<7UPNnaD@4j3Gnx7pqi$M9nizwFiG(SJx-*@69UC> zpss_~0;&-&2fix>p@4P3%#b?-D-l#faVoYyyj93pP$HhMLgU>vnPv)x@qA5&Y8{x7 zAbL4MPdw&WTL2Xpa-^>zFi6;@U}Hsn zQ&YMAG}W1=hL-UP-I<2@YM1NIpt@1{k;-=B)+DYXBH@A*PllV_1tpX{or*X<8!`cA zl2R%zYg}rm=|tM4#yfOp8kXERsyjotEheUDNs?+x<~H68$>?QnWl{u>ZH?`XuoMcc zu|FXi$!(F65t4OeRmR5@8O9!iCm^E+Fa&Iy{~|jJj9Y?Q*-zQ{P{)|bpaf(@qh$k+ z;4P0@G#ibV>&`SRDp6=E4J%r0uBkLDy*R6>G%V42v1YF% zM7p>s5L1ABARFivX!86Ot&EThbWjR{f|FyDV0Xom6-A{;%FvS9HB-I<1%qVaQ@GY#u=nVO0O>O7e8TzcZ> zl((b)57C~IZbLCbF_0ws84*i`N48k32ev)5FU)t0cmyM*k9a4^QhwPvMUw&4MTIBA zSn_)D@}cVUkQ6DB*c@zV)FD#nBtHk>SIT}e@0r#_F_CcObtcufG|le4M0W?1tV4LUmVDxlL;;>4~+`A1lj zBPBsxM7t1Qq(TcYg$X_-C6m}A3PZ5sNUCOqVK?H3F#b#R24EQ=q{JgmAS#iI&?1lk zJcJBgs%(&bfz1UNhxeUYRyaqAm?PQ%+ZB|7*BSqz$N=|J=?Hrq9GB>&fDlX-!A?%A zl;jRbutU?pHpSnU6LChAT1nnGfsc6M>4EV!O{Qs3+B?-iHLZy$mCOmpX_(Bdhr4;W zo}POROH$3%RGJlRxJr!@Wz9e&fI*48puixAJ{7ejBNt@@I7ZNOiW^`fk=sl9CWxJ6 z@Z$SpsZnddGX74J0TIS*rEnOgF8?DmnH*8jI(7^R{F8dBlqDhDkQ@i)TJh!XBs-HjMu5oG_4l$$eK#CVuZum|9_7$^}eZNQ|C^8Ve(az z*H4a2d}!j%32Xdo;1;Hw6&9~>F@(7>GoR{z(?9k{iBL*K{yp4At1zR`J0=ZT%I z#vd8?+}HR|x=+;TN<(ug>FSbJO=H`WuC8fCD08Ds`J}6hT2bPqQILGn)m5zs)FMP5 zlR*Gj0`tKd$bi7IB`y_Zi>ztfBjB?DdTeaCMx^8?kxJ107+NZfNEL37qttv>6-F8;)Ja!W>@rDy zYZSYmbainn{OF*SAyatX=j5TV^<3A>BLp;FvXb22MaR4T9ajBqEQ@{}T zHRC)iNV>YfRbEivjD%O>iR4rYR6L3RNgO!A zfaIG}G=yjgs!n22lU#z!nmlldD-%K_DK6sv)fKKvz$7wv2yxFT^~A43!sjUrf&-ga zMSQ^Iy%WlTt}1R5LI#LH052Bv2M=_{I@Kkvnp#pxSJ$}0-;9L7jR9m$jT8_FkP5oN zxJs2EDI_V893v{C9e8K3IPr&(5kRQ7xJ|%e`3cn}$+Ey@ zgovqoe+N=eZ{U&rH1heNNq;qsew*$ zMv#C?%^`{~b#y1Ggg)iN8^gGjo%OqG;NR|Rmz zM@T_F;&Tj$LW*(|KRVIpb zqp}-GSJlljDY4loYc}bsdRHbvdUAjU)-&bH@ZeD$f+S`v0_rs5Hev3|IU+cuFlVU{ z>`|o@(3IFKLTfO0aYyTVS5g;}(n?Yw6nonukrlTD#k{157H%D)yF}+Fc7qfdLbGsV zAf|{BA&Y@^UwTRRV#Q~qaHFJ=%Ke(-gu>y1t?#M^7~=V~}(`MTcr$ zjhsVC*Hv@~lx&pYlyn_Mhq8Z-O35T$TYpzWJS6E_iVlwSMh=Xmd!eEuMHQ%x2+&LD zjD!=i(5YU}XMFfik_~5u8 zB2yB*B^VDhi~3dgbfx4io-2@G$p=SwMU0SS9WW1P6&*}}wtif-Xk#3W#aSb&8X)Qi z5*F1_N}SV0vTlI{a|+{0iW&!AP~kuo)=0YND>_Y`AxZZH3nZW^rorz z4{asKe|G40q5$47`IzCSO>P|AH}R?dv5DtSTsHol!B3CBdwhO;_t+PQy|JGgyLtE{ zW0QR!9sT&|%IL!e-edlY`SyW%bIzRU|H8ocMm|5VyZ^=!bLbyOUO94o|Euq7@wazc z4*YO9pkO&7C<)yoAda$a6m}qvnGhI4!31`cBIZ;>)qR&a~LV{yD zr)o0flMp`$N{JjIGP#mMD<$O}Df2~gJwaipf^3aM)S=g7Ba#b=bBnTN1cDOH-_f2G z07=RmBABol5pPl_m1q$nt%=^J?gpj@#ugDf0?ZR?BIFT!_<-?Q5Qi3brc`f;g(CKX zSTKN3j3cVb6Uac25853eX(2id<`TF+m1Brc1^p+xl`vW=N&@jyPN_4d$xw6=F+l#c zn4J{c!2^sGB?f?4AqpZ$AsLBBLUF>vLLL*E01*riwp@;d>zJAh4i?}6PTRoSLCrYI z$-y`ZuHg`r;&~vlD5I1OLW#vVqRt#ZKY3D8yH=uxJA;}GX{S{0hC^t~I1Lft7>F)T zb96mQxDkd!z*J7W1QsCi=M;}8U5}D`RQgsv#_uUI% z5C($VDby|nnI&|e%`Y~-s>n2jAB;cJy=jO(7=NgH)6m#AeqWJkYU>-H)V*nF>KnhO z$TYRAjo(#dni`JAM>Ls+c9HQLnoL8p$oTb|H_faNop;1&SO&w6h0yfcRKVrP+Z=m1b|l3{m5ax-$SmxW*s!QL1O_&NQ^& zjkhZ@O|$8YSE_F!Fp*%z2Y)=`shUc|BQYMKsWd#CZK0;pEK%bbioH#fIgOtc`+s`) zd^!J5SND7E@CPOzJ-K1x6B8>F7mxqT_}fQ1w$C7KQv>a$T&~l*VK+A#u_c)-pT#lk9dKhR6PcSu)$@C@Z zPYx_45|OZY%4ZT1D`(|NT?I(i=~rY(LZjR=kzQo46Scu{f|B@3d?i01 z2v|5mci0I-!}ePlcCx$c_HNg5EUdY74js(&N#lCl6Ul! zFQ7V#9Pdhf6mU8@Fazf;@%K`-&vlIdS$`BIYsn-jDd0|EFkWgxV9-$j)5M!dyf2Xz z#P)(y${|+3cD#`+UCB*^j%|EJ=|-TpB-Tp>S^)!bRs8=vjQ7c!O2hLXAE&7_OksVx zrqb|$!$;Lrh@z3)D+16-bf!)UZY)Af@cqbfU(7qgKsd~ba2hgms4qap7NABsNuFsE zhSN2bW@kU%qNy}I>+=bE!Zh^C8NaN_Gz^+BKBUPsjEyrsqsTPPEH}D}!%Zt!9oJMy z^&^FWECK9X{2gTPJDf{NK_wED5mV$ZQ`!`d36TdB&E^Oy(m+X%B}mOC-YPm%e^SF@ zIL6OwG7W1E8E??v)bRMe{WX3FxSoAfTOdK4%Bq}RKsX{%26)am&4I#8 z$Z`@#@hVbditr|~&Tyx37>AS@lsGSrf$)9ZnTAn1#(O3H-|2kg|497*|9cwUk6(|H z6$h+w9~B8h*La3rOaxI*gI+D*Eul+y@x#~KCgBwbz0sp(OqNmmzhqLfObvq+P!uI5BYP@}_0 zlddl3#Mx1eP9sgas_wDbGzmEA>bg)(k1bBRs(!NBw02U`RrQn2ra)EFRrQliITx!5 zQk8TURoraT)1{K`lNFt&QD{l`n2rZ-SX?UUF6ek*%CB%BIAMo5X-+=Db2huPN|yex^NLQ>^Qx2XQALM3nvG6VPP#`F9g4?s zJ`5EM$cG_t91M&EF;0snor{Dt@*^msN#P5nAebl{6epnoUeZ0R=rpZ& zm2{t^=rql2Pr7r84g=IE{4nVr(*0?8fKk#tsOYGm&jxXfNw-pTnx2G|bW24C;Jgu} zoOBD_pN7Z4CEZ-nX<9)m>1K+K)$kzqq?;-_1P(VkQBM5-XAR$_{r|I*e?#@Z#|+;! zxrzFJ&zsmg{v8$Zf37~)Z*0Txhem&IWO(%1qnDWfYQCLV|6L=WA9=;dwLgA#bo<3E z2U-ra9B4Vva^OL6K;idIQMAseBGVMK>I`c#4KbNcpC;3|_+L$?nZ|GYgC^6~5Ip&a z6XUg-GYv}#8J}04thN*(1ztIKg>-OAvy-4!RU?`))Hk8lEAcKN2OHa1jFiv^M+3^F zeh5i#l=h=`KE+e1G()lJf|4mxMw9bNsNxY>qne7Gb55EvRr@GQPCN{i#N`}-h;lPT z4e@VWzExaMN`~Ma(w|dKKc;2?<@iWg^PER(DpW-$OPYi3C|*NN6iOvfNuJ6hz^fHC z45?904O$A)0CP*td^*M1taJu^fEWS4Pf@{!BAboGVlZ0rw<)c{@e3S;l5#=|M>5MP zOB{hBdD&Ee#cNB+7Ea6sPA7bb^u^NERB&fXObVqmN{XvXd=#gjQih*HQaFqWq=+vD zQ0IhQ$^%ky@1MLsb;cD6eFJ~!nOg%ZZ z#2hh7xz6mQG$Zqno|6pAAy*t!Ax;zfGEKz=NFvdQ$Tkl4Az70TQ`)1cIMhex2yJzg4rjt}0v>6f_*XdG0OoMw2X%5> znR}F_l={`I1ES#McvI+b@R(*y!xMsxpHZE`{|=rMa3TRc~k@U$O zCITfy$@iu9h$koe5D^3us3ZU?mrd1FnjJ>)RP{+s59ctRBmV#AbslC+{q@wRrrtC4 zhN%}%-8EHC-7pnS?V6gJ{4SLMJ~{cW$)BHm;p82YkDoj+dBNnw#8)RiI`OuNmrOim z;t3P`CmuR6NuI!G$3HOs#_{KnD{$TTdE=YLzCHH2u@8;CdF^ONSg%-5KAlSgoc`A~Di$iH$zz(+>jI`UH^%Okgr z#3MU~|AQKX?-+jh@bTf>h95qBHfIceW9So{Iq69sDpS4t58h zFt~5f8a#7wc;KG~{&e6s2i`RByn!PFw+zGsXATVZ|3m+u^uLdqgwO7OLjS}1&*=Mh z-@SeB?0Z??T;COa)1B{jKGS(e=lPuycKkqp9wJA-^j&Ql_b!!3%6xflVSe`LLNQw% zNtX^CnLBuB`7-Cu3y#en+$m?f^zAi`KVAReq4Y?#Ja@E|XU>*$Tl%gvjnCfyGpnTV z=LxAlJNdE2VsRlqtV!Xk6HVbzI~?g7biV7#{{2WeII@{dEZ~Je`v8h zlHR#W2p^gd`uzH5=7k0M!lL@VbMTM#9Wagm@c_@%w9dl))pv<${N+iHEmY~;5&5_o z{95v^zq-E%Y9iD4toklBjW4W!=2*E1I=n1YcHxccOHAX7_xnUm=1g2oeHWX?m)1X| zK3#}R<3f_x{M(84#dN+{I6AxVlyXtM2Ir8x=HKu4iTX9CKiLCv_(Q;W2+BTdV@)ooAi=)g9{g&o!vY*<=O7uBbo0=(U~)9jqGbjQNN(jlR_4WwE=YpDM5Y__nxyr4#DC*U)Sr!&xN zN{<~|TuAdnCr0Z`aAlcf-#vNsdy^2nxO0g)wRB{zC>Nz4%lgN4`>Ei-eY;HKKlB5c zeEwo;3t0Ps&>8824?i-+nrUM?0Z8t{+(QAwqqIYNDOQoVZWpnjV}%rO-}n-m$b$ zEzjx)?>IEKw9I^D6waakXXk2j!vj&Md$EP`o-#8!iprt6<@`u`OpPM-I%O35dJWaw z{G1-(t(4&G*yd8NWqx7tXnI8bh@zTsbh+2Era#-MliBgjie|%y-$evQBOKKP%ay`v#IiFtNTr3>Vj+C;O)Ze-5I-W!F{LuqF zCtN@CS{`EVy%Di>cy9jS>`hnAF7atcY9iaOq1`o&*Yw)gNYEXRq>X<5{7G%SpI)th z`r2OeO`5WrN{`S#ecefI^-niZk+E;LX}o^5S$?#I`icyk@rKpA_2Z{sNdw~Z7gihU zkC0!Twx32;!hbu_D7UvJ+`iB>ezte}SUO*-?Pco~y5+z3T5h_ke$Uo@y5&v1mgPB_ zdQdz4a$4xYo7c7U7_pZ|c=(oHoI~-e!_@fTy0j2pfH~>;%%#U zdyJsco^<2qdJXEM>c%rEpC?28>R#i~^v-W1G?+Z_A ztADzQ>TtrM7p*oc7E$X>SoGr6yS+D2yiMNpl3qjo74oA^jwbc8)iyP@Hh1~3SM~1p z_@>@t>G!<6*SN3uo(uJRezMna#hNd=fHuD9r%r0?ebFq-Dmy zTy8&0_wskwHq_nNex`2wM6XRgt!~@AgSPWb<0Gqm%nnK)H=m*J|6ctY^&W)$=F|24 zPuBMpm$-lWcJ)nLHHT-5xvHAWj~`jSOLm4W+i1KH9Yq-%v*n$v??M`OZiN~u`tPqF znDjW>P`!Jfy@f}vG>uQ)|0DHhoVA&!P`>|Q{Zo2`<$JcB#sju#{PvpftbcpzCYsPg zKCGH%mEE&-Blnn`Z$B}SdQYFKpMI}?+7;p(PT}5JrtywNj$l=SW8((yoMRgARCns1 zZkggHQ|w)fCECdny6vLYVz*Dyz!dw{wGF-ZP0&W~-mTwf$@@+n*Z1C|@7e0!n7;RG z`ktCCr;h4-@6-1@d2Xx8J-+Ni`X2jYUd}7a;?xoS^!s&-uc(Th$lE`l?*;0fn1tN> zpuQKXdtw4|@7MLcNZk{Ak9)tN@5OR&v)Fe`x8KzFkmE;FxxYyaI%LOh>HEck>e1;~ za4eLu`Gsk%hfJgTO>5QdLb3m!+nF_{J~#E=saH?~;M%G4rp(E|oBZv`*G)cavY5PV z@~lZ?;>(-}@VbeoPTV?i!Nl`~VxpT3(RcofTa;yg1 z5dm?!1SK;Cmg{!Z-Zh%)MhP|U>^7^T<^1vdi0GkMMB62B7zK77?X#R+PUj9UoY=!o zk!aw~F7u%L1Zwhw(K|^A@K%KCf@)P?+3Sx4 ztkR#Q6FfVi_ivRbRNgnX3USQ#?1=3WSjz9#G@Y;{wn(%nuUpf2C|#Bz+A8s%>nR?t zErP8Q(n-^?6Fup@Y@0-BG9jP5zDW)Gc8R>CBa0{6PIP3Ggj06*n0wVY%^s@}g3S^g ziJ@KJUO;XiR4VQU33u%5G`HSysAu+`DUpke=w+IOepVqL^`5gqLK6AS={aWnvJ$Tw zCAyHQc80!Hv-%W?6=XK;(07%gzCq&t`0<(Qo|3~GC4`S3pQUfD{`g62e(cmgo>M=b zR!^$UWuwH&@#CHP*6NQBsvpnjyBZ>p2sU`I3k7y*VUd}%a9HtRn}npn?6cQ4)Py!j zm>JzYN4JSE-Y5}cyzwFWR{h{<690vm*sbp_FV?WuMv3R*>2vXkr0n>!YKz(|r=rl8 zJ*#(XG8^TX6CQtPuR*bQlN?&Ys7@-)KBNC?1K1>IkZ^nZn%gYcO>(3Nx3}rr?6GR$ zpC%`S(8E)+j6vq#EGQL*I2k4|ci-|s$hAG2Fn5X#{modt8^2m8qyTi=)CJUWNX z$?x;aRRCkn?=FzzJ35d5!G0%Qbmg3m&OvkP`@CokR#CJ6A~}_#bKKnUgZ--RVEyA$ zr^=uf-mXaotK+C5UaFYM{%%92F-rXMeHcBaV9w%H}KdxT7Sqh``IK}zv zyEU^nN~LoiC;Lk`s7bV0>X!4kyLxx^@mcjaXISdT)#TYMWyN`1%8)Cu-hX(u4cfHmK14l!@#@{x z$InrZKTK@cB?Zj0n74G#=I%+oQCruR?ymc`s9kD%cLpUEL+v>|vvBVTb!|s?Cyjss zKXFoH-HlCMvE9*tpS#+wr-oSWsLan>yrlR2}C(J$E9KdgI5uWI@5o!#2_|Hu1zpeC}dE7m_I$k*3DBfmeRD>i><8vnGu zZT0#~tZUXQS-)TGe!z!s-p>Q8?-v|^_kU~sGy4641n~aCUcz0+9db z6U__9#M64B3uku)CSdIT<^G?kNuALZ;D9&%>wO-Rj-1sM_<#ZX_I;Xb0@GcrKk@hd z&50*48)oa7Bt&Mq7=bdTUs?Z%av3@`owK@P5%Q)l-`_*JFJc$+#OKyOaY%V)<%#L8 zn27Y{Pu4#nZi4zXySid4GVwpN{*e>jX(1%`zd!ot#?)7*K0fs;Q$L0GKbyL2>Z~ba z@=KE+oqYS`^Czpx8zsO}-#327*uRba+1LlieqroIW5>tRv8%^C6#?+~ysG`va-iiv%Yl{yEeBc- zv>a$T&~o61&w;5GV41!hX6IDgD)Sc*%yY(yU@>tI9TCiXEyS^LMF5%I<}LU$IC18n zMqp1_5hP}pd24-FbmmPfg2C)DZ>#Us3irkpL0?u09PSYi%w?DP82zvc(A=~lc*`F1 zvGu)G0s^w^HpjVrBC=}piU2IL=G5xl`tj{604qwI2e$_~sMVJ$ATM?ynsyg27yD-hOi9 z>Ng7-v)g>4c#_ong~OXy1d5q8)7886#Jia;^v^hR2b)6|L}F=v@q^PH~`jPVtLV78gb!3Fu%)QSKvXPNsC z_F`r>u4uqz>mjDnat25s_~wQc0bF*PS3=0rv?o^tYT034CAWmuDJz1q>@p9im+6N# ztq8!f$9zP6ukQb;D}u0`V?I*du5WHy5pZRXc{4XTq51IYvmh%xdyT%W>7KR%q5|l5 zZSUqPV?a{q=XLtNbZg@ZC<=pdy}ngHxN!vpg}XQCyY;-@yaHmv5Z$tWN5h4qtd*9ve*=Ss7<|3_B85Ilr0EafR7ARIq^1q=ii-_x{* z6|8xAZvJ>L67KXB&<|RlQv1QQr=k~bcg70%2SQ=J>r`5m6Yb0u@DE_jJJ$Vpjq~)Z z74Q$B)AhfvA5bJVt$=pw^MxJ9a^ o@uR2frZwyR45@#{cK~WhaN=4QumatstQn(qxBj8_Tnokj50VjuX#fBK literal 0 HcmV?d00001 From b1cf014dc21a2dd61040522df015de5e4b6ed02a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 16:18:39 +0900 Subject: [PATCH 077/306] Add test coverage of EF to Realm migration process --- .../Navigation/TestEFToRealmMigration.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs b/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs new file mode 100644 index 0000000000..00a06d420e --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Models; +using osu.Game.Scoring; +using osu.Game.Skinning; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestEFToRealmMigration : OsuGameTestScene + { + public override void RecycleLocalStorage(bool isDisposing) + { + base.RecycleLocalStorage(isDisposing); + + if (isDisposing) + return; + + using (var outStream = LocalStorage.GetStream(DatabaseContextFactory.DATABASE_NAME, FileAccess.Write, FileMode.Create)) + using (var stream = TestResources.OpenResource(DatabaseContextFactory.DATABASE_NAME)) + stream.CopyTo(outStream); + } + + [Test] + public void TestMigration() + { + // Numbers are taken from the test database (see commit f03de16ee5a46deac3b5f2ca1edfba5c4c5dca7d). + AddAssert("Check beatmaps", () => Game.Dependencies.Get().Run(r => r.All().Count(s => !s.Protected) == 1)); + AddAssert("Check skins", () => Game.Dependencies.Get().Run(r => r.All().Count(s => !s.Protected) == 1)); + AddAssert("Check scores", () => Game.Dependencies.Get().Run(r => r.All().Count() == 1)); + + // One extra file is created during realm migration / startup due to the circles intro import. + AddAssert("Check files", () => Game.Dependencies.Get().Run(r => r.All().Count() == 271)); + } + } +} From d79845fb1df28e63545aa0f4bf9552e16965d9bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 17:06:33 +0900 Subject: [PATCH 078/306] Revert `NUnit3TestAdaptor` (again) Console output is still broken. See https://github.com/ppy/osu/runs/5196023462?check_suite_focus=true. --- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 2bdb6a650c..434c0e0367 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index a6c614f22f..fc6d900567 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index ff49d6d4dd..ddad2adfea 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -4,7 +4,7 @@ - + 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 a2e54f5cdc..bd4c3d3345 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 @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index debddae037..a6b8eb8651 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 40969c8b29..acf1e8470b 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 9fd73f2c1b..c7314a4969 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe From 03106e846c6a6a70c944253a333aa922c1c2bde7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 17:13:31 +0900 Subject: [PATCH 079/306] Fix test failures due to async mod icon loads --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 4dd3427bee..7c18ed2572 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -173,7 +173,8 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddAssert("mod select contains only double time mod", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Mod is OsuModDoubleTime); + AddUntilStep("mod select contains only double time mod", + () => this.ChildrenOfType().SingleOrDefault()?.ChildrenOfType().SingleOrDefault()?.Mod is OsuModDoubleTime); } } } From b2276baf71bf461c0ba67d2ca6eb634c1ba9299a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 20:55:56 +0900 Subject: [PATCH 080/306] Seal OnlinePlayTestScene.CreateChildDependencies() --- osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 430aae72f8..df2b4f6192 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay }); } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent)); return dependencies; From c48a0dc993e4b5621ba9f92625f486d93229ea94 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 20:56:46 +0900 Subject: [PATCH 081/306] Move UserLookupCache to online play test dependencies --- .../Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs | 4 ++-- .../TestSceneMultiplayerGameplayLeaderboardTeams.cs | 2 +- .../Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs | 6 ------ osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 1 - .../Visual/Multiplayer/MultiplayerTestSceneDependencies.cs | 4 ---- .../Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs | 5 +++++ osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs | 1 + .../Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs | 4 ++++ 8 files changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 1322fbc96e..437f4e3308 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).GetResultSafely()); + AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = UserLookupCache.GetUserAsync(1).GetResultSafely()); AddStep("create leaderboard", () => { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestUserQuit() { foreach (int user in users) - AddStep($"mark user {user} quit", () => Client.RemoveUser(LookupCache.GetUserAsync(user).GetResultSafely().AsNonNull())); + AddStep($"mark user {user} quit", () => Client.RemoveUser(UserLookupCache.GetUserAsync(user).GetResultSafely().AsNonNull())); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 8a78c12042..cd3ae50dab 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).GetResultSafely()); + AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = UserLookupCache.GetUserAsync(1).GetResultSafely()); AddStep("create leaderboard", () => { diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs index 204c189591..f166154103 100644 --- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.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.Game.Database; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; @@ -24,11 +23,6 @@ namespace osu.Game.Tests.Visual.Multiplayer /// new TestMultiplayerRoomManager RoomManager { get; } - /// - /// The cached . - /// - TestUserLookupCache LookupCache { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index ed86d572b9..a9b3ca4991 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -19,7 +19,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestMultiplayerClient Client => OnlinePlayDependencies.Client; public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; - public TestUserLookupCache LookupCache => OnlinePlayDependencies?.LookupCache; public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient; protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs index ed349a7103..d9fe77ae44 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.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.Game.Database; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Screens.OnlinePlay; @@ -16,18 +15,15 @@ namespace osu.Game.Tests.Visual.Multiplayer public class MultiplayerTestSceneDependencies : OnlinePlayTestSceneDependencies, IMultiplayerTestSceneDependencies { public TestMultiplayerClient Client { get; } - public TestUserLookupCache LookupCache { get; } public TestSpectatorClient SpectatorClient { get; } public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager; public MultiplayerTestSceneDependencies() { Client = new TestMultiplayerClient(RoomManager); - LookupCache = new TestUserLookupCache(); SpectatorClient = CreateSpectatorClient(); CacheAs(Client); - CacheAs(LookupCache); CacheAs(SpectatorClient); } diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 71acefb158..feb9b55743 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -31,5 +31,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// The cached . /// OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } + + /// + /// The cached . + /// + TestUserLookupCache UserLookupCache { get; } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index df2b4f6192..99a492cd6d 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -22,6 +22,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public IRoomManager RoomManager => OnlinePlayDependencies?.RoomManager; public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies?.OngoingOperationTracker; public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies?.AvailabilityTracker; + public TestUserLookupCache UserLookupCache => OnlinePlayDependencies?.UserLookupCache; /// /// All dependencies required for online play components and screens. diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index 24c4ff79d4..47893519c7 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay; @@ -22,6 +23,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public OngoingOperationTracker OngoingOperationTracker { get; } public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public TestRoomRequestsHandler RequestsHandler { get; } + public TestUserLookupCache UserLookupCache { get; } /// /// All cached dependencies which are also components. @@ -38,6 +40,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); RoomManager = CreateRoomManager(); + UserLookupCache = new TestUserLookupCache(); dependencies = new DependencyContainer(new CachedModelDependencyContainer(null) { Model = { BindTarget = SelectedRoom } }); @@ -47,6 +50,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay CacheAs(OngoingOperationTracker); CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); + CacheAs(UserLookupCache); } public object Get(Type type) From 2675bb87ff3009322be4dcad2e3244ef33425fd3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 21:05:05 +0900 Subject: [PATCH 082/306] Add BeatmapLookupCache as another dependency --- .../Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs | 6 ++++++ osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs | 2 ++ .../Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs | 3 +++ 3 files changed, 11 insertions(+) diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index feb9b55743..c94e288e11 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; @@ -36,5 +37,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// The cached . /// TestUserLookupCache UserLookupCache { get; } + + /// + /// The cached . + /// + BeatmapLookupCache BeatmapLookupCache { get; } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 99a492cd6d..baff7c168f 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; @@ -23,6 +24,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies?.OngoingOperationTracker; public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies?.AvailabilityTracker; public TestUserLookupCache UserLookupCache => OnlinePlayDependencies?.UserLookupCache; + public BeatmapLookupCache BeatmapLookupCache => OnlinePlayDependencies?.BeatmapLookupCache; /// /// All dependencies required for online play components and screens. diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index 47893519c7..7c8bc2d535 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -24,6 +24,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public TestRoomRequestsHandler RequestsHandler { get; } public TestUserLookupCache UserLookupCache { get; } + public BeatmapLookupCache BeatmapLookupCache { get; } /// /// All cached dependencies which are also components. @@ -41,6 +42,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); RoomManager = CreateRoomManager(); UserLookupCache = new TestUserLookupCache(); + BeatmapLookupCache = new BeatmapLookupCache(); dependencies = new DependencyContainer(new CachedModelDependencyContainer(null) { Model = { BindTarget = SelectedRoom } }); @@ -51,6 +53,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); CacheAs(UserLookupCache); + CacheAs(BeatmapLookupCache); } public object Get(Type type) From 539cbe62c6be88f6da5b6cabc1df39f845f8b58b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 21:08:27 +0900 Subject: [PATCH 083/306] Fix incorrect usages of user lookup cache in tests --- .../Visual/Gameplay/TestSceneSpectator.cs | 25 +++++++++---------- .../Visual/Multiplayer/QueueModeTestScene.cs | 4 --- .../TestSceneDrawableRoomPlaylist.cs | 5 +--- .../Multiplayer/TestSceneMultiplayer.cs | 4 --- .../TestSceneMultiplayerQueueList.cs | 4 --- .../TestScenePlaylistsRoomSettingsPlaylist.cs | 8 ++---- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 4 --- .../Visual/TestMultiplayerComponents.cs | 9 +++++++ 8 files changed, 24 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 157c248d69..6dca256d31 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -39,7 +39,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuGameBase game { get; set; } - private TestSpectatorClient spectatorClient; + private TestSpectatorClient spectatorClient => dependenciesScreen.Client; + private DependenciesScreen dependenciesScreen; private SoloSpectator spectatorScreen; private BeatmapSetInfo importedBeatmap; @@ -48,16 +49,16 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUpSteps] public void SetupSteps() { - DependenciesScreen dependenciesScreen = null; - AddStep("load dependencies", () => { - spectatorClient = new TestSpectatorClient(); + LoadScreen(dependenciesScreen = new DependenciesScreen()); - // The screen gets suspended so it stops receiving updates. - Child = spectatorClient; - - LoadScreen(dependenciesScreen = new DependenciesScreen(spectatorClient)); + // The dependencies screen gets suspended so it stops receiving updates. So its children are manually added to the test scene instead. + Children = new Drawable[] + { + dependenciesScreen.UserLookupCache, + dependenciesScreen.Client, + }; }); AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); @@ -335,12 +336,10 @@ namespace osu.Game.Tests.Visual.Gameplay private class DependenciesScreen : OsuScreen { [Cached(typeof(SpectatorClient))] - public readonly TestSpectatorClient Client; + public readonly TestSpectatorClient Client = new TestSpectatorClient(); - public DependenciesScreen(TestSpectatorClient client) - { - Client = client; - } + [Cached(typeof(UserLookupCache))] + public readonly TestUserLookupCache UserLookupCache = new TestUserLookupCache(); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 36d6c6a306..52801dd57a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -10,7 +10,6 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -41,9 +40,6 @@ namespace osu.Game.Tests.Visual.Multiplayer protected TestMultiplayerClient Client => multiplayerComponents.Client; - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 659cc22350..8c10a0d0d9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -30,16 +30,13 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneDrawableRoomPlaylist : OsuManualInputManagerTestScene + public class TestSceneDrawableRoomPlaylist : MultiplayerTestScene { private TestPlaylist playlist; private BeatmapManager manager; private RulesetStore rulesets; - [Cached(typeof(UserLookupCache))] - private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache(); - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 41715f6cfb..b04bf5e860 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -17,7 +17,6 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -56,9 +55,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerClient client => multiplayerComponents.Client; private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager; - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index ddf794b437..4040707c41 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -26,9 +25,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerQueueList : MultiplayerTestScene { - [Cached(typeof(UserLookupCache))] - private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache(); - private MultiplayerQueueList playlist; private BeatmapManager beatmaps; private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index 8bfdda29d5..9adf2c0370 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -4,13 +4,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; -using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Models; using osu.Game.Online.API; @@ -20,18 +18,16 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.OnlinePlay; using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestScenePlaylistsRoomSettingsPlaylist : OsuManualInputManagerTestScene + public class TestScenePlaylistsRoomSettingsPlaylist : OnlinePlayTestScene { private TestPlaylist playlist; - [Cached(typeof(UserLookupCache))] - private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache(); - [Test] public void TestItemRemovedOnDeletion() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 513c1413fa..b66657728e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -10,7 +10,6 @@ using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; @@ -36,9 +35,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerClient client => multiplayerComponents.Client; - [Cached(typeof(UserLookupCache))] - private UserLookupCache lookupCache = new TestUserLookupCache(); - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { diff --git a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs index cd7a936778..bd8fb8e58e 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Screens; @@ -39,6 +40,12 @@ namespace osu.Game.Tests.Visual [Cached(typeof(MultiplayerClient))] public readonly TestMultiplayerClient Client; + [Cached(typeof(UserLookupCache))] + private readonly UserLookupCache userLookupCache = new TestUserLookupCache(); + + [Cached] + private readonly BeatmapLookupCache beatmapLookupCache = new BeatmapLookupCache(); + private readonly OsuScreenStack screenStack; private readonly TestMultiplayer multiplayerScreen; @@ -48,6 +55,8 @@ namespace osu.Game.Tests.Visual InternalChildren = new Drawable[] { + userLookupCache, + beatmapLookupCache, Client = new TestMultiplayerClient(RoomManager), screenStack = new OsuScreenStack { From a5183cec77caaa7a8fdb64a8d7a8a7418b7c7f42 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 21:53:56 +0900 Subject: [PATCH 084/306] Add helper to construct APIBeatmap --- osu.Game/Tests/Visual/OsuTestScene.cs | 62 ++++++++++++++++----------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index ec02655544..f287a04d71 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -225,12 +225,24 @@ namespace osu.Game.Tests.Visual protected virtual IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset); /// - /// Returns a sample API Beatmap with BeatmapSet populated. + /// Returns a sample API beatmap with a populated beatmap set. /// /// The ruleset to create the sample model using. osu! ruleset will be used if not specified. - protected APIBeatmap CreateAPIBeatmap(RulesetInfo ruleset = null) + protected APIBeatmap CreateAPIBeatmap(RulesetInfo ruleset = null) => CreateAPIBeatmap(CreateBeatmap(ruleset ?? Ruleset.Value).BeatmapInfo); + + /// + /// Constructs a sample API beatmap set containing a beatmap. + /// + /// The ruleset to create the sample model using. osu! ruleset will be used if not specified. + protected APIBeatmapSet CreateAPIBeatmapSet(RulesetInfo ruleset = null) => CreateAPIBeatmapSet(CreateBeatmap(ruleset ?? Ruleset.Value).BeatmapInfo); + + /// + /// Constructs a sample API beatmap with a populated beatmap set from a given source beatmap. + /// + /// The source beatmap. + public static APIBeatmap CreateAPIBeatmap(IBeatmapInfo original) { - var beatmapSet = CreateAPIBeatmapSet(ruleset ?? Ruleset.Value); + var beatmapSet = CreateAPIBeatmapSet(original); // Avoid circular reference. var beatmap = beatmapSet.Beatmaps.First(); @@ -243,18 +255,16 @@ namespace osu.Game.Tests.Visual } /// - /// Returns a sample API BeatmapSet with beatmaps populated. + /// Constructs a sample API beatmap set containing a beatmap from a given source beatmap. /// - /// The ruleset to create the sample model using. osu! ruleset will be used if not specified. - protected APIBeatmapSet CreateAPIBeatmapSet(RulesetInfo ruleset = null) + /// The source beatmap. + public static APIBeatmapSet CreateAPIBeatmapSet(IBeatmapInfo original) { - var beatmap = CreateBeatmap(ruleset ?? Ruleset.Value).BeatmapInfo; - - Debug.Assert(beatmap.BeatmapSet != null); + Debug.Assert(original.BeatmapSet != null); return new APIBeatmapSet { - OnlineID = ((IBeatmapSetInfo)beatmap.BeatmapSet).OnlineID, + OnlineID = original.BeatmapSet.OnlineID, Status = BeatmapOnlineStatus.Ranked, Covers = new BeatmapSetOnlineCovers { @@ -262,29 +272,29 @@ namespace osu.Game.Tests.Visual Card = "https://assets.ppy.sh/beatmaps/163112/covers/card.jpg", List = "https://assets.ppy.sh/beatmaps/163112/covers/list.jpg" }, - Title = beatmap.Metadata.Title, - TitleUnicode = beatmap.Metadata.TitleUnicode, - Artist = beatmap.Metadata.Artist, - ArtistUnicode = beatmap.Metadata.ArtistUnicode, + Title = original.Metadata.Title, + TitleUnicode = original.Metadata.TitleUnicode, + Artist = original.Metadata.Artist, + ArtistUnicode = original.Metadata.ArtistUnicode, Author = new APIUser { - Username = beatmap.Metadata.Author.Username, - Id = beatmap.Metadata.Author.OnlineID + Username = original.Metadata.Author.Username, + Id = original.Metadata.Author.OnlineID }, - Source = beatmap.Metadata.Source, - Tags = beatmap.Metadata.Tags, + Source = original.Metadata.Source, + Tags = original.Metadata.Tags, Beatmaps = new[] { new APIBeatmap { - OnlineID = ((IBeatmapInfo)beatmap).OnlineID, - OnlineBeatmapSetID = ((IBeatmapSetInfo)beatmap.BeatmapSet).OnlineID, - Status = beatmap.Status, - Checksum = beatmap.MD5Hash, - AuthorID = beatmap.Metadata.Author.OnlineID, - RulesetID = beatmap.Ruleset.OnlineID, - StarRating = beatmap.StarRating, - DifficultyName = beatmap.DifficultyName, + OnlineID = original.OnlineID, + OnlineBeatmapSetID = original.BeatmapSet.OnlineID, + Status = ((BeatmapInfo)original).Status, + Checksum = original.MD5Hash, + AuthorID = original.Metadata.Author.OnlineID, + RulesetID = original.Ruleset.OnlineID, + StarRating = original.StarRating, + DifficultyName = original.DifficultyName, } } }; From ccd265ebe7237e50ecb7241dca5e3f3703b47a86 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 22:02:33 +0900 Subject: [PATCH 085/306] Handle beatmap lookup requests in TestRoomRequestsHandler --- .../Visual/TestMultiplayerComponents.cs | 8 ++++-- .../Online/API/Requests/GetBeatmapsRequest.cs | 6 ++-- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 11 +++++--- .../OnlinePlay/TestRoomRequestsHandler.cs | 28 +++++++++++++++++-- 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs index bd8fb8e58e..2e551947b6 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Database; +using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Screens; @@ -46,6 +47,9 @@ namespace osu.Game.Tests.Visual [Cached] private readonly BeatmapLookupCache beatmapLookupCache = new BeatmapLookupCache(); + [Resolved] + private BeatmapManager beatmapManager { get; set; } + private readonly OsuScreenStack screenStack; private readonly TestMultiplayer multiplayerScreen; @@ -69,9 +73,9 @@ namespace osu.Game.Tests.Visual } [BackgroundDependencyLoader] - private void load(IAPIProvider api, OsuGameBase game) + private void load(IAPIProvider api) { - ((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, game); + ((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, beatmapManager); } public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton(); diff --git a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs index 1d71e22b77..c07e5ef1c2 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs @@ -7,7 +7,7 @@ namespace osu.Game.Online.API.Requests { public class GetBeatmapsRequest : APIRequest { - private readonly int[] beatmapIds; + public readonly int[] BeatmapIds; private const int max_ids_per_request = 50; @@ -16,9 +16,9 @@ namespace osu.Game.Online.API.Requests if (beatmapIds.Length > max_ids_per_request) throw new ArgumentException($"{nameof(GetBeatmapsRequest)} calls only support up to {max_ids_per_request} IDs at once"); - this.beatmapIds = beatmapIds; + BeatmapIds = beatmapIds; } - protected override string Target => "beatmaps/?ids[]=" + string.Join("&ids[]=", beatmapIds); + protected override string Target => "beatmaps/?ids[]=" + string.Join("&ids[]=", BeatmapIds); } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index baff7c168f..448ec5e3d9 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Database; +using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; @@ -33,9 +34,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay protected override Container Content => content; - [Resolved] - private OsuGameBase game { get; set; } - private readonly Container content; private readonly Container drawableDependenciesContainer; private DelegatedDependencyContainer dependencies; @@ -71,7 +69,12 @@ namespace osu.Game.Tests.Visual.OnlinePlay AddStep("setup API", () => { var handler = OnlinePlayDependencies.RequestsHandler; - ((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, game); + + // Resolving the BeatmapManager in the test scene will inject the game-wide BeatmapManager, while many test scenes cache their own BeatmapManager instead. + // To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead. + var beatmapManager = dependencies.Get(); + + ((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); }); } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 5a0a7e71d4..8290af8f78 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -4,12 +4,16 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.OnlinePlay { @@ -33,9 +37,9 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// /// The API request to handle. /// The local user to store in responses where required. - /// The game base for cases where actual online requests need to be sent. + /// The beatmap manager to attempt to retrieve beatmaps from, prior to returning dummy beatmaps. /// Whether the request was successfully handled. - public bool HandleRequest(APIRequest request, APIUser localUser, OsuGameBase game) + public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager) { switch (request) { @@ -128,6 +132,26 @@ namespace osu.Game.Tests.Visual.OnlinePlay Statistics = new Dictionary() }); return true; + + case GetBeatmapsRequest getBeatmapsRequest: + var result = new List(); + + foreach (int id in getBeatmapsRequest.BeatmapIds) + { + var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id); + + if (baseBeatmap == null) + { + baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo; + baseBeatmap.OnlineID = id; + baseBeatmap.BeatmapSet!.OnlineID = id; + } + + result.Add(OsuTestScene.CreateAPIBeatmap(baseBeatmap)); + } + + getBeatmapsRequest.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = result }); + return true; } return false; From 73ce1b324e84e5eef58ddf9ef5010e69a36938c7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 22:44:20 +0900 Subject: [PATCH 086/306] Make DrawableRoom look up the online beatmap --- .../Lounge/Components/DrawableRoom.cs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index a87f21630c..60f65fbd9f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -12,6 +14,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -328,6 +331,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved] private OsuColour colours { get; set; } + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } + private SpriteText statusText; private LinkFlowContainer beatmapText; @@ -385,8 +391,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components SelectedItem.BindValueChanged(onSelectedItemChanged, true); } + private CancellationTokenSource beatmapLookupCancellation; + private void onSelectedItemChanged(ValueChangedEvent item) { + beatmapLookupCancellation?.Cancel(); beatmapText.Clear(); if (Type.Value == MatchType.Playlists) @@ -395,17 +404,25 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return; } - if (item.NewValue?.Beatmap.Value != null) - { - statusText.Text = "Currently playing "; - beatmapText.AddLink(item.NewValue.Beatmap.Value.GetDisplayTitleRomanisable(), - LinkAction.OpenBeatmap, - item.NewValue.Beatmap.Value.OnlineID.ToString(), - creationParameters: s => - { - s.Truncate = true; - }); - } + var beatmap = item.NewValue?.Beatmap; + if (beatmap == null) + return; + + var cancellationSource = beatmapLookupCancellation = new CancellationTokenSource(); + beatmapLookupCache.GetBeatmapAsync(beatmap.Value.OnlineID, cancellationSource.Token) + .ContinueWith(task => Schedule(() => + { + if (cancellationSource.IsCancellationRequested) + return; + + var retrievedBeatmap = task.GetResultSafely(); + + statusText.Text = "Currently playing "; + beatmapText.AddLink(retrievedBeatmap.GetDisplayTitleRomanisable(), + LinkAction.OpenBeatmap, + retrievedBeatmap.OnlineID.ToString(), + creationParameters: s => s.Truncate = true); + }), cancellationSource.Token); } } From afcb7a463061756988648e60f06a0ad9173f43ba Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:11:33 +0900 Subject: [PATCH 087/306] Make DrawableRoomPlaylistItem look up the online beatmap --- .../OnlinePlayBeatmapAvailabilityTracker.cs | 23 ++-- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 102 ++++++++---------- 2 files changed, 57 insertions(+), 68 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index c67cbade6a..51e77694a1 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -78,7 +78,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap.Value), (items, changes, ___) => { if (changes == null) return; @@ -108,12 +108,12 @@ namespace osu.Game.Online.Rooms break; case DownloadState.LocallyAvailable: - bool hashMatches = filteredBeatmaps().Any(); + bool available = QueryBeatmapForOnlinePlay(realm.Realm, SelectedItem.Value.Beatmap.Value).Any(); - availability.Value = hashMatches ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); + availability.Value = available ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); // only display a message to the user if a download seems to have just completed. - if (!hashMatches && downloadTracker.Progress.Value == 1) + if (!available && downloadTracker.Progress.Value == 1) Logger.Log("The imported beatmap set does not match the online version.", LoggingTarget.Runtime, LogLevel.Important); break; @@ -123,14 +123,15 @@ namespace osu.Game.Online.Rooms } } - private IQueryable filteredBeatmaps() + /// + /// Performs a query for a local matching a requested one for the purpose of online play. + /// + /// The realm to query from. + /// The requested beatmap. + /// A beatmap query. + public static IQueryable QueryBeatmapForOnlinePlay(Realm realm, IBeatmapInfo beatmap) { - int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID; - string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; - - return realm.Realm - .All() - .Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum); + return realm.All().Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", beatmap.OnlineID, beatmap.MD5Hash); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index dcf2a5a480..7674fac88e 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -25,7 +24,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.Chat; -using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.BeatmapSet; using osu.Game.Rulesets; @@ -68,8 +66,8 @@ namespace osu.Game.Screens.OnlinePlay private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; private readonly IBindable valid = new Bindable(); - private readonly Bindable beatmap = new Bindable(); + private IBeatmapInfo beatmap; private IRulesetInfo ruleset; private Mod[] requiredMods; @@ -96,13 +94,12 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private UserLookupCache userLookupCache { get; set; } - [CanBeNull] - [Resolved(CanBeNull = true)] - private MultiplayerClient multiplayerClient { get; set; } - [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } + [Resolved] + private RealmAccess realm { get; set; } + protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model; public DrawableRoomPlaylistItem(PlaylistItem item) @@ -110,7 +107,6 @@ namespace osu.Game.Screens.OnlinePlay { Item = item; - beatmap.BindTo(item.Beatmap); valid.BindTo(item.Valid); if (item.Expired) @@ -151,7 +147,6 @@ namespace osu.Game.Screens.OnlinePlay maskingContainer.BorderThickness = isCurrent ? 5 : 0; }, true); - beatmap.BindValueChanged(_ => Scheduler.AddOnce(refresh)); valid.BindValueChanged(_ => Scheduler.AddOnce(refresh)); onScreenLoader.DelayedLoadStarted += _ => @@ -166,19 +161,9 @@ namespace osu.Game.Screens.OnlinePlay Schedule(() => ownerAvatar.User = foundUser); } - if (Item.Beatmap.Value == null) - { - IBeatmapInfo foundBeatmap; + beatmap = await beatmapLookupCache.GetBeatmapAsync(Item.Beatmap.Value.OnlineID).ConfigureAwait(false); - if (multiplayerClient != null) - // This call can eventually go away (and use the else case below). - // Currently required only due to the method being overridden to provide special behaviour in tests. - foundBeatmap = await multiplayerClient.GetAPIBeatmap(Item.BeatmapID).ConfigureAwait(false); - else - foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false); - - Schedule(() => Item.Beatmap.Value = foundBeatmap); - } + Scheduler.AddOnce(refresh); } catch (Exception e) { @@ -280,18 +265,18 @@ namespace osu.Game.Screens.OnlinePlay maskingContainer.BorderColour = colours.Red; } - if (Item.Beatmap.Value != null) - difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; + if (beatmap != null) + difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; else difficultyIconContainer.Clear(); - panelBackground.Beatmap.Value = Item.Beatmap.Value; + panelBackground.Beatmap.Value = beatmap; beatmapText.Clear(); - if (Item.Beatmap.Value != null) + if (beatmap != null) { - beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineID.ToString(), null, text => + beatmapText.AddLink(beatmap.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, beatmap.OnlineID.ToString(), null, text => { text.Truncate = true; }); @@ -299,13 +284,13 @@ namespace osu.Game.Screens.OnlinePlay authorText.Clear(); - if (!string.IsNullOrEmpty(Item.Beatmap.Value?.Metadata.Author.Username)) + if (!string.IsNullOrEmpty(beatmap?.Metadata.Author.Username)) { authorText.AddText("mapped by "); - authorText.AddUserLink(Item.Beatmap.Value.Metadata.Author); + authorText.AddUserLink(beatmap.Metadata.Author); } - bool hasExplicitContent = (Item.Beatmap.Value?.BeatmapSet as IBeatmapSetOnlineInfo)?.HasExplicitContent == true; + bool hasExplicitContent = (beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.HasExplicitContent == true; explicitContentPill.Alpha = hasExplicitContent ? 1 : 0; modDisplay.Current.Value = requiredMods.ToArray(); @@ -448,31 +433,34 @@ namespace osu.Game.Screens.OnlinePlay }; } - private IEnumerable createButtons() => new[] + private IEnumerable createButtons() { - showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) + return new[] { - Size = new Vector2(30, 30), - Action = () => RequestResults?.Invoke(Item), - Alpha = AllowShowingResults ? 1 : 0, - TooltipText = "View results" - }, - Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item), - editButton = new PlaylistEditButton - { - Size = new Vector2(30, 30), - Alpha = AllowEditing ? 1 : 0, - Action = () => RequestEdit?.Invoke(Item), - TooltipText = "Edit" - }, - removeButton = new PlaylistRemoveButton - { - Size = new Vector2(30, 30), - Alpha = AllowDeletion ? 1 : 0, - Action = () => RequestDeletion?.Invoke(Item), - TooltipText = "Remove from playlist" - }, - }; + showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) + { + Size = new Vector2(30, 30), + Action = () => RequestResults?.Invoke(Item), + Alpha = AllowShowingResults ? 1 : 0, + TooltipText = "View results" + }, + OnlinePlayBeatmapAvailabilityTracker.QueryBeatmapForOnlinePlay(realm.Realm, beatmap).Any() ? Empty() : new PlaylistDownloadButton(beatmap), + editButton = new PlaylistEditButton + { + Size = new Vector2(30, 30), + Alpha = AllowEditing ? 1 : 0, + Action = () => RequestEdit?.Invoke(Item), + TooltipText = "Edit" + }, + removeButton = new PlaylistRemoveButton + { + Size = new Vector2(30, 30), + Alpha = AllowDeletion ? 1 : 0, + Action = () => RequestDeletion?.Invoke(Item), + TooltipText = "Remove from playlist" + }, + }; + } protected override bool OnClick(ClickEvent e) { @@ -499,7 +487,7 @@ namespace osu.Game.Screens.OnlinePlay private sealed class PlaylistDownloadButton : BeatmapDownloadButton { - private readonly PlaylistItem playlistItem; + private readonly IBeatmapInfo beatmap; [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -509,10 +497,10 @@ namespace osu.Game.Screens.OnlinePlay private const float width = 50; - public PlaylistDownloadButton(PlaylistItem playlistItem) - : base(playlistItem.Beatmap.Value.BeatmapSet) + public PlaylistDownloadButton(IBeatmapInfo beatmap) + : base(beatmap.BeatmapSet) { - this.playlistItem = playlistItem; + this.beatmap = beatmap; Size = new Vector2(width, 30); Alpha = 0; @@ -532,7 +520,7 @@ namespace osu.Game.Screens.OnlinePlay { case DownloadState.LocallyAvailable: // Perform a local query of the beatmap by beatmap checksum, and reset the state if not matching. - if (beatmapManager.QueryBeatmap(b => b.MD5Hash == playlistItem.Beatmap.Value.MD5Hash) == null) + if (beatmapManager.QueryBeatmap(b => b.MD5Hash == beatmap.MD5Hash) == null) State.Value = DownloadState.NotDownloaded; else { From 94a974e1c97ea36318488ba9c4c17db1d7ea9c67 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:30:39 +0900 Subject: [PATCH 088/306] Make OnlinePlayBeatmapAvailabilityTracker look up the online beatmap --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 57 ++++++++++++--- .../OnlinePlayBeatmapAvailabilityTracker.cs | 73 ++++++++++++------- 2 files changed, 93 insertions(+), 37 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 9aa04dda92..4b98385a51 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.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.Diagnostics; using System.IO; using System.Threading; @@ -12,6 +13,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Graphics; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Framework.Testing; @@ -21,6 +23,8 @@ using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Tests.Resources; @@ -53,6 +57,25 @@ namespace osu.Game.Tests.Online [SetUp] public void SetUp() => Schedule(() => { + ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case GetBeatmapsRequest beatmapsReq: + var beatmap = CreateAPIBeatmap(); + beatmap.OnlineID = testBeatmapInfo.OnlineID; + beatmap.OnlineBeatmapSetID = testBeatmapSet.OnlineID; + beatmap.Checksum = testBeatmapInfo.MD5Hash; + beatmap.BeatmapSet!.OnlineID = testBeatmapSet.OnlineID; + + beatmapsReq.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = new List { beatmap } }); + return true; + + default: + return false; + } + }; + beatmaps.AllowImport = new TaskCompletionSource(); testBeatmapFile = TestResources.GetQuickTestBeatmapForImport(); @@ -69,12 +92,30 @@ namespace osu.Game.Tests.Online RulesetID = testBeatmapInfo.Ruleset.OnlineID, }; - Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker - { - SelectedItem = { BindTarget = selectedItem, } - }; + recreateChildren(); }); + private void recreateChildren() + { + var beatmapLookupCache = new BeatmapLookupCache(); + + Child = new DependencyProvidingContainer + { + CachedDependencies = new[] + { + (typeof(BeatmapLookupCache), (object)beatmapLookupCache) + }, + Children = new Drawable[] + { + beatmapLookupCache, + availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker + { + SelectedItem = { BindTarget = selectedItem, } + } + } + }; + } + [Test] public void TestBeatmapDownloadingFlow() { @@ -123,10 +164,7 @@ namespace osu.Game.Tests.Online }); addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded); - AddStep("recreate tracker", () => Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker - { - SelectedItem = { BindTarget = selectedItem } - }); + AddStep("recreate tracker", recreateChildren); addAvailabilityCheckStep("state not downloaded as well", BeatmapAvailability.NotDownloaded); AddStep("reimport original beatmap", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely()); @@ -167,7 +205,8 @@ namespace osu.Game.Tests.Online public Live CurrentImport { get; private set; } - public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) + public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, + GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap) { } diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 51e77694a1..90532d88b8 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -4,14 +4,17 @@ using System; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; using Realms; namespace osu.Game.Online.Rooms @@ -32,6 +35,9 @@ namespace osu.Game.Online.Rooms [Resolved] private RealmAccess realm { get; set; } = null!; + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + /// /// The availability state of the currently selected playlist item. /// @@ -58,39 +64,50 @@ namespace osu.Game.Online.Rooms downloadTracker?.RemoveAndDisposeImmediately(); - Debug.Assert(item.NewValue.Beatmap.Value.BeatmapSet != null); - - downloadTracker = new BeatmapDownloadTracker(item.NewValue.Beatmap.Value.BeatmapSet); - - AddInternal(downloadTracker); - - downloadTracker.State.BindValueChanged(_ => Scheduler.AddOnce(updateAvailability), true); - downloadTracker.Progress.BindValueChanged(_ => + beatmapLookupCache.GetBeatmapAsync(item.NewValue.Beatmap.Value.OnlineID).ContinueWith(task => Schedule(() => { - if (downloadTracker.State.Value != DownloadState.Downloading) - return; + var beatmap = task.GetResultSafely(); - // incoming progress changes are going to be at a very high rate. - // we don't want to flood the network with this, so rate limit how often we send progress updates. - if (progressUpdate?.Completed != false) - progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); - }, true); - - // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). - realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap.Value), (items, changes, ___) => - { - if (changes == null) - return; - - Scheduler.AddOnce(updateAvailability); - }); + if (SelectedItem.Value?.Beatmap.Value.OnlineID == beatmap.OnlineID) + beginTracking(beatmap); + }), TaskContinuationOptions.OnlyOnRanToCompletion); }, true); } - private void updateAvailability() + private void beginTracking(APIBeatmap beatmap) { - if (downloadTracker == null || SelectedItem.Value == null) + Debug.Assert(beatmap.BeatmapSet != null); + + downloadTracker = new BeatmapDownloadTracker(beatmap.BeatmapSet); + + AddInternal(downloadTracker); + + downloadTracker.State.BindValueChanged(_ => Scheduler.AddOnce(updateAvailability, beatmap), true); + downloadTracker.Progress.BindValueChanged(_ => + { + if (downloadTracker.State.Value != DownloadState.Downloading) + return; + + // incoming progress changes are going to be at a very high rate. + // we don't want to flood the network with this, so rate limit how often we send progress updates. + if (progressUpdate?.Completed != false) + progressUpdate = Scheduler.AddDelayed(updateAvailability, beatmap, progressUpdate == null ? 0 : 500); + }, true); + + // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). + realmSubscription?.Dispose(); + realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap.Value), (items, changes, ___) => + { + if (changes == null) + return; + + Scheduler.AddOnce(updateAvailability, beatmap); + }); + } + + private void updateAvailability(APIBeatmap beatmap) + { + if (downloadTracker == null) return; switch (downloadTracker.State.Value) @@ -108,7 +125,7 @@ namespace osu.Game.Online.Rooms break; case DownloadState.LocallyAvailable: - bool available = QueryBeatmapForOnlinePlay(realm.Realm, SelectedItem.Value.Beatmap.Value).Any(); + bool available = QueryBeatmapForOnlinePlay(realm.Realm, beatmap).Any(); availability.Value = available ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); From bdc3b76df0b58406796e2b08db13be7f2140fa7e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:33:26 +0900 Subject: [PATCH 089/306] Remove beatmap bindable from PlaylistItem --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 3 +- .../OnlinePlay/PlaylistExtensionsTest.cs | 25 ++--- .../Visual/Multiplayer/QueueModeTestScene.cs | 3 +- .../Multiplayer/TestSceneDrawableRoom.cs | 68 ++++-------- .../TestSceneDrawableRoomPlaylist.cs | 37 +++---- .../Multiplayer/TestSceneHostOnlyQueueMode.cs | 8 +- .../TestSceneMatchBeatmapDetailArea.cs | 3 +- .../Multiplayer/TestSceneMultiplayer.cs | 101 +++++++----------- .../TestSceneMultiplayerMatchSubScreen.cs | 15 +-- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 3 +- .../TestSceneMultiplayerPlaylist.cs | 12 +-- .../TestSceneMultiplayerQueueList.cs | 6 +- .../TestSceneMultiplayerReadyButton.cs | 3 +- .../TestSceneMultiplayerResults.cs | 7 +- .../TestSceneMultiplayerSpectateButton.cs | 3 +- .../TestSceneMultiplayerTeamResults.cs | 7 +- .../TestScenePlaylistsRoomSettingsPlaylist.cs | 38 +++---- .../TestSceneStarRatingRangeDisplay.cs | 4 +- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 16 ++- .../TestScenePlaylistsMatchSettingsOverlay.cs | 8 +- .../TestScenePlaylistsResultsScreen.cs | 3 +- .../TestScenePlaylistsRoomCreation.cs | 31 +++--- .../Online/Multiplayer/MultiplayerClient.cs | 11 +- .../Multiplayer/OnlineMultiplayerClient.cs | 10 -- .../Online/Rooms/MultiplayerPlaylistItem.cs | 4 +- .../OnlinePlayBeatmapAvailabilityTracker.cs | 6 +- osu.Game/Online/Rooms/PlaylistExtensions.cs | 2 +- osu.Game/Online/Rooms/PlaylistItem.cs | 74 +++++++++---- .../OnlinePlay/Components/BeatmapTitle.cs | 4 +- .../OnlinePlay/Components/ModeTypeInfo.cs | 2 +- .../Components/OnlinePlayBackgroundScreen.cs | 2 +- .../Components/OnlinePlayBackgroundSprite.cs | 2 +- .../Components/PlaylistItemBackground.cs | 2 +- .../OnlinePlay/Components/RoomManager.cs | 3 - .../Components/StarRatingRangeDisplay.cs | 2 +- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- .../Lounge/Components/DrawableRoom.cs | 2 +- .../OnlinePlay/Match/DrawableMatchRoom.cs | 2 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- .../Multiplayer/MultiplayerMatchSongSelect.cs | 4 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 34 +----- .../OnlinePlay/OnlinePlaySongSelect.cs | 6 +- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- .../Playlists/PlaylistsRoomSettingsOverlay.cs | 2 +- .../Playlists/PlaylistsSongSelect.cs | 3 +- .../Multiplayer/MultiplayerTestScene.cs | 3 +- .../Multiplayer/TestMultiplayerClient.cs | 23 ---- .../Visual/OnlinePlay/TestRoomManager.cs | 9 +- 48 files changed, 227 insertions(+), 395 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 4b98385a51..dd7feb6699 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -86,9 +86,8 @@ namespace osu.Game.Tests.Online Realm.Write(r => r.RemoveAll()); Realm.Write(r => r.RemoveAll()); - selectedItem.Value = new PlaylistItem + selectedItem.Value = new PlaylistItem(testBeatmapInfo) { - Beatmap = { Value = testBeatmapInfo }, RulesetID = testBeatmapInfo.Ruleset.OnlineID, }; diff --git a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs index d33081662d..9e7ea02101 100644 --- a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs +++ b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs @@ -3,6 +3,7 @@ using System; using NUnit.Framework; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; namespace osu.Game.Tests.OnlinePlay @@ -29,9 +30,9 @@ namespace osu.Game.Tests.OnlinePlay { var items = new[] { - new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1 }, - new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2 }, - new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, PlaylistOrder = 1 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, PlaylistOrder = 2 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1003 }) { ID = 3, PlaylistOrder = 3 }, }; Assert.Multiple(() => @@ -47,9 +48,9 @@ namespace osu.Game.Tests.OnlinePlay { var items = new[] { - new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2 }, - new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1 }, - new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, PlaylistOrder = 2 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, PlaylistOrder = 1 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1003 }) { ID = 3, PlaylistOrder = 3 }, }; Assert.Multiple(() => @@ -65,9 +66,9 @@ namespace osu.Game.Tests.OnlinePlay { var items = new[] { - new PlaylistItem { ID = 1, BeatmapID = 1001, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) }, - new PlaylistItem { ID = 2, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) }, - new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 }, + new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) }, + new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) }, + new PlaylistItem(new APIBeatmap { OnlineID = 1003 }) { ID = 3, PlaylistOrder = 3 }, }; Assert.Multiple(() => @@ -83,9 +84,9 @@ namespace osu.Game.Tests.OnlinePlay { var items = new[] { - new PlaylistItem { ID = 1, BeatmapID = 1001, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) }, - new PlaylistItem { ID = 2, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) }, - new PlaylistItem { ID = 3, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 57, 0, TimeSpan.Zero) }, + new PlaylistItem(new APIBeatmap { OnlineID = 1001 }) { ID = 1, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) }, + new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 2, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) }, + new PlaylistItem(new APIBeatmap { OnlineID = 1002 }) { ID = 3, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 57, 0, TimeSpan.Zero) }, }; Assert.Multiple(() => diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 52801dd57a..5a211e7bec 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -71,9 +71,8 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = Mode }, Playlist = { - new PlaylistItem + new PlaylistItem(InitialBeatmap) { - Beatmap = { Value = InitialBeatmap }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index 423822cbe4..d8ec0ad1f0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -53,19 +53,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.HeadToHead }, Playlist = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) { - Beatmap = + BeatmapInfo = { - Value = new TestBeatmap(new OsuRuleset().RulesetInfo) - { - BeatmapInfo = - { - StarRating = 2.5 - } - }.BeatmapInfo, + StarRating = 2.5 } - } + }.BeatmapInfo) } }), createLoungeRoom(new Room @@ -76,26 +70,20 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.HeadToHead }, Playlist = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) { - Beatmap = + BeatmapInfo = { - Value = new TestBeatmap(new OsuRuleset().RulesetInfo) + StarRating = 2.5, + Metadata = { - BeatmapInfo = - { - StarRating = 2.5, - Metadata = - { - Artist = "very very very very very very very very very long artist", - ArtistUnicode = "very very very very very very very very very long artist", - Title = "very very very very very very very very very very very long title", - TitleUnicode = "very very very very very very very very very very very long title", - } - } - }.BeatmapInfo, + Artist = "very very very very very very very very very long artist", + ArtistUnicode = "very very very very very very very very very long artist", + Title = "very very very very very very very very very very very long title", + TitleUnicode = "very very very very very very very very very very very long title", + } } - } + }.BeatmapInfo) } }), createLoungeRoom(new Room @@ -105,32 +93,20 @@ namespace osu.Game.Tests.Visual.Multiplayer EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, Playlist = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) { - Beatmap = + BeatmapInfo = { - Value = new TestBeatmap(new OsuRuleset().RulesetInfo) - { - BeatmapInfo = - { - StarRating = 2.5 - } - }.BeatmapInfo, + StarRating = 2.5 } - }, - new PlaylistItem + }.BeatmapInfo), + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) { - Beatmap = + BeatmapInfo = { - Value = new TestBeatmap(new OsuRuleset().RulesetInfo) - { - BeatmapInfo = - { - StarRating = 4.5 - } - }.BeatmapInfo, + StarRating = 4.5 } - } + }.BeatmapInfo) } }), createLoungeRoom(new Room diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 8c10a0d0d9..bea15e6e58 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -209,10 +209,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Size = new Vector2(500, 300), Items = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { ID = 0, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, Expired = true, RequiredMods = new[] @@ -222,10 +221,9 @@ namespace osu.Game.Tests.Visual.Multiplayer new APIMod(new OsuModAutoplay()) } }, - new PlaylistItem + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { ID = 1, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { @@ -293,25 +291,21 @@ namespace osu.Game.Tests.Visual.Multiplayer for (int i = 0; i < 20; i++) { - playlist.Items.Add(new PlaylistItem + playlist.Items.Add(new PlaylistItem(i % 2 == 1 + ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo + : new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "Artist", + Author = new RealmUser { Username = "Creator name here" }, + Title = "Long title used to check background colour", + }, + BeatmapSet = new BeatmapSetInfo() + }) { ID = i, OwnerID = 2, - Beatmap = - { - Value = i % 2 == 1 - ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo - : new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Artist = "Artist", - Author = new RealmUser { Username = "Creator name here" }, - Title = "Long title used to check background colour", - }, - BeatmapSet = new BeatmapSetInfo() - } - }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { @@ -341,11 +335,10 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (var b in beatmaps()) { - playlist.Items.Add(new PlaylistItem + playlist.Items.Add(new PlaylistItem(b) { ID = index++, OwnerID = 2, - Beatmap = { Value = b }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index c7eeff81fe..7614e52218 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -57,12 +57,12 @@ namespace osu.Game.Tests.Visual.Multiplayer RunGameplay(); IBeatmapInfo firstBeatmap = null; - AddStep("get first playlist item beatmap", () => firstBeatmap = Client.APIRoom?.Playlist[0].Beatmap.Value); + AddStep("get first playlist item beatmap", () => firstBeatmap = Client.APIRoom?.Playlist[0].Beatmap); selectNewItem(() => OtherBeatmap); - AddAssert("first playlist item hasn't changed", () => Client.APIRoom?.Playlist[0].Beatmap.Value == firstBeatmap); - AddAssert("second playlist item changed", () => Client.APIRoom?.Playlist[1].Beatmap.Value != firstBeatmap); + AddAssert("first playlist item hasn't changed", () => Client.APIRoom?.Playlist[0].Beatmap == firstBeatmap); + AddAssert("second playlist item changed", () => Client.APIRoom?.Playlist[1].Beatmap != firstBeatmap); } [Test] @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap())); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); - AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value?.BeatmapID == otherBeatmap.OnlineID); + AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value?.Beatmap.OnlineID == otherBeatmap.OnlineID); } private void addItem(Func beatmap) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 6144824ba0..6f43511e8a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -32,10 +32,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createNewItem() { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { ID = SelectedRoom.Value.Playlist.Count, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index b04bf5e860..4d87480841 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -93,9 +93,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -229,9 +228,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -251,9 +249,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -281,9 +278,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -312,9 +308,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Password = { Value = "password" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -334,9 +329,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Password = { Value = "password" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -367,9 +361,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Password = { Value = "password" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -387,9 +380,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -409,9 +401,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -448,9 +439,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -487,9 +477,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -526,9 +515,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -560,9 +548,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -600,9 +587,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -620,9 +606,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, AllowedMods = new[] { new APIMod(new OsuModHidden()) } } @@ -660,10 +645,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -691,10 +675,9 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = QueueMode.AllPlayers }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }, API.LocalUser.Value); @@ -708,11 +691,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change server-side settings", () => { roomManager.ServerSideRooms[0].Name.Value = "New name"; - roomManager.ServerSideRooms[0].Playlist.Add(new PlaylistItem + roomManager.ServerSideRooms[0].Playlist.Add(new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { ID = 2, - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, }); }); @@ -737,10 +719,9 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = QueueMode.AllPlayers }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -773,10 +754,9 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = QueueMode.AllPlayers }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -812,10 +792,9 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = QueueMode.AllPlayers }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -823,10 +802,11 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem - { - BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID - }))); + AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem( + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + }))); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); @@ -843,10 +823,9 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = { Value = QueueMode.AllPlayers }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -854,10 +833,11 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem - { - BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID - }))); + AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem( + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + }))); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); @@ -876,9 +856,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 7c18ed2572..e137bf83e4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -73,9 +73,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -90,9 +89,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, AllowedMods = new[] { new APIMod(new TaikoModSwap()) } }); @@ -113,9 +111,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set playlist", () => { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -128,9 +125,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set playlist", () => { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -159,9 +155,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with allowed mod", () => { - SelectedRoom.Value.Playlist.Add(new PlaylistItem + SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, AllowedMods = new[] { new APIMod(new OsuModDoubleTime()) } }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 010e9dc078..c402aff771 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -28,9 +28,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("initialise gameplay", () => { - Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem + Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem(Beatmap.Value.BeatmapInfo) { - Beatmap = { Value = Beatmap.Value.BeatmapInfo }, RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }, Client.Room?.Users.ToArray())); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 361178bfe4..57f1c31589 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -143,14 +143,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "test name" }, Playlist = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - RulesetID = Ruleset.Value.OnlineID, + RulesetID = Ruleset.Value.OnlineID }, - new PlaylistItem + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, RulesetID = Ruleset.Value.OnlineID, Expired = true } @@ -167,10 +165,8 @@ namespace osu.Game.Tests.Visual.Multiplayer /// /// Adds a step to create a new playlist item. /// - private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem + private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) { - Beatmap = { Value = importedBeatmap }, - BeatmapID = importedBeatmap.OnlineID, Expired = expired, PlayedAt = DateTimeOffset.Now }))); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 4040707c41..02bd93ed28 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -120,11 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add playlist item", () => { - MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem - { - Beatmap = { Value = importedBeatmap }, - BeatmapID = importedBeatmap.OnlineID, - }); + MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)); Client.AddUserPlaylistItem(userId(), item); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 7834226f15..d486a69061 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -54,9 +54,8 @@ namespace osu.Game.Tests.Visual.Multiplayer importedSet = beatmaps.GetAllUsableBeatmapSets().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); - selectedItem.Value = new PlaylistItem + selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo) { - Beatmap = { Value = Beatmap.Value.BeatmapInfo }, RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID }; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs index 44a1745eee..cc08135939 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs @@ -22,12 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; var score = TestResources.CreateTestScoreInfo(beatmapInfo); - PlaylistItem playlistItem = new PlaylistItem - { - BeatmapID = beatmapInfo.OnlineID, - }; - - Stack.Push(screen = new MultiplayerResultsScreen(score, 1, playlistItem)); + Stack.Push(screen = new MultiplayerResultsScreen(score, 1, new PlaylistItem(beatmapInfo))); }); AddUntilStep("wait for loaded", () => screen.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 70d4d9dd55..c8077a49dc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -55,9 +55,8 @@ namespace osu.Game.Tests.Visual.Multiplayer importedSet = beatmaps.GetAllUsableBeatmapSets().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); - selectedItem.Value = new PlaylistItem + selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo) { - Beatmap = { Value = Beatmap.Value.BeatmapInfo }, RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs index dfc16c44f2..bdc348b043 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -26,18 +26,13 @@ namespace osu.Game.Tests.Visual.Multiplayer var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; var score = TestResources.CreateTestScoreInfo(beatmapInfo); - PlaylistItem playlistItem = new PlaylistItem - { - BeatmapID = beatmapInfo.OnlineID, - }; - SortedDictionary teamScores = new SortedDictionary { { 0, new BindableInt(team1Score) }, { 1, new BindableInt(team2Score) } }; - Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, playlistItem, teamScores)); + Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, new PlaylistItem(beatmapInfo), teamScores)); }); AddUntilStep("wait for loaded", () => screen.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index 9adf2c0370..0ef8b16f68 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -107,16 +107,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); } - [Test] - public void TestChangeBeatmapAndRemove() - { - createPlaylist(); - - AddStep("change beatmap of first item", () => playlist.Items[0].BeatmapID = 30); - moveToDeleteButton(0); - AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); - } - private void moveToItem(int index, Vector2? offset = null) => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); @@ -139,25 +129,21 @@ namespace osu.Game.Tests.Visual.Multiplayer for (int i = 0; i < 20; i++) { - playlist.Items.Add(new PlaylistItem + playlist.Items.Add(new PlaylistItem(i % 2 == 1 + ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo + : new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "Artist", + Author = new RealmUser { Username = "Creator name here" }, + Title = "Long title used to check background colour", + }, + BeatmapSet = new BeatmapSetInfo() + }) { ID = i, OwnerID = 2, - Beatmap = - { - Value = i % 2 == 1 - ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo - : new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Artist = "Artist", - Author = new RealmUser { Username = "Creator name here" }, - Title = "Long title used to check background colour", - }, - BeatmapSet = new BeatmapSetInfo() - } - }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs index 20db922122..5e4013b0f1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs @@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { SelectedRoom.Value.Playlist.AddRange(new[] { - new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarRating = min } } }, - new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarRating = max } } }, + new PlaylistItem(new BeatmapInfo { StarRating = min }), + new PlaylistItem(new BeatmapInfo { StarRating = max }), }); }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index b66657728e..bd57c8afa5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -67,9 +67,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.TeamVersus }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -88,9 +87,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.TeamVersus }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } @@ -126,10 +124,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.HeadToHead }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); @@ -152,10 +149,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "Test Room" }, Playlist = { - new PlaylistItem + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { - Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - RulesetID = new OsuRuleset().RulesetInfo.OnlineID + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, } } }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index ca3387392a..666e32d1d0 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("set name", () => SelectedRoom.Value.Name.Value = "Room name"); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set beatmap", () => SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } })); + AddStep("set beatmap", () => SelectedRoom.Value.Playlist.Add(new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo))); AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value); AddStep("clear name", () => SelectedRoom.Value.Name.Value = ""); @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Playlists { settings.NameField.Current.Value = expected_name; settings.DurationField.Current.Value = expectedDuration; - SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); + SelectedRoom.Value.Playlist.Add(new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)); RoomManager.CreateRequested = r => { @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Playlists var beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo; SelectedRoom.Value.Name.Value = "Test Room"; - SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = beatmap } }); + SelectedRoom.Value.Playlist.Add(new PlaylistItem(beatmap)); errorMessage = $"{not_found_prefix} {beatmap.OnlineID}"; @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("setup", () => { SelectedRoom.Value.Name.Value = "Test Room"; - SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); + SelectedRoom.Value.Playlist.Add(new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)); RoomManager.CreateRequested = _ => failText; }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index a05d01613c..161624413d 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -165,9 +165,8 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("load results", () => { - LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem + LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID })); }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 578ea63b4e..28b1f6eff5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -64,9 +64,8 @@ namespace osu.Game.Tests.Visual.Playlists room.Host.Value = API.LocalUser.Value; room.RecentParticipants.Add(room.Host.Value); room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); - room.Playlist.Add(new PlaylistItem + room.Playlist.Add(new PlaylistItem(importedBeatmap.Beatmaps.First()) { - Beatmap = { Value = importedBeatmap.Beatmaps.First() }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -89,9 +88,8 @@ namespace osu.Game.Tests.Visual.Playlists room.Host.Value = API.LocalUser.Value; room.RecentParticipants.Add(room.Host.Value); room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); - room.Playlist.Add(new PlaylistItem + room.Playlist.Add(new PlaylistItem(importedBeatmap.Beatmaps.First()) { - Beatmap = { Value = importedBeatmap.Beatmaps.First() }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -106,9 +104,8 @@ namespace osu.Game.Tests.Visual.Playlists { room.Name.Value = "my awesome room"; room.Host.Value = API.LocalUser.Value; - room.Playlist.Add(new PlaylistItem + room.Playlist.Add(new PlaylistItem(importedBeatmap.Beatmaps.First()) { - Beatmap = { Value = importedBeatmap.Beatmaps.First() }, RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -158,21 +155,17 @@ namespace osu.Game.Tests.Visual.Playlists { room.Name.Value = "my awesome room"; room.Host.Value = API.LocalUser.Value; - room.Playlist.Add(new PlaylistItem + room.Playlist.Add(new PlaylistItem(new BeatmapInfo { - Beatmap = + MD5Hash = realHash, + OnlineID = realOnlineId, + Metadata = new BeatmapMetadata(), + BeatmapSet = new BeatmapSetInfo { - Value = new BeatmapInfo - { - MD5Hash = realHash, - OnlineID = realOnlineId, - Metadata = new BeatmapMetadata(), - BeatmapSet = new BeatmapSetInfo - { - OnlineID = realOnlineSetId, - } - } - }, + OnlineID = realOnlineSetId, + } + }) + { RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 9d45229961..a56cc7f8d6 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -727,10 +727,9 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); } - private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem + private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem(new APIBeatmap { OnlineID = item.BeatmapID }) { ID = item.ID, - BeatmapID = item.BeatmapID, OwnerID = item.OwnerID, RulesetID = item.RulesetID, Expired = item.Expired, @@ -740,14 +739,6 @@ namespace osu.Game.Online.Multiplayer AllowedMods = item.AllowedMods.ToArray() }; - /// - /// Retrieves a from an online source. - /// - /// The beatmap ID. - /// A token to cancel the request. - /// The retrieval task. - public abstract Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default); - /// /// For the provided user ID, update whether the user is included in . /// diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 3794bec228..e92bcd9769 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -9,9 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Database; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer @@ -29,9 +27,6 @@ namespace osu.Game.Online.Multiplayer private HubConnection? connection => connector?.CurrentConnection; - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - public OnlineMultiplayerClient(EndpointConfiguration endpoints) { endpoint = endpoints.MultiplayerEndpointUrl; @@ -186,11 +181,6 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } - public override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) - { - return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken); - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index d74cdd8c34..388a02f798 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -63,8 +63,8 @@ namespace osu.Game.Online.Rooms { ID = item.ID; OwnerID = item.OwnerID; - BeatmapID = item.BeatmapID; - BeatmapChecksum = item.Beatmap.Value?.MD5Hash ?? string.Empty; + BeatmapID = item.Beatmap.OnlineID; + BeatmapChecksum = item.Beatmap.MD5Hash; RulesetID = item.RulesetID; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 90532d88b8..3620e703ae 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -64,11 +64,11 @@ namespace osu.Game.Online.Rooms downloadTracker?.RemoveAndDisposeImmediately(); - beatmapLookupCache.GetBeatmapAsync(item.NewValue.Beatmap.Value.OnlineID).ContinueWith(task => Schedule(() => + beatmapLookupCache.GetBeatmapAsync(item.NewValue.Beatmap.OnlineID).ContinueWith(task => Schedule(() => { var beatmap = task.GetResultSafely(); - if (SelectedItem.Value?.Beatmap.Value.OnlineID == beatmap.OnlineID) + if (SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID) beginTracking(beatmap); }), TaskContinuationOptions.OnlyOnRanToCompletion); }, true); @@ -96,7 +96,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap.Value), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap), (items, changes, ___) => { if (changes == null) return; diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs index e78f91f20b..34c93bd9e0 100644 --- a/osu.Game/Online/Rooms/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -41,6 +41,6 @@ namespace osu.Game.Online.Rooms } public static string GetTotalDuration(this BindableList playlist) => - playlist.Select(p => p.Beatmap.Value.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); + playlist.Select(p => p.Beatmap.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); } } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index c082babb01..33718f050b 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Linq; using JetBrains.Annotations; @@ -12,6 +14,7 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.Rooms { + [JsonObject(MemberSerialization.OptIn)] public class PlaylistItem : IEquatable { [JsonProperty("id")] @@ -20,9 +23,6 @@ namespace osu.Game.Online.Rooms [JsonProperty("owner_id")] public int OwnerID { get; set; } - [JsonProperty("beatmap_id")] - public int BeatmapID { get; set; } - [JsonProperty("ruleset_id")] public int RulesetID { get; set; } @@ -38,35 +38,50 @@ namespace osu.Game.Online.Rooms [JsonProperty("played_at")] public DateTimeOffset? PlayedAt { get; set; } - [JsonIgnore] - public IBindable Valid => valid; - - private readonly Bindable valid = new BindableBool(true); - - [JsonIgnore] - public readonly Bindable Beatmap = new Bindable(); - - [JsonProperty("beatmap")] - private APIBeatmap apiBeatmap { get; set; } - [JsonProperty("allowed_mods")] public APIMod[] AllowedMods { get; set; } = Array.Empty(); [JsonProperty("required_mods")] public APIMod[] RequiredMods { get; set; } = Array.Empty(); - public PlaylistItem() + /// + /// Used for deserialising from the API. + /// + [JsonProperty("beatmap")] + private APIBeatmap apiBeatmap { - Beatmap.BindValueChanged(beatmap => BeatmapID = beatmap.NewValue?.OnlineID ?? -1); + // This getter is required/used internally by JSON.NET during deserialisation to do default-value comparisons. It is never used during serialisation (see: ShouldSerializeapiBeatmap()). + // It will always return a null value on deserialisation, which JSON.NET will handle gracefully. + get => (APIBeatmap)Beatmap; + set => Beatmap = value; + } + + /// + /// Used for serialising to the API. + /// + [JsonProperty("beatmap_id")] + private int onlineBeatmapId => Beatmap.OnlineID; + + [JsonIgnore] + public IBeatmapInfo Beatmap { get; set; } = null!; + + [JsonIgnore] + public IBindable Valid => valid; + + private readonly Bindable valid = new BindableBool(true); + + [JsonConstructor] + private PlaylistItem() + { + } + + public PlaylistItem(IBeatmapInfo beatmap) + { + Beatmap = beatmap; } public void MarkInvalid() => valid.Value = false; - public void MapObjects() - { - Beatmap.Value ??= apiBeatmap; - } - #region Newtonsoft.Json implicit ShouldSerialize() methods // The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases. @@ -82,9 +97,22 @@ namespace osu.Game.Online.Rooms #endregion - public bool Equals(PlaylistItem other) + public PlaylistItem With(IBeatmapInfo beatmap) => new PlaylistItem(beatmap) + { + ID = ID, + OwnerID = OwnerID, + RulesetID = RulesetID, + Expired = Expired, + PlaylistOrder = PlaylistOrder, + PlayedAt = PlayedAt, + AllowedMods = AllowedMods, + RequiredMods = RequiredMods, + valid = { Value = Valid.Value }, + }; + + public bool Equals(PlaylistItem? other) => ID == other?.ID - && BeatmapID == other.BeatmapID + && Beatmap.OnlineID == other.Beatmap.OnlineID && RulesetID == other.RulesetID && Expired == other.Expired && AllowedMods.SequenceEqual(other.AllowedMods) diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index e948c1adae..7cbe1a9017 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -68,14 +68,14 @@ namespace osu.Game.Screens.OnlinePlay.Components } else { - var metadataInfo = beatmap.Value.Metadata; + var metadataInfo = beatmap.Metadata; string artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode; string titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode; var title = new RomanisableString($"{artistUnicode} - {titleUnicode}".Trim(), $"{metadataInfo.Artist} - {metadataInfo.Title}".Trim()); - textFlow.AddLink(title, LinkAction.OpenBeatmap, beatmap.Value.OnlineID.ToString(), "Open beatmap"); + textFlow.AddLink(title, LinkAction.OpenBeatmap, beatmap.OnlineID.ToString(), "Open beatmap"); } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs index d534a1e374..8402619ebc 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Components var mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); drawableRuleset.FadeIn(transition_duration); - drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, ruleset.RulesetInfo, mods) { Size = new Vector2(height) }; + drawableRuleset.Child = new DifficultyIcon(item.Beatmap, ruleset.RulesetInfo, mods) { Size = new Vector2(height) }; } else drawableRuleset.FadeOut(transition_duration); diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs index ffc5c07d4e..8906bebf0e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { Schedule(() => { - var beatmap = playlistItem?.Beatmap.Value; + var beatmap = playlistItem?.Beatmap; string? lastCover = (background?.Beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.Covers.Cover; string? newCover = (beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.Covers.Cover; diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs index d144e1e3a9..d46ff12279 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateBeatmap() { - sprite.Beatmap.Value = Playlist.GetCurrentItem()?.Beatmap.Value; + sprite.Beatmap.Value = Playlist.GetCurrentItem()?.Beatmap; } protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs index f3e90aa396..7e31591389 100644 --- a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs +++ b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Components public PlaylistItemBackground(PlaylistItem? playlistItem) { - Beatmap = playlistItem?.Beatmap.Value; + Beatmap = playlistItem?.Beatmap; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 21b64b61bb..4242886e66 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -112,9 +112,6 @@ namespace osu.Game.Screens.OnlinePlay.Components try { - foreach (var pi in room.Playlist) - pi.MapObjects(); - var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); if (existing == null) rooms.Add(room); diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index edf9c5d155..95ecadd21a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateRange(object sender, NotifyCollectionChangedEventArgs e) { - var orderedDifficulties = Playlist.Where(p => p.Beatmap.Value != null).Select(p => p.Beatmap.Value).OrderBy(b => b.StarRating).ToArray(); + var orderedDifficulties = Playlist.Select(p => p.Beatmap).OrderBy(b => b.StarRating).ToArray(); StarDifficulty minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0); StarDifficulty maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarRating : 0, 0); diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 7674fac88e..4be1927e57 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay Schedule(() => ownerAvatar.User = foundUser); } - beatmap = await beatmapLookupCache.GetBeatmapAsync(Item.Beatmap.Value.OnlineID).ConfigureAwait(false); + beatmap = await beatmapLookupCache.GetBeatmapAsync(Item.Beatmap.OnlineID).ConfigureAwait(false); Scheduler.AddOnce(refresh); } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 60f65fbd9f..a1a82c907a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -409,7 +409,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return; var cancellationSource = beatmapLookupCancellation = new CancellationTokenSource(); - beatmapLookupCache.GetBeatmapAsync(beatmap.Value.OnlineID, cancellationSource.Token) + beatmapLookupCache.GetBeatmapAsync(beatmap.OnlineID, cancellationSource.Token) .ContinueWith(task => Schedule(() => { if (cancellationSource.IsCancellationRequested) diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index a7b907c7d2..cdd2ae0c9c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (editButton != null) host.BindValueChanged(h => editButton.Alpha = h.NewValue?.Equals(api.LocalUser.Value) == true ? 1 : 0, true); - SelectedItem.BindValueChanged(item => background.Beatmap.Value = item.NewValue?.Beatmap.Value, true); + SelectedItem.BindValueChanged(item => background.Beatmap.Value = item.NewValue?.Beatmap, true); } protected override Drawable CreateBackground() => background = new BackgroundSprite(); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 836629ada0..e297c90491 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -376,7 +376,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private void updateWorkingBeatmap() { - var beatmap = SelectedItem.Value?.Beatmap.Value; + var beatmap = SelectedItem.Value?.Beatmap; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 12caf1fde1..e30ec36e9c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -67,8 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer var multiplayerItem = new MultiplayerPlaylistItem { ID = itemToEdit ?? 0, - BeatmapID = item.BeatmapID, - BeatmapChecksum = item.Beatmap.Value.MD5Hash, + BeatmapID = item.Beatmap.OnlineID, + BeatmapChecksum = item.Beatmap.MD5Hash, RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), AllowedMods = item.AllowedMods.ToArray() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index cb50a56052..429b0ad89b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -398,38 +397,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void updateCurrentItem() { Debug.Assert(client.Room != null); - - var expectedSelectedItem = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); - - if (expectedSelectedItem == null) - return; - - // There's no reason to renew the selected item if its content hasn't changed. - if (SelectedItem.Value?.Equals(expectedSelectedItem) == true && expectedSelectedItem.Beatmap.Value != null) - return; - - // Clear the selected item while the lookup is performed, so components like the ready button can enter their disabled states. - SelectedItem.Value = null; - - if (expectedSelectedItem.Beatmap.Value == null) - { - Task.Run(async () => - { - var beatmap = await client.GetAPIBeatmap(expectedSelectedItem.BeatmapID).ConfigureAwait(false); - - Schedule(() => - { - expectedSelectedItem.Beatmap.Value = beatmap; - - if (Room.Playlist.SingleOrDefault(i => i.ID == client.Room?.Settings.PlaylistItemId)?.Equals(expectedSelectedItem) == true) - applyCurrentItem(); - }); - }); - } - else - applyCurrentItem(); - - void applyCurrentItem() => SelectedItem.Value = expectedSelectedItem; + SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); } private void handleRoomLost() => Schedule(() => diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index eab1f83967..7b64784316 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -113,12 +113,8 @@ namespace osu.Game.Screens.OnlinePlay { itemSelected = true; - var item = new PlaylistItem + var item = new PlaylistItem(Beatmap.Value.BeatmapInfo) { - Beatmap = - { - Value = Beatmap.Value.BeatmapInfo - }, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 2b071175d5..8403e1e0fe 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void load(IBindable ruleset) { // Sanity checks to ensure that PlaylistsPlayer matches the settings for the current PlaylistItem - if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(PlaylistItem.Beatmap.Value)) + if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(PlaylistItem.Beatmap)) throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap"); if (ruleset.Value.OnlineID != PlaylistItem.RulesetID) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 6c8ab52d22..6674a37c3c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -392,7 +392,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists foreach (var item in Playlist) { - if (invalidBeatmapIDs.Contains(item.BeatmapID)) + if (invalidBeatmapIDs.Contains(item.Beatmap.OnlineID)) item.MarkInvalid(); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 3ac576b18e..86591c1d6d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -41,10 +41,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void createNewItem() { - PlaylistItem item = new PlaylistItem + PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1, - Beatmap = { Value = Beatmap.Value.BeatmapInfo }, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index a9b3ca4991..2fecf0a4cc 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -46,9 +46,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = { Value = "test name" }, Playlist = { - new PlaylistItem + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, RulesetID = Ruleset.Value.OnlineID } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 15ede6cc26..6dc5159b6f 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -7,14 +7,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Game.Beatmaps; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; @@ -39,9 +36,6 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private BeatmapManager beatmaps { get; set; } = null!; - private readonly TestMultiplayerRoomManager roomManager; /// @@ -407,23 +401,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId); - public override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) - { - IBeatmapInfo? beatmap = roomManager.ServerSideRooms.SelectMany(r => r.Playlist) - .FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value - ?? beatmaps.QueryBeatmap(b => b.OnlineID == beatmapId); - - if (beatmap == null) - throw new InvalidOperationException("Beatmap not found."); - - return Task.FromResult(new APIBeatmap - { - BeatmapSet = new APIBeatmapSet { OnlineID = beatmap.BeatmapSet?.OnlineID ?? -1 }, - OnlineID = beatmapId, - Checksum = beatmap.MD5Hash - }); - } - private async Task changeMatchType(MatchType type) { Debug.Assert(Room != null); diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 73d0df2c36..8dfd969c51 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -43,16 +43,9 @@ namespace osu.Game.Tests.Visual.OnlinePlay if (ruleset != null) { - room.Playlist.Add(new PlaylistItem + room.Playlist.Add(new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID, - Beatmap = - { - Value = new BeatmapInfo - { - Metadata = new BeatmapMetadata() - } - } }); } From 48573d2401d33434399f9abaf98659ac2776fa81 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:42:37 +0900 Subject: [PATCH 090/306] Move test request handling earlier in setup --- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 448ec5e3d9..b6a347a896 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -60,24 +60,16 @@ namespace osu.Game.Tests.Visual.OnlinePlay drawableDependenciesContainer.Clear(); dependencies.OnlinePlayDependencies = CreateOnlinePlayDependencies(); drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents); + + var handler = OnlinePlayDependencies.RequestsHandler; + + // Resolving the BeatmapManager in the test scene will inject the game-wide BeatmapManager, while many test scenes cache their own BeatmapManager instead. + // To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead. + var beatmapManager = dependencies.Get(); + + ((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); }); - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("setup API", () => - { - var handler = OnlinePlayDependencies.RequestsHandler; - - // Resolving the BeatmapManager in the test scene will inject the game-wide BeatmapManager, while many test scenes cache their own BeatmapManager instead. - // To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead. - var beatmapManager = dependencies.Get(); - - ((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); - }); - } - /// /// Creates the room dependencies. Called every . /// From b1dbd4abfe6697134717439d1fbe06eb91c94cf5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:50:07 +0900 Subject: [PATCH 091/306] Fix incorrect playlist item <-> availability tracker logic Results in revert to some prior logic for the tracker implementation. --- .../OnlinePlayBeatmapAvailabilityTracker.cs | 19 +++++++++---------- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 5 +---- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 3620e703ae..02bcd1f30c 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -96,7 +96,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => QueryBeatmapForOnlinePlay(r, SelectedItem.Value.Beatmap), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(beatmap), (items, changes, ___) => { if (changes == null) return; @@ -125,7 +125,7 @@ namespace osu.Game.Online.Rooms break; case DownloadState.LocallyAvailable: - bool available = QueryBeatmapForOnlinePlay(realm.Realm, beatmap).Any(); + bool available = filteredBeatmaps(beatmap).Any(); availability.Value = available ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); @@ -140,15 +140,14 @@ namespace osu.Game.Online.Rooms } } - /// - /// Performs a query for a local matching a requested one for the purpose of online play. - /// - /// The realm to query from. - /// The requested beatmap. - /// A beatmap query. - public static IQueryable QueryBeatmapForOnlinePlay(Realm realm, IBeatmapInfo beatmap) + private IQueryable filteredBeatmaps(APIBeatmap beatmap) { - return realm.All().Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", beatmap.OnlineID, beatmap.MD5Hash); + int onlineId = beatmap.OnlineID; + string checksum = beatmap.MD5Hash; + + return realm.Realm + .All() + .Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 4be1927e57..faeb73cfa3 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -97,9 +97,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } - [Resolved] - private RealmAccess realm { get; set; } - protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model; public DrawableRoomPlaylistItem(PlaylistItem item) @@ -444,7 +441,7 @@ namespace osu.Game.Screens.OnlinePlay Alpha = AllowShowingResults ? 1 : 0, TooltipText = "View results" }, - OnlinePlayBeatmapAvailabilityTracker.QueryBeatmapForOnlinePlay(realm.Realm, beatmap).Any() ? Empty() : new PlaylistDownloadButton(beatmap), + beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap), editButton = new PlaylistEditButton { Size = new Vector2(30, 30), From f5b34e03139a7d098fe30e44c9e6fcad3cd890ca Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 23:51:44 +0900 Subject: [PATCH 092/306] Fix some test failures due to now-async lookups --- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 4 ++-- .../Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index bea15e6e58..4f01c14659 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -168,7 +168,7 @@ namespace osu.Game.Tests.Visual.Multiplayer assertDownloadButtonVisible(false); void assertDownloadButtonVisible(bool visible) => AddUntilStep($"download button {(visible ? "shown" : "hidden")}", - () => playlist.ChildrenOfType().Single().Alpha == (visible ? 1 : 0)); + () => playlist.ChildrenOfType().SingleOrDefault()?.Alpha == (visible ? 1 : 0)); } [Test] @@ -260,7 +260,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } private void moveToItem(int index, Vector2? offset = null) - => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); + => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); private void moveToDragger(int index, Vector2? offset = null) => AddStep($"move mouse to dragger {index}", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index 0ef8b16f68..98dc243ab5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; using osu.Game.Models; using osu.Game.Online.API; @@ -108,7 +107,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } private void moveToItem(int index, Vector2? offset = null) - => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); + => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () => { From 6e99fe04c3b71ce5a36609062ceb08bd6fb1e400 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 00:39:43 +0900 Subject: [PATCH 093/306] Revert more NUnit test adapter bumps --- .../osu.Game.Rulesets.EmptyFreeform.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- .../osu.Game.Rulesets.EmptyScrolling.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index c305872288..cb922c5a58 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index ec90885cd9..5ecd9cc675 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index f2e143a9c5..33ad0ac4f7 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index ec90885cd9..5ecd9cc675 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + From 6a08fd57efb0012a723d658bc279d88292f3754e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 09:43:28 +0900 Subject: [PATCH 094/306] Rename "client" fields in tests to specify whether spectator or multiplayer --- .../StatefulMultiplayerClientTest.cs | 28 +-- .../Visual/Gameplay/TestSceneSpectator.cs | 6 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 10 +- .../TestSceneAllPlayersQueueMode.cs | 38 ++-- .../TestSceneCreateMultiplayerMatchButton.cs | 4 +- .../Multiplayer/TestSceneHostOnlyQueueMode.cs | 26 +-- .../TestSceneMultiSpectatorLeaderboard.cs | 2 +- .../TestSceneMultiSpectatorScreen.cs | 12 +- .../Multiplayer/TestSceneMultiplayer.cs | 180 +++++++++--------- ...TestSceneMultiplayerGameplayLeaderboard.cs | 6 +- ...ceneMultiplayerGameplayLeaderboardTeams.cs | 4 +- .../TestSceneMultiplayerMatchSubScreen.cs | 10 +- .../TestSceneMultiplayerParticipantsList.cs | 98 +++++----- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 6 +- .../TestSceneMultiplayerPlaylist.cs | 12 +- .../TestSceneMultiplayerQueueList.cs | 28 +-- .../TestSceneMultiplayerReadyButton.cs | 56 +++--- .../TestSceneMultiplayerSpectateButton.cs | 26 +-- .../Multiplayer/TestSceneRankRangePill.cs | 18 +- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 30 +-- .../Visual/TestMultiplayerComponents.cs | 4 +- .../IMultiplayerTestSceneDependencies.cs | 5 +- .../Multiplayer/MultiplayerTestScene.cs | 4 +- .../MultiplayerTestSceneDependencies.cs | 6 +- 24 files changed, 309 insertions(+), 310 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 0c49a18c8f..4adb7002a0 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -21,8 +21,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer { var user = new APIUser { Id = 33 }; - AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + AddRepeatStep("add user multiple times", () => MultiplayerClient.AddUser(user), 3); + AddAssert("room has 2 users", () => MultiplayerClient.Room?.Users.Count == 2); } [Test] @@ -30,11 +30,11 @@ namespace osu.Game.Tests.NonVisual.Multiplayer { var user = new APIUser { Id = 44 }; - AddStep("add user", () => Client.AddUser(user)); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + AddStep("add user", () => MultiplayerClient.AddUser(user)); + AddAssert("room has 2 users", () => MultiplayerClient.Room?.Users.Count == 2); - AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); - AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + AddRepeatStep("remove user multiple times", () => MultiplayerClient.RemoveUser(user), 3); + AddAssert("room has 1 user", () => MultiplayerClient.Room?.Users.Count == 1); } [Test] @@ -42,7 +42,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer { int id = 2000; - AddRepeatStep("add some users", () => Client.AddUser(new APIUser { Id = id++ }), 5); + AddRepeatStep("add some users", () => MultiplayerClient.AddUser(new APIUser { Id = id++ }), 5); checkPlayingUserCount(0); changeState(3, MultiplayerUserState.WaitingForLoad); @@ -57,17 +57,17 @@ namespace osu.Game.Tests.NonVisual.Multiplayer changeState(6, MultiplayerUserState.WaitingForLoad); checkPlayingUserCount(6); - AddStep("another user left", () => Client.RemoveUser((Client.Room?.Users.Last().User).AsNonNull())); + AddStep("another user left", () => MultiplayerClient.RemoveUser((MultiplayerClient.Room?.Users.Last().User).AsNonNull())); checkPlayingUserCount(5); - AddStep("leave room", () => Client.LeaveRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); checkPlayingUserCount(0); } [Test] public void TestPlayingUsersUpdatedOnJoin() { - AddStep("leave room", () => Client.LeaveRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); AddStep("create room initially in gameplay", () => @@ -76,7 +76,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer newRoom.CopyFrom(SelectedRoom.Value); newRoom.RoomID.Value = null; - Client.RoomSetupAction = room => + MultiplayerClient.RoomSetupAction = room => { room.State = MultiplayerRoomState.Playing; room.Users.Add(new MultiplayerRoomUser(PLAYER_1_ID) @@ -94,15 +94,15 @@ namespace osu.Game.Tests.NonVisual.Multiplayer } private void checkPlayingUserCount(int expectedCount) - => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => Client.CurrentMatchPlayingUserIds.Count == expectedCount); + => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count == expectedCount); private void changeState(int userCount, MultiplayerUserState state) => AddStep($"{"user".ToQuantity(userCount)} in {state}", () => { for (int i = 0; i < userCount; ++i) { - int userId = Client.Room?.Users[i].UserID ?? throw new AssertionException("Room cannot be null!"); - Client.ChangeUserState(userId, state); + int userId = MultiplayerClient.Room?.Users[i].UserID ?? throw new AssertionException("Room cannot be null!"); + MultiplayerClient.ChangeUserState(userId, state); } }); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 6dca256d31..d47ebf9f0d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuGameBase game { get; set; } - private TestSpectatorClient spectatorClient => dependenciesScreen.Client; + private TestSpectatorClient spectatorClient => dependenciesScreen.SpectatorClient; private DependenciesScreen dependenciesScreen; private SoloSpectator spectatorScreen; @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay Children = new Drawable[] { dependenciesScreen.UserLookupCache, - dependenciesScreen.Client, + dependenciesScreen.SpectatorClient, }; }); @@ -336,7 +336,7 @@ namespace osu.Game.Tests.Visual.Gameplay private class DependenciesScreen : OsuScreen { [Cached(typeof(SpectatorClient))] - public readonly TestSpectatorClient Client = new TestSpectatorClient(); + public readonly TestSpectatorClient SpectatorClient = new TestSpectatorClient(); [Cached(typeof(UserLookupCache))] public readonly TestUserLookupCache UserLookupCache = new TestUserLookupCache(); diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 52801dd57a..2a3b44d619 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerComponents multiplayerComponents; - protected TestMultiplayerClient Client => multiplayerComponents.Client; + protected TestMultiplayerClient MultiplayerClient => multiplayerComponents.MultiplayerClient; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -84,21 +84,21 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddUntilStep("wait for join", () => Client.RoomJoined); + AddUntilStep("wait for join", () => MultiplayerClient.RoomJoined); } [Test] public void TestCreatedWithCorrectMode() { - AddAssert("room created with correct mode", () => Client.APIRoom?.QueueMode.Value == Mode); + AddAssert("room created with correct mode", () => MultiplayerClient.APIRoom?.QueueMode.Value == Mode); } protected void RunGameplay() { - AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); ClickButtonWhenEnabled(); - AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); + AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index ad60ac824d..0785315b26 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -31,19 +31,19 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestFirstItemSelectedByDefault() { - AddAssert("first item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("first item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] public void TestItemAddedToTheEndOfQueue() { addItem(() => OtherBeatmap); - AddAssert("playlist has 2 items", () => Client.APIRoom?.Playlist.Count == 2); + AddAssert("playlist has 2 items", () => MultiplayerClient.APIRoom?.Playlist.Count == 2); addItem(() => InitialBeatmap); - AddAssert("playlist has 3 items", () => Client.APIRoom?.Playlist.Count == 3); + AddAssert("playlist has 3 items", () => MultiplayerClient.APIRoom?.Playlist.Count == 3); - AddAssert("first item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("first item still selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] @@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { RunGameplay(); - AddAssert("playlist has only one item", () => Client.APIRoom?.Playlist.Count == 1); - AddAssert("playlist item is expired", () => Client.APIRoom?.Playlist[0].Expired == true); - AddAssert("last item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("playlist has only one item", () => MultiplayerClient.APIRoom?.Playlist.Count == 1); + AddAssert("playlist item is expired", () => MultiplayerClient.APIRoom?.Playlist[0].Expired == true); + AddAssert("last item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] @@ -64,13 +64,13 @@ namespace osu.Game.Tests.Visual.Multiplayer RunGameplay(); - AddAssert("first item expired", () => Client.APIRoom?.Playlist[0].Expired == true); - AddAssert("next item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[1].ID); + AddAssert("first item expired", () => MultiplayerClient.APIRoom?.Playlist[0].Expired == true); + AddAssert("next item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[1].ID); RunGameplay(); - AddAssert("second item expired", () => Client.APIRoom?.Playlist[1].Expired == true); - AddAssert("next item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[2].ID); + AddAssert("second item expired", () => MultiplayerClient.APIRoom?.Playlist[1].Expired == true); + AddAssert("next item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[2].ID); } [Test] @@ -82,10 +82,10 @@ namespace osu.Game.Tests.Visual.Multiplayer // Move to the "other" beatmap. RunGameplay(); - AddStep("change queue mode", () => Client.ChangeSettings(queueMode: QueueMode.HostOnly)); - AddAssert("playlist has 3 items", () => Client.APIRoom?.Playlist.Count == 3); - AddAssert("item 2 is not expired", () => Client.APIRoom?.Playlist[1].Expired == false); - AddAssert("current item is the other beatmap", () => Client.Room?.Settings.PlaylistItemId == 2); + AddStep("change queue mode", () => MultiplayerClient.ChangeSettings(queueMode: QueueMode.HostOnly)); + AddAssert("playlist has 3 items", () => MultiplayerClient.APIRoom?.Playlist.Count == 3); + AddAssert("item 2 is not expired", () => MultiplayerClient.APIRoom?.Playlist[1].Expired == false); + AddAssert("current item is the other beatmap", () => MultiplayerClient.Room?.Settings.PlaylistItemId == 2); } [Test] @@ -101,10 +101,10 @@ namespace osu.Game.Tests.Visual.Multiplayer addItem(() => OtherBeatmap, new CatchRuleset().RulesetInfo); AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); - AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); ClickButtonWhenEnabled(); - AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); + AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); @@ -118,10 +118,10 @@ namespace osu.Game.Tests.Visual.Multiplayer addItem(() => OtherBeatmap, mods: new Mod[] { new OsuModDoubleTime() }); AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); - AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); ClickButtonWhenEnabled(); - AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); + AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs index 2f0398c6ef..0674fc7a39 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs @@ -37,10 +37,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("end joining room", () => joiningRoomOperation.Dispose()); assertButtonEnableState(true); - AddStep("disconnect client", () => Client.Disconnect()); + AddStep("disconnect client", () => MultiplayerClient.Disconnect()); assertButtonEnableState(false); - AddStep("re-connect client", () => Client.Connect()); + AddStep("re-connect client", () => MultiplayerClient.Connect()); assertButtonEnableState(true); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index cd14a98751..a308d60cf1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestFirstItemSelectedByDefault() { - AddAssert("first item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("first item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { selectNewItem(() => InitialBeatmap); - AddAssert("playlist item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("playlist item still selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { selectNewItem(() => OtherBeatmap); - AddAssert("playlist item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID); + AddAssert("playlist item still selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID); } [Test] @@ -46,10 +46,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { RunGameplay(); - AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2); - AddAssert("first playlist item expired", () => Client.APIRoom?.Playlist[0].Expired == true); - AddAssert("second playlist item not expired", () => Client.APIRoom?.Playlist[1].Expired == false); - AddAssert("second playlist item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[1].ID); + AddAssert("playlist contains two items", () => MultiplayerClient.APIRoom?.Playlist.Count == 2); + AddAssert("first playlist item expired", () => MultiplayerClient.APIRoom?.Playlist[0].Expired == true); + AddAssert("second playlist item not expired", () => MultiplayerClient.APIRoom?.Playlist[1].Expired == false); + AddAssert("second playlist item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[1].ID); } [Test] @@ -58,23 +58,23 @@ namespace osu.Game.Tests.Visual.Multiplayer RunGameplay(); IBeatmapInfo firstBeatmap = null; - AddStep("get first playlist item beatmap", () => firstBeatmap = Client.APIRoom?.Playlist[0].Beatmap.Value); + AddStep("get first playlist item beatmap", () => firstBeatmap = MultiplayerClient.APIRoom?.Playlist[0].Beatmap.Value); selectNewItem(() => OtherBeatmap); - AddAssert("first playlist item hasn't changed", () => Client.APIRoom?.Playlist[0].Beatmap.Value == firstBeatmap); - AddAssert("second playlist item changed", () => Client.APIRoom?.Playlist[1].Beatmap.Value != firstBeatmap); + AddAssert("first playlist item hasn't changed", () => MultiplayerClient.APIRoom?.Playlist[0].Beatmap.Value == firstBeatmap); + AddAssert("second playlist item changed", () => MultiplayerClient.APIRoom?.Playlist[1].Beatmap.Value != firstBeatmap); } [Test] public void TestSettingsUpdatedWhenChangingQueueMode() { - AddStep("change queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings + AddStep("change queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); - AddUntilStep("api room updated", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + AddUntilStep("api room updated", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); } [Test] @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { addItem(() => OtherBeatmap); - AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2); + AddAssert("playlist contains two items", () => MultiplayerClient.APIRoom?.Playlist.Count == 2); } private void selectNewItem(Func beatmap) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 543e6a91d0..6b3573b3cb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach ((int userId, var _) in clocks) { SpectatorClient.StartPlay(userId, 0); - OnlinePlayDependencies.Client.AddUser(new APIUser { Id = userId }); + OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId }); } }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 56cb6036c7..7ce0c6a94d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -60,8 +60,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("start players silently", () => { - OnlinePlayDependencies.Client.AddUser(new APIUser { Id = PLAYER_1_ID }, true); - OnlinePlayDependencies.Client.AddUser(new APIUser { Id = PLAYER_2_ID }, true); + OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }, true); + OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = PLAYER_2_ID }, true); playingUsers.Add(new MultiplayerRoomUser(PLAYER_1_ID)); playingUsers.Add(new MultiplayerRoomUser(PLAYER_2_ID)); @@ -121,13 +121,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("start players", () => { - var player1 = OnlinePlayDependencies.Client.AddUser(new APIUser { Id = PLAYER_1_ID }, true); + var player1 = OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }, true); player1.MatchState = new TeamVersusUserState { TeamID = 0, }; - var player2 = OnlinePlayDependencies.Client.AddUser(new APIUser { Id = PLAYER_2_ID }, true); + var player2 = OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = PLAYER_2_ID }, true); player2.MatchState = new TeamVersusUserState { TeamID = 1, @@ -396,7 +396,7 @@ namespace osu.Game.Tests.Visual.Multiplayer User = new APIUser { Id = id }, }; - OnlinePlayDependencies.Client.AddUser(user.User, true); + OnlinePlayDependencies.MultiplayerClient.AddUser(user.User, true); SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); playingUsers.Add(user); @@ -410,7 +410,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { var user = playingUsers.Single(u => u.UserID == userId); - OnlinePlayDependencies.Client.RemoveUser(user.User.AsNonNull()); + OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull()); SpectatorClient.EndPlay(userId); playingUsers.Remove(user); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 5439b3eeea..3f9aec3a42 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerComponents multiplayerComponents; - private TestMultiplayerClient client => multiplayerComponents.Client; + private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient; private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager; [BackgroundDependencyLoader] @@ -109,66 +109,66 @@ namespace osu.Game.Tests.Visual.Multiplayer // all ready AddUntilStep("all players ready", () => { - var nextUnready = client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); + var nextUnready = multiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); if (nextUnready != null) - client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); + multiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); - return client.Room?.Users.All(u => u.State == MultiplayerUserState.Ready) == true; + return multiplayerClient.Room?.Users.All(u => u.State == MultiplayerUserState.Ready) == true; }); AddStep("unready all players at once", () => { - Debug.Assert(client.Room != null); + Debug.Assert(multiplayerClient.Room != null); - foreach (var u in client.Room.Users) client.ChangeUserState(u.UserID, MultiplayerUserState.Idle); + foreach (var u in multiplayerClient.Room.Users) multiplayerClient.ChangeUserState(u.UserID, MultiplayerUserState.Idle); }); AddStep("ready all players at once", () => { - Debug.Assert(client.Room != null); + Debug.Assert(multiplayerClient.Room != null); - foreach (var u in client.Room.Users) client.ChangeUserState(u.UserID, MultiplayerUserState.Ready); + foreach (var u in multiplayerClient.Room.Users) multiplayerClient.ChangeUserState(u.UserID, MultiplayerUserState.Ready); }); } private void addRandomPlayer() { int randomUser = RNG.Next(200000, 500000); - client.AddUser(new APIUser { Id = randomUser, Username = $"user {randomUser}" }); + multiplayerClient.AddUser(new APIUser { Id = randomUser, Username = $"user {randomUser}" }); } private void removeLastUser() { - APIUser lastUser = client.Room?.Users.Last().User; + APIUser lastUser = multiplayerClient.Room?.Users.Last().User; - if (lastUser == null || lastUser == client.LocalUser?.User) + if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User) return; - client.RemoveUser(lastUser); + multiplayerClient.RemoveUser(lastUser); } private void kickLastUser() { - APIUser lastUser = client.Room?.Users.Last().User; + APIUser lastUser = multiplayerClient.Room?.Users.Last().User; - if (lastUser == null || lastUser == client.LocalUser?.User) + if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User) return; - client.KickUser(lastUser.Id); + multiplayerClient.KickUser(lastUser.Id); } private void markNextPlayerReady() { - var nextUnready = client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); + var nextUnready = multiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); if (nextUnready != null) - client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); + multiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); } private void markNextPlayerIdle() { - var nextUnready = client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Ready); + var nextUnready = multiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Ready); if (nextUnready != null) - client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Idle); + multiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Idle); } private void performRandomAction() @@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Press select", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); } [Test] @@ -237,8 +237,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1); - AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1); + AddAssert("Check participant count correct", () => multiplayerClient.APIRoom?.ParticipantCount.Value == 1); + AddAssert("Check participant list contains user", () => multiplayerClient.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1); } [Test] @@ -297,10 +297,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); - AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1); - AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1); + AddAssert("Check participant count correct", () => multiplayerClient.APIRoom?.ParticipantCount.Value == 1); + AddAssert("Check participant list contains user", () => multiplayerClient.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1); } [Test] @@ -320,7 +320,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddAssert("room has password", () => client.APIRoom?.Password.Value == "password"); + AddAssert("room has password", () => multiplayerClient.APIRoom?.Password.Value == "password"); } [Test] @@ -355,7 +355,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); } [Test] @@ -375,8 +375,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("change password", () => client.ChangeSettings(password: "password2")); - AddUntilStep("local password changed", () => client.APIRoom?.Password.Value == "password2"); + AddStep("change password", () => multiplayerClient.ChangeSettings(password: "password2")); + AddUntilStep("local password changed", () => multiplayerClient.APIRoom?.Password.Value == "password2"); } [Test] @@ -398,7 +398,7 @@ namespace osu.Game.Tests.Visual.Multiplayer pressReadyButton(); AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); - AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("user state is idle", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); } [Test] @@ -422,22 +422,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; - ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId); + ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.Room?.Settings.PlaylistItemId); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); - AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); + AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == multiplayerClient.Room?.Playlist.First().BeatmapID); AddStep("Select next beatmap", () => InputManager.Key(Key.Down)); - AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != client.Room?.Playlist.First().BeatmapID); + AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID); - AddStep("start match externally", () => client.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); - AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); + AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == multiplayerClient.Room?.Playlist.First().BeatmapID); } [Test] @@ -461,22 +461,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; - ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId); + ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.Room?.Settings.PlaylistItemId); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); - AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == client.Room?.Playlist.First().RulesetID); + AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == multiplayerClient.Room?.Playlist.First().RulesetID); AddStep("Switch ruleset", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Ruleset.Value = new CatchRuleset().RulesetInfo); - AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != client.Room?.Playlist.First().RulesetID); + AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID); - AddStep("start match externally", () => client.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); - AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == client.Room?.Playlist.First().RulesetID); + AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == multiplayerClient.Room?.Playlist.First().RulesetID); } [Test] @@ -500,22 +500,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; - ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId); + ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.Room?.Settings.PlaylistItemId); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); - AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(client.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); + AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() }); - AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(client.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); + AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); - AddStep("start match externally", () => client.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); - AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(client.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); + AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); } [Test] @@ -536,18 +536,18 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user (ready, host)", () => { - client.AddUser(new APIUser { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); - client.TransferHost(MultiplayerTestScene.PLAYER_1_ID); - client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); + multiplayerClient.AddUser(new APIUser { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); + multiplayerClient.TransferHost(MultiplayerTestScene.PLAYER_1_ID); + multiplayerClient.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); }); AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); ClickButtonWhenEnabled(); - AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("start match externally", () => client.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch()); AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen()); } @@ -572,16 +572,16 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user (ready, host)", () => { - client.AddUser(new APIUser { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); - client.TransferHost(MultiplayerTestScene.PLAYER_1_ID); - client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); + multiplayerClient.AddUser(new APIUser { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" }); + multiplayerClient.TransferHost(MultiplayerTestScene.PLAYER_1_ID); + multiplayerClient.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); }); ClickButtonWhenEnabled(); - AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("start match externally", () => client.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch()); AddStep("restore beatmap", () => { @@ -608,7 +608,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("disconnect", () => client.Disconnect()); + AddStep("disconnect", () => multiplayerClient.Disconnect()); AddUntilStep("back in lounge", () => this.ChildrenOfType().FirstOrDefault()?.IsCurrentScreen() == true); } @@ -718,7 +718,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); AddAssert("local room has correct settings", () => { @@ -745,11 +745,11 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); - AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddStep("set spectating state", () => multiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("state set to spectating", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready)); + AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); + AddStep("set other user ready", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready)); pressReadyButton(1234); AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); @@ -761,7 +761,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen()); - AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); + AddUntilStep("user state is idle", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); } [Test] @@ -781,16 +781,16 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); - AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddStep("set spectating state", () => multiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("state set to spectating", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready)); + AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); + AddStep("set other user ready", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready)); pressReadyButton(1234); AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); - AddStep("set other user loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); - AddStep("set other user finished play", () => client.ChangeUserState(1234, MultiplayerUserState.FinishedPlay)); + AddStep("set other user loaded", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Loaded)); + AddStep("set other user finished play", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.FinishedPlay)); AddStep("press back button and exit", () => { @@ -800,7 +800,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen()); AddWaitStep("wait for possible state change", 5); - AddUntilStep("user state is spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddUntilStep("user state is spectating", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); } [Test] @@ -822,13 +822,13 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); - AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem + AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); + AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID })).WaitSafely()); - AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); + AddUntilStep("item arrived in playlist", () => multiplayerClient.Room?.Playlist.Count == 2); AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); AddUntilStep("queue contains item", () => this.ChildrenOfType().Single().Items.Single().ID == 2); @@ -853,16 +853,16 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); - AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem + AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); + AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo.OnlineID })).WaitSafely()); - AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); + AddUntilStep("item arrived in playlist", () => multiplayerClient.Room?.Playlist.Count == 2); - AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2).WaitSafely()); - AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1); + AddStep("delete item as other user", () => multiplayerClient.RemoveUserPlaylistItem(1234, 2).WaitSafely()); + AddUntilStep("item removed from playlist", () => multiplayerClient.Room?.Playlist.Count == 1); AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0); @@ -886,17 +886,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user and make host", () => { - client.AddUser(new APIUser { Id = 1234 }); - client.TransferHost(1234); + multiplayerClient.AddUser(new APIUser { Id = 1234 }); + multiplayerClient.TransferHost(1234); }); - AddStep("set local user spectating", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); - AddUntilStep("wait for spectating state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + AddStep("set local user spectating", () => multiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("wait for spectating state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); runGameplay(); - AddStep("exit gameplay for other user", () => client.ChangeUserState(1234, MultiplayerUserState.Idle)); - AddUntilStep("wait for room to be idle", () => client.Room?.State == MultiplayerRoomState.Open); + AddStep("exit gameplay for other user", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Idle)); + AddUntilStep("wait for room to be idle", () => multiplayerClient.Room?.State == MultiplayerRoomState.Open); runGameplay(); @@ -904,13 +904,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("start match by other user", () => { - client.ChangeUserState(1234, MultiplayerUserState.Ready); - client.StartMatch(); + multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready); + multiplayerClient.StartMatch(); }); - AddUntilStep("wait for loading", () => client.Room?.State == MultiplayerRoomState.WaitingForLoad); - AddStep("set player loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); - AddUntilStep("wait for gameplay to start", () => client.Room?.State == MultiplayerRoomState.Playing); + AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); + AddStep("set player loaded", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Loaded)); + AddUntilStep("wait for gameplay to start", () => multiplayerClient.Room?.State == MultiplayerRoomState.Playing); AddUntilStep("wait for local user to enter spectator", () => multiplayerComponents.CurrentScreen is MultiSpectatorScreen); } } @@ -935,7 +935,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("click ready button", () => { - user = playingUserId == null ? client.LocalUser : client.Room?.Users.Single(u => u.UserID == playingUserId); + user = playingUserId == null ? multiplayerClient.LocalUser : multiplayerClient.Room?.Users.Single(u => u.UserID == playingUserId); lastState = user?.State ?? MultiplayerUserState.Idle; InputManager.MoveMouseTo(readyButton); @@ -955,7 +955,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 437f4e3308..6605f82d5c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int user in users) { SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); - multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new APIUser { Id = user }, true)); + multiplayerUsers.Add(OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true)); } Children = new Drawable[] @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for load", () => leaderboard.IsLoaded); - AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0); + AddUntilStep("wait for user population", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count > 0); } [Test] @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestUserQuit() { foreach (int user in users) - AddStep($"mark user {user} quit", () => Client.RemoveUser(UserLookupCache.GetUserAsync(user).GetResultSafely().AsNonNull())); + AddStep($"mark user {user} quit", () => MultiplayerClient.RemoveUser(UserLookupCache.GetUserAsync(user).GetResultSafely().AsNonNull())); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index cd3ae50dab..dabc1c1e5a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int user in users) { SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); - var roomUser = OnlinePlayDependencies.Client.AddUser(new APIUser { Id = user }, true); + var roomUser = OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true); roomUser.MatchState = new TeamVersusUserState { @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for load", () => leaderboard.IsLoaded); - AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0); + AddUntilStep("wait for user population", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count > 0); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 7c18ed2572..7d2ef8276d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); - AddStep("select swap mod", () => Client.ChangeUserMods(API.LocalUser.Value.OnlineID, new[] { new TaikoModSwap() })); + AddStep("select swap mod", () => MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, new[] { new TaikoModSwap() })); AddUntilStep("participant panel has mod", () => this.ChildrenOfType().Any(p => p.ChildrenOfType().Any(m => m.Mod is TaikoModSwap))); } @@ -141,17 +141,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user (ready)", () => { - Client.AddUser(new APIUser { Id = PLAYER_1_ID }); - Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready); + MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }); + MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready); }); ClickButtonWhenEnabled(); - AddUntilStep("wait for spectating user state", () => Client.LocalUser?.State == MultiplayerUserState.Spectating); + AddUntilStep("wait for spectating user state", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); ClickButtonWhenEnabled(); - AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); + AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 671b85164b..292319171d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); - AddStep("add user", () => Client.AddUser(new APIUser + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, Username = "Second", @@ -50,15 +50,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); - AddStep("add non-resolvable user", () => Client.TestAddUnresolvedUser()); - AddAssert("null user added", () => Client.Room.AsNonNull().Users.Count(u => u.User == null) == 1); + AddStep("add non-resolvable user", () => MultiplayerClient.TestAddUnresolvedUser()); + AddAssert("null user added", () => MultiplayerClient.Room.AsNonNull().Users.Count(u => u.User == null) == 1); AddUntilStep("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); AddStep("kick null user", () => this.ChildrenOfType().Single(p => p.User.User == null) .ChildrenOfType().Single().TriggerClick()); - AddAssert("null user kicked", () => Client.Room.AsNonNull().Users.Count == 1); + AddAssert("null user kicked", () => MultiplayerClient.Room.AsNonNull().Users.Count == 1); } [Test] @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add a user", () => { - Client.AddUser(secondUser = new APIUser + MultiplayerClient.AddUser(secondUser = new APIUser { Id = 3, Username = "Second", @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); }); - AddStep("remove host", () => Client.RemoveUser(API.LocalUser.Value)); + AddStep("remove host", () => MultiplayerClient.RemoveUser(API.LocalUser.Value)); AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.User == secondUser); } @@ -84,21 +84,21 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestGameStateHasPriorityOverDownloadState() { - AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); checkProgressBarVisibility(true); - AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Results)); + AddStep("make user ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Results)); checkProgressBarVisibility(false); AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); - AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Idle)); + AddStep("make user ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Idle)); checkProgressBarVisibility(true); } [Test] public void TestCorrectInitialState() { - AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); createNewParticipantsList(); checkProgressBarVisibility(true); } @@ -106,23 +106,23 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBeatmapDownloadingStates() { - AddStep("set to no map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); - AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); + AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); checkProgressBarVisibility(true); AddRepeatStep("increment progress", () => { float progress = this.ChildrenOfType().Single().User.BeatmapAvailability.DownloadProgress ?? 0; - Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f))); + MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f))); }, 25); AddAssert("progress bar increased", () => this.ChildrenOfType().Single().Current.Value > 0); - AddStep("set to importing map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Importing())); + AddStep("set to importing map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Importing())); checkProgressBarVisibility(false); - AddStep("set to available", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); + AddStep("set to available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); } [Test] @@ -130,24 +130,24 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); - AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready)); + AddStep("make user ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Ready)); AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); - AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle)); + AddStep("make user idle", () => MultiplayerClient.ChangeState(MultiplayerUserState.Idle)); AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); } [Test] public void TestToggleSpectateState() { - AddStep("make user spectating", () => Client.ChangeState(MultiplayerUserState.Spectating)); - AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle)); + AddStep("make user spectating", () => MultiplayerClient.ChangeState(MultiplayerUserState.Spectating)); + AddStep("make user idle", () => MultiplayerClient.ChangeState(MultiplayerUserState.Idle)); } [Test] public void TestCrownChangesStateWhenHostTransferred() { - AddStep("add user", () => Client.AddUser(new APIUser + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, Username = "Second", @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("first user crown visible", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 1); AddUntilStep("second user crown hidden", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 0); - AddStep("make second user host", () => Client.TransferHost(3)); + AddStep("make second user host", () => MultiplayerClient.TransferHost(3)); AddUntilStep("first user crown hidden", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 0); AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestKickButtonOnlyPresentWhenHost() { - AddStep("add user", () => Client.AddUser(new APIUser + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, Username = "Second", @@ -175,11 +175,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); - AddStep("make second user host", () => Client.TransferHost(3)); + AddStep("make second user host", () => MultiplayerClient.TransferHost(3)); AddUntilStep("kick buttons not visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 0); - AddStep("make local user host again", () => Client.TransferHost(API.LocalUser.Value.Id)); + AddStep("make local user host again", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id)); AddUntilStep("kick buttons visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 1); } @@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestKickButtonKicks() { - AddStep("add user", () => Client.AddUser(new APIUser + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = 3, Username = "Second", @@ -196,7 +196,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("kick second user", () => this.ChildrenOfType().Single(d => d.IsPresent).TriggerClick()); - AddAssert("second user kicked", () => Client.Room?.Users.Single().UserID == API.LocalUser.Value.Id); + AddAssert("second user kicked", () => MultiplayerClient.Room?.Users.Single().UserID == API.LocalUser.Value.Id); } [Test] @@ -206,7 +206,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { for (int i = 0; i < 20; i++) { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = i, Username = $"User {i}", @@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Multiplayer CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); - Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1)); + MultiplayerClient.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1)); if (RNG.NextBool()) { @@ -229,15 +229,15 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (beatmapState) { case DownloadState.NotDownloaded: - Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.NotDownloaded()); + MultiplayerClient.ChangeUserBeatmapAvailability(i, BeatmapAvailability.NotDownloaded()); break; case DownloadState.Downloading: - Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Downloading(RNG.NextSingle())); + MultiplayerClient.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Downloading(RNG.NextSingle())); break; case DownloadState.Importing: - Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Importing()); + MultiplayerClient.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Importing()); break; } } @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add user", () => { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 0, Username = "User 0", @@ -264,7 +264,7 @@ namespace osu.Game.Tests.Visual.Multiplayer CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); - Client.ChangeUserMods(0, new Mod[] + MultiplayerClient.ChangeUserMods(0, new Mod[] { new OsuModHardRock(), new OsuModDifficultyAdjust { ApproachRate = { Value = 1 } } @@ -274,12 +274,12 @@ namespace osu.Game.Tests.Visual.Multiplayer for (var i = MultiplayerUserState.Idle; i < MultiplayerUserState.Results; i++) { var state = i; - AddStep($"set state: {state}", () => Client.ChangeUserState(0, state)); + AddStep($"set state: {state}", () => MultiplayerClient.ChangeUserState(0, state)); } - AddStep("set state: downloading", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.Downloading(0))); + AddStep("set state: downloading", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.Downloading(0))); - AddStep("set state: locally available", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); + AddStep("set state: locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); } [Test] @@ -287,7 +287,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add dummy mods", () => { - Client.ChangeUserMods(new Mod[] + MultiplayerClient.ChangeUserMods(new Mod[] { new OsuModNoFail(), new OsuModDoubleTime() @@ -296,7 +296,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add user with mods", () => { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 0, Username = "Baka", @@ -309,34 +309,34 @@ namespace osu.Game.Tests.Visual.Multiplayer }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); - Client.ChangeUserMods(0, new Mod[] + MultiplayerClient.ChangeUserMods(0, new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }); }); - AddStep("set 0 ready", () => Client.ChangeState(MultiplayerUserState.Ready)); + AddStep("set 0 ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Ready)); - AddStep("set 1 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating)); + AddStep("set 1 spectate", () => MultiplayerClient.ChangeUserState(0, MultiplayerUserState.Spectating)); // Have to set back to idle due to status priority. AddStep("set 0 no map, 1 ready", () => { - Client.ChangeState(MultiplayerUserState.Idle); - Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()); - Client.ChangeUserState(0, MultiplayerUserState.Ready); + MultiplayerClient.ChangeState(MultiplayerUserState.Idle); + MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()); + MultiplayerClient.ChangeUserState(0, MultiplayerUserState.Ready); }); - AddStep("set 0 downloading", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + AddStep("set 0 downloading", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); - AddStep("set 0 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating)); + AddStep("set 0 spectate", () => MultiplayerClient.ChangeUserState(0, MultiplayerUserState.Spectating)); AddStep("make both default", () => { - Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()); - Client.ChangeUserState(0, MultiplayerUserState.Idle); - Client.ChangeState(MultiplayerUserState.Idle); + MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()); + MultiplayerClient.ChangeUserState(0, MultiplayerUserState.Idle); + MultiplayerClient.ChangeState(MultiplayerUserState.Idle); }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 010e9dc078..d391fbcf8f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -28,15 +28,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("initialise gameplay", () => { - Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem + Stack.Push(player = new MultiplayerPlayer(MultiplayerClient.APIRoom, new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, - }, Client.Room?.Users.ToArray())); + }, MultiplayerClient.Room?.Users.ToArray())); }); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded); - AddStep("start gameplay", () => ((IMultiplayerClient)Client).MatchStarted()); + AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).MatchStarted()); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 7bc5990c32..f84abc7443 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Multiplayer importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); }); - AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddStep("change to all players mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); } [Test] @@ -97,19 +97,19 @@ namespace osu.Game.Tests.Visual.Multiplayer addItemStep(); addItemStep(); - AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); + AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(1, 0); assertItemInQueueListStep(2, 0); assertItemInQueueListStep(3, 1); - AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); + AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(2, 0); assertItemInHistoryListStep(1, 1); assertItemInQueueListStep(3, 0); - AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); + AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); assertItemInHistoryListStep(3, 0); assertItemInHistoryListStep(2, 1); @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestListsClearedWhenRoomLeft() { addItemStep(); - AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); + AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); AddStep("leave room", () => RoomManager.PartRoom()); AddUntilStep("wait for room part", () => !RoomJoined); @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Multiplayer /// /// Adds a step to create a new playlist item. /// - private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem + private void addItemStep(bool expired = false) => AddStep("add item", () => MultiplayerClient.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem { Beatmap = { Value = importedBeatmap }, BeatmapID = importedBeatmap.OnlineID, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 0b3e1ba20a..bf06b6ad73 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(500, 300), - Items = { BindTarget = Client.APIRoom!.Playlist } + Items = { BindTarget = MultiplayerClient.APIRoom!.Playlist } }; }); @@ -61,14 +61,14 @@ namespace osu.Game.Tests.Visual.Multiplayer importedBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); }); - AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddStep("change to all players mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); } [Test] public void TestDeleteButtonAlwaysVisibleForHost() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); - AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddUntilStep("wait for queue mode change", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); addPlaylistItem(() => API.LocalUser.Value.OnlineID); assertDeleteButtonVisibility(1, true); @@ -79,18 +79,18 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); - AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddUntilStep("wait for queue mode change", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); - AddStep("join other user", () => Client.AddUser(new APIUser { Id = 1234 })); - AddStep("set other user as host", () => Client.TransferHost(1234)); + AddStep("join other user", () => MultiplayerClient.AddUser(new APIUser { Id = 1234 })); + AddStep("set other user as host", () => MultiplayerClient.TransferHost(1234)); addPlaylistItem(() => API.LocalUser.Value.OnlineID); assertDeleteButtonVisibility(1, true); addPlaylistItem(() => 1234); assertDeleteButtonVisibility(2, false); - AddStep("set local user as host", () => Client.TransferHost(API.LocalUser.Value.OnlineID)); + AddStep("set local user as host", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); assertDeleteButtonVisibility(1, true); assertDeleteButtonVisibility(2, true); } @@ -98,16 +98,16 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestCurrentItemDoesNotHaveDeleteButton() { - AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); - AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); + AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); + AddUntilStep("wait for queue mode change", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); addPlaylistItem(() => API.LocalUser.Value.OnlineID); assertDeleteButtonVisibility(0, false); assertDeleteButtonVisibility(1, true); - AddStep("finish current item", () => Client.FinishCurrentItem().WaitSafely()); - AddUntilStep("wait for next item to be selected", () => Client.Room?.Settings.PlaylistItemId == 2); + AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); + AddUntilStep("wait for next item to be selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == 2); AddUntilStep("wait for two items in playlist", () => playlist.ChildrenOfType().Count() == 2); assertDeleteButtonVisibility(0, false); @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapID = importedBeatmap.OnlineID, }); - Client.AddUserPlaylistItem(userId(), item).WaitSafely(); + MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely(); itemId = item.ID; }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 7834226f15..609693e54b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -74,13 +74,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Task.Run(async () => { - if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + if (MultiplayerClient.IsHost && MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready) { - await Client.StartMatch(); + await MultiplayerClient.StartMatch(); return; } - await Client.ToggleReady(); + await MultiplayerClient.ToggleReady(); readyClickOperation.Dispose(); }); @@ -110,15 +110,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add second user as host", () => { - Client.AddUser(new APIUser { Id = 2, Username = "Another user" }); - Client.TransferHost(2); + MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); + MultiplayerClient.TransferHost(2); }); ClickButtonWhenEnabled(); - AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); ClickButtonWhenEnabled(); - AddUntilStep("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } [TestCase(true)] @@ -127,14 +127,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("setup", () => { - Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0); if (!allReady) - Client.AddUser(new APIUser { Id = 2, Username = "Another user" }); + MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); ClickButtonWhenEnabled(); - AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); verifyGameplayStartFlow(); } @@ -144,12 +144,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add host", () => { - Client.AddUser(new APIUser { Id = 2, Username = "Another user" }); - Client.TransferHost(2); + MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); + MultiplayerClient.TransferHost(2); }); ClickButtonWhenEnabled(); - AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); + AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0)); verifyGameplayStartFlow(); } @@ -159,17 +159,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("setup", () => { - Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); - Client.AddUser(new APIUser { Id = 2, Username = "Another user" }); + MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0); + MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); ClickButtonWhenEnabled(); - AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0)); + AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); ClickButtonWhenEnabled(); - AddUntilStep("user is idle (match not started)", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); } @@ -180,42 +180,42 @@ namespace osu.Game.Tests.Visual.Multiplayer const int users = 10; AddStep("setup", () => { - Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0); for (int i = 0; i < users; i++) - Client.AddUser(new APIUser { Id = i, Username = "Another user" }); + MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" }); }); if (!isHost) - AddStep("transfer host", () => Client.TransferHost(2)); + AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); ClickButtonWhenEnabled(); AddRepeatStep("change user ready state", () => { - Client.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle); + MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle); }, 20); AddRepeatStep("ready all users", () => { - var nextUnready = Client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); + var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); if (nextUnready != null) - Client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); + MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); }, users); } private void verifyGameplayStartFlow() { - AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); ClickButtonWhenEnabled(); - AddUntilStep("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddAssert("ready button disabled", () => !button.ChildrenOfType().Single().Enabled.Value); AddStep("transitioned to gameplay", () => readyClickOperation.Dispose()); AddStep("finish gameplay", () => { - Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded); - Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay); + MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded); + MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay); }); AddUntilStep("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 70d4d9dd55..c4ea78116a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Task.Run(async () => { - await Client.ToggleSpectate(); + await MultiplayerClient.ToggleSpectate(); readyClickOperation.Dispose(); }); } @@ -94,13 +94,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Task.Run(async () => { - if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + if (MultiplayerClient.IsHost && MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready) { - await Client.StartMatch(); + await MultiplayerClient.StartMatch(); return; } - await Client.ToggleReady(); + await MultiplayerClient.ToggleReady(); readyClickOperation.Dispose(); }); @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [TestCase(MultiplayerRoomState.Playing)] public void TestEnabledWhenRoomOpenOrInGameplay(MultiplayerRoomState roomState) { - AddStep($"change room to {roomState}", () => Client.ChangeRoomState(roomState)); + AddStep($"change room to {roomState}", () => MultiplayerClient.ChangeRoomState(roomState)); assertSpectateButtonEnablement(true); } @@ -124,16 +124,16 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestToggleWhenIdle(MultiplayerUserState initialState) { ClickButtonWhenEnabled(); - AddUntilStep("user is spectating", () => Client.Room?.Users[0].State == MultiplayerUserState.Spectating); + AddUntilStep("user is spectating", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Spectating); ClickButtonWhenEnabled(); - AddUntilStep("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } [TestCase(MultiplayerRoomState.Closed)] public void TestDisabledWhenClosed(MultiplayerRoomState roomState) { - AddStep($"change room to {roomState}", () => Client.ChangeRoomState(roomState)); + AddStep($"change room to {roomState}", () => MultiplayerClient.ChangeRoomState(roomState)); assertSpectateButtonEnablement(false); } @@ -147,8 +147,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestReadyButtonEnabledWhenHostAndUsersReady() { - AddStep("add user", () => Client.AddUser(new APIUser { Id = PLAYER_1_ID })); - AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID })); + AddStep("set user ready", () => MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); ClickButtonWhenEnabled(); assertReadyButtonEnablement(true); @@ -159,11 +159,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add user and transfer host", () => { - Client.AddUser(new APIUser { Id = PLAYER_1_ID }); - Client.TransferHost(PLAYER_1_ID); + MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }); + MultiplayerClient.TransferHost(PLAYER_1_ID); }); - AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); + AddStep("set user ready", () => MultiplayerClient.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); ClickButtonWhenEnabled(); assertReadyButtonEnablement(false); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs index 823ac07cf7..f95e73ff3c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRankRangePill.cs @@ -25,14 +25,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add user", () => { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 2, Statistics = { GlobalRank = 1234 } }); // Remove the local user so only the one above is displayed. - Client.RemoveUser(API.LocalUser.Value); + MultiplayerClient.RemoveUser(API.LocalUser.Value); }); } @@ -41,26 +41,26 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add users", () => { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 2, Statistics = { GlobalRank = 1234 } }); - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 3, Statistics = { GlobalRank = 3333 } }); - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 4, Statistics = { GlobalRank = 4321 } }); // Remove the local user so only the ones above are displayed. - Client.RemoveUser(API.LocalUser.Value); + MultiplayerClient.RemoveUser(API.LocalUser.Value); }); } @@ -75,20 +75,20 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add users", () => { - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 2, Statistics = { GlobalRank = min } }); - Client.AddUser(new APIUser + MultiplayerClient.AddUser(new APIUser { Id = 3, Statistics = { GlobalRank = max } }); // Remove the local user so only the ones above are displayed. - Client.RemoveUser(API.LocalUser.Value); + MultiplayerClient.RemoveUser(API.LocalUser.Value); }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 7a5bd0b8f4..50b3f52047 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerComponents multiplayerComponents; - private TestMultiplayerClient client => multiplayerComponents.Client; + private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -75,8 +75,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); - AddAssert("user state arrived", () => client.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState); + AddUntilStep("room type is team vs", () => multiplayerClient.Room?.Settings.MatchType == MatchType.TeamVersus); + AddAssert("user state arrived", () => multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState); } [Test] @@ -96,25 +96,25 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); - AddStep("add another user", () => client.AddUser(new APIUser { Username = "otheruser", Id = 44 })); + AddAssert("user on team 0", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); + AddStep("add another user", () => multiplayerClient.AddUser(new APIUser { Username = "otheruser", Id = 44 })); AddStep("press own button", () => { InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); - AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1); + AddAssert("user on team 1", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1); AddStep("press own button again", () => InputManager.Click(MouseButton.Left)); - AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); + AddAssert("user on team 0", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); AddStep("press other user's button", () => { InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Left); }); - AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); + AddAssert("user still on team 0", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); } [Test] @@ -134,14 +134,14 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddUntilStep("match type head to head", () => client.APIRoom?.Type.Value == MatchType.HeadToHead); + AddUntilStep("match type head to head", () => multiplayerClient.APIRoom?.Type.Value == MatchType.HeadToHead); - AddStep("change match type", () => client.ChangeSettings(new MultiplayerRoomSettings + AddStep("change match type", () => multiplayerClient.ChangeSettings(new MultiplayerRoomSettings { MatchType = MatchType.TeamVersus }).WaitSafely()); - AddUntilStep("api room updated to team versus", () => client.APIRoom?.Type.Value == MatchType.TeamVersus); + AddUntilStep("api room updated to team versus", () => multiplayerClient.APIRoom?.Type.Value == MatchType.TeamVersus); } [Test] @@ -160,13 +160,13 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddUntilStep("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead); + AddUntilStep("room type is head to head", () => multiplayerClient.Room?.Settings.MatchType == MatchType.HeadToHead); AddUntilStep("team displays are not displaying teams", () => multiplayerComponents.ChildrenOfType().All(d => d.DisplayedTeam == null)); - AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus)); + AddStep("change to team vs", () => multiplayerClient.ChangeSettings(matchType: MatchType.TeamVersus)); - AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); + AddUntilStep("room type is team vs", () => multiplayerClient.Room?.Settings.MatchType == MatchType.TeamVersus); AddUntilStep("team displays are displaying teams", () => multiplayerComponents.ChildrenOfType().All(d => d.DisplayedTeam != null)); } @@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddUntilStep("wait for join", () => client.RoomJoined); + AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); } } } diff --git a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs index bd8fb8e58e..062aa33b99 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual public new bool IsLoaded => base.IsLoaded && MultiplayerScreen.IsLoaded; [Cached(typeof(MultiplayerClient))] - public readonly TestMultiplayerClient Client; + public readonly TestMultiplayerClient MultiplayerClient; [Cached(typeof(UserLookupCache))] private readonly UserLookupCache userLookupCache = new TestUserLookupCache(); @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual { userLookupCache, beatmapLookupCache, - Client = new TestMultiplayerClient(RoomManager), + MultiplayerClient = new TestMultiplayerClient(RoomManager), screenStack = new OsuScreenStack { Name = nameof(TestMultiplayerComponents), diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs index f166154103..62d1c9ceca 100644 --- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.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.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; @@ -14,9 +13,9 @@ namespace osu.Game.Tests.Visual.Multiplayer public interface IMultiplayerTestSceneDependencies : IOnlinePlayTestSceneDependencies { /// - /// The cached . + /// The cached . /// - TestMultiplayerClient Client { get; } + TestMultiplayerClient MultiplayerClient { get; } /// /// The cached . diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index a9b3ca4991..19f9952d14 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -17,13 +17,13 @@ namespace osu.Game.Tests.Visual.Multiplayer public const int PLAYER_1_ID = 55; public const int PLAYER_2_ID = 56; - public TestMultiplayerClient Client => OnlinePlayDependencies.Client; + public TestMultiplayerClient MultiplayerClient => OnlinePlayDependencies.MultiplayerClient; public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient; protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; - public bool RoomJoined => Client.RoomJoined; + public bool RoomJoined => MultiplayerClient.RoomJoined; private readonly bool joinRoom; diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs index d9fe77ae44..6b4e01b673 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs @@ -14,16 +14,16 @@ namespace osu.Game.Tests.Visual.Multiplayer /// public class MultiplayerTestSceneDependencies : OnlinePlayTestSceneDependencies, IMultiplayerTestSceneDependencies { - public TestMultiplayerClient Client { get; } + public TestMultiplayerClient MultiplayerClient { get; } public TestSpectatorClient SpectatorClient { get; } public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager; public MultiplayerTestSceneDependencies() { - Client = new TestMultiplayerClient(RoomManager); + MultiplayerClient = new TestMultiplayerClient(RoomManager); SpectatorClient = CreateSpectatorClient(); - CacheAs(Client); + CacheAs(MultiplayerClient); CacheAs(SpectatorClient); } From 5db63a8751e38d9701b9987ae30508b9be516475 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 11:30:03 +0900 Subject: [PATCH 095/306] Expose read-only list from request --- osu.Game/Online/API/Requests/GetBeatmapsRequest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs index c07e5ef1c2..22af022659 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs @@ -2,12 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; namespace osu.Game.Online.API.Requests { public class GetBeatmapsRequest : APIRequest { - public readonly int[] BeatmapIds; + public readonly IReadOnlyList BeatmapIds; private const int max_ids_per_request = 50; From 84e82ef5e466d1802f1273e72a389cdf1f54c6fd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 14:09:19 +0900 Subject: [PATCH 096/306] Add XMLDocs to difficulty attribute properties --- .../Difficulty/CatchDifficultyAttributes.cs | 6 +++ .../Difficulty/ManiaDifficultyAttributes.cs | 9 +++++ .../Difficulty/OsuDifficultyAttributes.cs | 38 +++++++++++++++++++ .../Difficulty/TaikoDifficultyAttributes.cs | 21 ++++++++++ .../Difficulty/DifficultyAttributes.cs | 2 +- 5 files changed, 75 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 39a58d336d..8e069d7d16 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -9,6 +9,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyAttributes : DifficultyAttributes { + /// + /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("approach_rate")] public double ApproachRate { get; set; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index 979a04ddf8..efd5868504 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -9,9 +9,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty { public class ManiaDifficultyAttributes : DifficultyAttributes { + /// + /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("great_hit_window")] public double GreatHitWindow { get; set; } + /// + /// The score multiplier applied via score-reducing mods. + /// [JsonProperty("score_multiplier")] public double ScoreMultiplier { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 126a9b0183..3deed4ea3d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -12,30 +12,68 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuDifficultyAttributes : DifficultyAttributes { + /// + /// The difficulty corresponding to the aim skill. + /// [JsonProperty("aim_difficulty")] public double AimDifficulty { get; set; } + /// + /// The difficulty corresponding to the speed skill. + /// [JsonProperty("speed_difficulty")] public double SpeedDifficulty { get; set; } + /// + /// The difficulty corresponding to the flashlight skill. + /// [JsonProperty("flashlight_difficulty")] public double FlashlightDifficulty { get; set; } + /// + /// Describes how much of is contributed to by hitcircles or sliders. + /// A value closer to 1.0 indicates most of is contributed by hitcircles. + /// A value closer to 0.0 indicates most of is contributed by sliders. + /// [JsonProperty("slider_factor")] public double SliderFactor { get; set; } + /// + /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("approach_rate")] public double ApproachRate { get; set; } + /// + /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("overall_difficulty")] public double OverallDifficulty { get; set; } + /// + /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods. + /// public double DrainRate { get; set; } + /// + /// The number of hitcircles in the beatmap. + /// public int HitCircleCount { get; set; } + /// + /// The number of sliders in the beatmap. + /// public int SliderCount { get; set; } + /// + /// The number of spinners in the beatmap. + /// public int SpinnerCount { get; set; } public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 31f5a6f570..3dc5438072 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -9,18 +9,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyAttributes : DifficultyAttributes { + /// + /// The difficulty corresponding to the stamina skill. + /// [JsonProperty("stamina_difficulty")] public double StaminaDifficulty { get; set; } + /// + /// The difficulty corresponding to the rhythm skill. + /// [JsonProperty("rhythm_difficulty")] public double RhythmDifficulty { get; set; } + /// + /// The difficulty corresponding to the colour skill. + /// [JsonProperty("colour_difficulty")] public double ColourDifficulty { get; set; } + /// + /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("approach_rate")] public double ApproachRate { get; set; } + /// + /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + /// + /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. + /// [JsonProperty("great_hit_window")] public double GreatHitWindow { get; set; } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 991b567f57..ec3d22b67a 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Difficulty public Mod[] Mods { get; set; } /// - /// The combined star rating of all skill. + /// The combined star rating of all skills. /// [JsonProperty("star_rating", Order = -3)] public double StarRating { get; set; } From f703828e1bc17e3ec5421dbc09ea9a62ca3f8437 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 15:27:48 +0900 Subject: [PATCH 097/306] Clarify ambiguous conditionals in `LegacyStageBackground` --- .../Skinning/Legacy/LegacyStageBackground.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs index 952fc7ddd6..fdacc75c92 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs @@ -98,8 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy float rightLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1; bool hasLeftLine = leftLineWidth > 0; - bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value >= 2.4m - || isLastColumn; + bool hasRightLine = (rightLineWidth > 0 && skin.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value >= 2.4m) || isLastColumn; Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White; Color4 backgroundColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, columnIndex)?.Value ?? Color4.Black; From edd31bf3aa913a7b01306538f88a6c44d160d729 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 15:51:18 +0900 Subject: [PATCH 098/306] Revert styling change --- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index faeb73cfa3..8d840280b3 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -430,34 +430,31 @@ namespace osu.Game.Screens.OnlinePlay }; } - private IEnumerable createButtons() + private IEnumerable createButtons() => new[] { - return new[] + showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) { - showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) - { - Size = new Vector2(30, 30), - Action = () => RequestResults?.Invoke(Item), - Alpha = AllowShowingResults ? 1 : 0, - TooltipText = "View results" - }, - beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap), - editButton = new PlaylistEditButton - { - Size = new Vector2(30, 30), - Alpha = AllowEditing ? 1 : 0, - Action = () => RequestEdit?.Invoke(Item), - TooltipText = "Edit" - }, - removeButton = new PlaylistRemoveButton - { - Size = new Vector2(30, 30), - Alpha = AllowDeletion ? 1 : 0, - Action = () => RequestDeletion?.Invoke(Item), - TooltipText = "Remove from playlist" - }, - }; - } + Size = new Vector2(30, 30), + Action = () => RequestResults?.Invoke(Item), + Alpha = AllowShowingResults ? 1 : 0, + TooltipText = "View results" + }, + beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap), + editButton = new PlaylistEditButton + { + Size = new Vector2(30, 30), + Alpha = AllowEditing ? 1 : 0, + Action = () => RequestEdit?.Invoke(Item), + TooltipText = "Edit" + }, + removeButton = new PlaylistRemoveButton + { + Size = new Vector2(30, 30), + Alpha = AllowDeletion ? 1 : 0, + Action = () => RequestDeletion?.Invoke(Item), + TooltipText = "Remove from playlist" + }, + }; protected override bool OnClick(ClickEvent e) { From 55d9f0b44b507d4ff8140271e7fb3807ad2c4ef2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 16:03:08 +0900 Subject: [PATCH 099/306] Store beatmap to a field instead --- .../OnlinePlayBeatmapAvailabilityTracker.cs | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 02bcd1f30c..07506ba1f0 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -46,10 +46,9 @@ namespace osu.Game.Online.Rooms private readonly Bindable availability = new Bindable(BeatmapAvailability.NotDownloaded()); private ScheduledDelegate progressUpdate; - private BeatmapDownloadTracker downloadTracker; - private IDisposable realmSubscription; + private APIBeatmap selectedBeatmap; protected override void LoadComplete() { @@ -63,26 +62,30 @@ namespace osu.Game.Online.Rooms return; downloadTracker?.RemoveAndDisposeImmediately(); + selectedBeatmap = null; beatmapLookupCache.GetBeatmapAsync(item.NewValue.Beatmap.OnlineID).ContinueWith(task => Schedule(() => { var beatmap = task.GetResultSafely(); if (SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID) - beginTracking(beatmap); + { + selectedBeatmap = beatmap; + beginTracking(); + } }), TaskContinuationOptions.OnlyOnRanToCompletion); }, true); } - private void beginTracking(APIBeatmap beatmap) + private void beginTracking() { - Debug.Assert(beatmap.BeatmapSet != null); + Debug.Assert(selectedBeatmap.BeatmapSet != null); - downloadTracker = new BeatmapDownloadTracker(beatmap.BeatmapSet); + downloadTracker = new BeatmapDownloadTracker(selectedBeatmap.BeatmapSet); AddInternal(downloadTracker); - downloadTracker.State.BindValueChanged(_ => Scheduler.AddOnce(updateAvailability, beatmap), true); + downloadTracker.State.BindValueChanged(_ => Scheduler.AddOnce(updateAvailability), true); downloadTracker.Progress.BindValueChanged(_ => { if (downloadTracker.State.Value != DownloadState.Downloading) @@ -91,23 +94,23 @@ namespace osu.Game.Online.Rooms // incoming progress changes are going to be at a very high rate. // we don't want to flood the network with this, so rate limit how often we send progress updates. if (progressUpdate?.Completed != false) - progressUpdate = Scheduler.AddDelayed(updateAvailability, beatmap, progressUpdate == null ? 0 : 500); + progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); }, true); // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(beatmap), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(), (items, changes, ___) => { if (changes == null) return; - Scheduler.AddOnce(updateAvailability, beatmap); + Scheduler.AddOnce(updateAvailability); }); } - private void updateAvailability(APIBeatmap beatmap) + private void updateAvailability() { - if (downloadTracker == null) + if (downloadTracker == null || selectedBeatmap == null) return; switch (downloadTracker.State.Value) @@ -125,7 +128,7 @@ namespace osu.Game.Online.Rooms break; case DownloadState.LocallyAvailable: - bool available = filteredBeatmaps(beatmap).Any(); + bool available = filteredBeatmaps().Any(); availability.Value = available ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); @@ -140,10 +143,10 @@ namespace osu.Game.Online.Rooms } } - private IQueryable filteredBeatmaps(APIBeatmap beatmap) + private IQueryable filteredBeatmaps() { - int onlineId = beatmap.OnlineID; - string checksum = beatmap.MD5Hash; + int onlineId = selectedBeatmap.OnlineID; + string checksum = selectedBeatmap.MD5Hash; return realm.Realm .All() From d4bf335fccd31e921447b999a91d722998d51417 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 16:24:39 +0900 Subject: [PATCH 100/306] Use score multiplier attribute in ManiaPerformanceCalculator --- .../Difficulty/ManiaPerformanceCalculator.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 8a8c41bb8a..beba29b8bb 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -43,14 +43,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); - - double scoreMultiplier = 1.0; - foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) - scoreMultiplier *= m.ScoreMultiplier; - - // Scale score up, so it's comparable to other keymods - scaledScore *= 1.0 / scoreMultiplier; + if (Attributes.ScoreMultiplier > 0) + { + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / Attributes.ScoreMultiplier; + } // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // The specific number has no intrinsic meaning and can be adjusted as needed. @@ -80,6 +77,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeDifficultyValue() { + if (Attributes.ScoreMultiplier <= 0) + return 0; + double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); From 5dd9771c5fbb2ae68efe146b769375a6210b00d7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 16:27:27 +0900 Subject: [PATCH 101/306] Remove mod multipliers from being applied to scores --- .../TestSceneFooterButtonMods.cs | 27 ++----------------- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 13 +-------- osu.Game/Screens/Select/FooterButtonMods.cs | 25 ----------------- 3 files changed, 3 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 0631059d1a..e9014c0941 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Linq; using NUnit.Framework; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select; @@ -14,11 +12,11 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneFooterButtonMods : OsuTestScene { - private readonly TestFooterButtonMods footerButtonMods; + private readonly FooterButtonMods footerButtonMods; public TestSceneFooterButtonMods() { - Add(footerButtonMods = new TestFooterButtonMods()); + Add(footerButtonMods = new FooterButtonMods()); } [Test] @@ -26,19 +24,15 @@ namespace osu.Game.Tests.Visual.UserInterface { var hiddenMod = new Mod[] { new OsuModHidden() }; AddStep(@"Add Hidden", () => changeMods(hiddenMod)); - AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod)); var hardRockMod = new Mod[] { new OsuModHardRock() }; AddStep(@"Add HardRock", () => changeMods(hardRockMod)); - AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod)); var doubleTimeMod = new Mod[] { new OsuModDoubleTime() }; AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); - AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod)); var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods)); - AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods)); } [Test] @@ -46,15 +40,12 @@ namespace osu.Game.Tests.Visual.UserInterface { var easyMod = new Mod[] { new OsuModEasy() }; AddStep(@"Add Easy", () => changeMods(easyMod)); - AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod)); var noFailMod = new Mod[] { new OsuModNoFail() }; AddStep(@"Add NoFail", () => changeMods(noFailMod)); - AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod)); var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() }; AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods)); - AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods)); } [Test] @@ -63,25 +54,11 @@ namespace osu.Game.Tests.Visual.UserInterface var multipleMods = new Mod[] { new OsuModDoubleTime(), new OsuModFlashlight() }; AddStep(@"Add mods", () => changeMods(multipleMods)); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); - AddAssert(@"Check empty multiplier", () => assertModsMultiplier(Array.Empty())); } private void changeMods(IReadOnlyList mods) { footerButtonMods.Current.Value = mods; } - - private bool assertModsMultiplier(IEnumerable mods) - { - double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); - string expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; - - return expectedValue == footerButtonMods.MultiplierText.Current.Value; - } - - private class TestFooterButtonMods : FooterButtonMods - { - public new OsuSpriteText MultiplierText => base.MultiplierText; - } } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 79861c0ecc..ce1486b02f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -92,8 +92,6 @@ namespace osu.Game.Rulesets.Scoring private readonly List hitEvents = new List(); private HitObject lastHitObject; - private double scoreMultiplier = 1; - public ScoreProcessor() { accuracyPortion = DefaultAccuracyPortion; @@ -111,15 +109,6 @@ namespace osu.Game.Rulesets.Scoring }; Mode.ValueChanged += _ => updateScore(); - Mods.ValueChanged += mods => - { - scoreMultiplier = 1; - - foreach (var m in mods.NewValue) - scoreMultiplier *= m.ScoreMultiplier; - - updateScore(); - }; } private readonly Dictionary scoreResultCounts = new Dictionary(); @@ -235,7 +224,7 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Standardised: double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; + return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)); case ScoringMode.Classic: // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 5bbca5ca1a..1f1aa4c4b3 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -6,14 +6,11 @@ using osu.Framework.Graphics; using osu.Game.Screens.Play.HUD; using osu.Game.Rulesets.Mods; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osuTK; -using osuTK.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Screens.Select @@ -26,10 +23,7 @@ namespace osu.Game.Screens.Select set => modDisplay.Current = value; } - protected readonly OsuSpriteText MultiplierText; private readonly ModDisplay modDisplay; - private Color4 lowMultiplierColour; - private Color4 highMultiplierColour; public FooterButtonMods() { @@ -40,12 +34,6 @@ namespace osu.Game.Screens.Select Scale = new Vector2(0.8f), ExpansionMode = ExpansionMode.AlwaysContracted, }); - ButtonContentContainer.Add(MultiplierText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - }); } [BackgroundDependencyLoader] @@ -53,8 +41,6 @@ namespace osu.Game.Screens.Select { SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); - lowMultiplierColour = colours.Red; - highMultiplierColour = colours.Green; Text = @"mods"; Hotkey = GlobalAction.ToggleModSelection; } @@ -68,17 +54,6 @@ namespace osu.Game.Screens.Select private void updateMultiplierText() { - double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; - - MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; - - if (multiplier > 1.0) - MultiplierText.FadeColour(highMultiplierColour, 200); - else if (multiplier < 1.0) - MultiplierText.FadeColour(lowMultiplierColour, 200); - else - MultiplierText.FadeColour(Color4.White, 200); - if (Current.Value?.Count > 0) modDisplay.FadeIn(); else From 4c1413e0c793995d5a9ea4802419d9131d9abe53 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 16:36:02 +0900 Subject: [PATCH 102/306] No longer require Mod implementation --- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 7136795461..5555770822 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Mods /// The score multiplier of this mod. /// [JsonIgnore] - public abstract double ScoreMultiplier { get; } + public virtual double ScoreMultiplier => 1; /// /// Returns true if this mod is implemented (and playable). From 3d3f0a89c2c1b933eff1a7f8575ba7ac6a890f52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 16:46:53 +0900 Subject: [PATCH 103/306] Remove legacy `RulesetID` property from `BeatmapInfo` --- osu.Game/Beatmaps/BeatmapInfo.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index e4bfd768b7..305b3979a0 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -152,18 +152,6 @@ namespace osu.Game.Beatmaps #region Compatibility properties - [Ignored] - public int RulesetID - { - set - { - if (!string.IsNullOrEmpty(Ruleset.InstantiationInfo)) - throw new InvalidOperationException($"Cannot set a {nameof(RulesetID)} when {nameof(Ruleset)} is already set to an actual ruleset."); - - Ruleset.OnlineID = value; - } - } - [Ignored] [Obsolete("Use BeatmapInfo.Difficulty instead.")] // can be removed 20220719 public BeatmapDifficulty BaseDifficulty From 7a69de0060c2c55d731fd89f3a62d72eeb93c424 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 16:57:28 +0900 Subject: [PATCH 104/306] Split out realm portion of `RulesetStore` --- osu.Game/OsuGameBase.cs | 7 +- osu.Game/Rulesets/RealmRulesetStore.cs | 101 +++++++++++++++++++++++++ osu.Game/Rulesets/RulesetStore.cs | 101 +++---------------------- 3 files changed, 115 insertions(+), 94 deletions(-) create mode 100644 osu.Game/Rulesets/RealmRulesetStore.cs diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0b2644d5ba..d89e1e8193 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -22,6 +22,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; @@ -109,7 +110,7 @@ namespace osu.Game protected SkinManager SkinManager { get; private set; } - protected RulesetStore RulesetStore { get; private set; } + protected RealmRulesetStore RulesetStore { get; private set; } protected RealmKeyBindingStore KeyBindingStore { get; private set; } @@ -200,9 +201,11 @@ namespace osu.Game dependencies.Cache(realm = new RealmAccess(Storage, "client", EFContextFactory)); - dependencies.Cache(RulesetStore = new RulesetStore(realm, Storage)); + dependencies.CacheAs(RulesetStore = new RealmRulesetStore(realm, Storage)); dependencies.CacheAs(RulesetStore); + Decoder.RegisterDependencies(RulesetStore); + // Backup is taken here rather than in EFToRealmMigrator to avoid recycling realm contexts // after initial usages below. It can be moved once a direction is established for handling re-subscription. // See https://github.com/ppy/osu/pull/16547 for more discussion. diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs new file mode 100644 index 0000000000..f42bf06da4 --- /dev/null +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -0,0 +1,101 @@ +// 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.ObjectExtensions; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Database; + +#nullable enable + +namespace osu.Game.Rulesets +{ + public class RealmRulesetStore : RulesetStore + { + public override IEnumerable AvailableRulesets => availableRulesets; + + private readonly List availableRulesets = new List(); + + public RealmRulesetStore(RealmAccess realm, Storage? storage = null) + : base(storage) + { + prepareDetachedRulesets(realm); + } + + private void prepareDetachedRulesets(RealmAccess realmAccess) + { + realmAccess.Write(realm => + { + var rulesets = realm.All(); + + List instances = LoadedAssemblies.Values + .Select(r => Activator.CreateInstance(r) as Ruleset) + .Where(r => r != null) + .Select(r => r.AsNonNull()) + .ToList(); + + // add all legacy rulesets first to ensure they have exclusive choice of primary key. + foreach (var r in instances.Where(r => r is ILegacyRuleset)) + { + if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null) + realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); + } + + // 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 (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) + { + var existingSameShortName = rulesets.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 + realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); + } + } + + List detachedRulesets = new List(); + + // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage. + foreach (var r in rulesets.OrderBy(r => r.OnlineID)) + { + try + { + var resolvedType = Type.GetType(r.InstantiationInfo) + ?? throw new RulesetLoadException(@"Type could not be resolved"); + + var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo + ?? throw new RulesetLoadException(@"Instantiation failure"); + + // If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution. + // To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw. + resolvedType.Assembly.GetTypes(); + + r.Name = instanceInfo.Name; + r.ShortName = instanceInfo.ShortName; + r.InstantiationInfo = instanceInfo.InstantiationInfo; + r.Available = true; + + detachedRulesets.Add(r.Clone()); + } + catch (Exception ex) + { + r.Available = false; + Logger.Log($"Could not load ruleset {r}: {ex.Message}"); + } + } + + availableRulesets.AddRange(detachedRulesets.OrderBy(r => r)); + }); + } + } +} diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index dd25005006..6f88d97a58 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -7,34 +7,26 @@ using System.IO; using System.Linq; using System.Reflection; using osu.Framework; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Game.Database; #nullable enable namespace osu.Game.Rulesets { - public class RulesetStore : IDisposable, IRulesetStore + public abstract class RulesetStore : IDisposable, IRulesetStore { - private readonly RealmAccess realmAccess; - private const string ruleset_library_prefix = @"osu.Game.Rulesets"; - private readonly Dictionary loadedAssemblies = new Dictionary(); + protected readonly Dictionary LoadedAssemblies = new Dictionary(); /// /// All available rulesets. /// - public IEnumerable AvailableRulesets => availableRulesets; + public abstract IEnumerable AvailableRulesets { get; } - private readonly List availableRulesets = new List(); - - public RulesetStore(RealmAccess realm, Storage? storage = null) + protected RulesetStore(Storage? storage = null) { - realmAccess = realm; - // On android in release configuration assemblies are loaded from the apk directly into memory. // We cannot read assemblies from cwd, so should check loaded assemblies instead. loadFromAppDomain(); @@ -53,8 +45,6 @@ namespace osu.Game.Rulesets var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets"); if (rulesetStorage != null) loadUserRulesets(rulesetStorage); - - addMissingRulesets(); } /// @@ -95,80 +85,7 @@ namespace osu.Game.Rulesets if (domainAssembly != null) return domainAssembly; - return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName); - } - - private void addMissingRulesets() - { - realmAccess.Write(realm => - { - var rulesets = realm.All(); - - List instances = loadedAssemblies.Values - .Select(r => Activator.CreateInstance(r) as Ruleset) - .Where(r => r != null) - .Select(r => r.AsNonNull()) - .ToList(); - - // add all legacy rulesets first to ensure they have exclusive choice of primary key. - foreach (var r in instances.Where(r => r is ILegacyRuleset)) - { - if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null) - realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); - } - - // 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 (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) - { - var existingSameShortName = rulesets.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 - realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); - } - } - - List detachedRulesets = new List(); - - // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage. - foreach (var r in rulesets.OrderBy(r => r.OnlineID)) - { - try - { - var resolvedType = Type.GetType(r.InstantiationInfo) - ?? throw new RulesetLoadException(@"Type could not be resolved"); - - var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo - ?? throw new RulesetLoadException(@"Instantiation failure"); - - // If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution. - // To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw. - resolvedType.Assembly.GetTypes(); - - r.Name = instanceInfo.Name; - r.ShortName = instanceInfo.ShortName; - r.InstantiationInfo = instanceInfo.InstantiationInfo; - r.Available = true; - - detachedRulesets.Add(r.Clone()); - } - catch (Exception ex) - { - r.Available = false; - Logger.Log($"Could not load ruleset {r}: {ex.Message}"); - } - } - - availableRulesets.AddRange(detachedRulesets.OrderBy(r => r)); - }); + return LoadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName); } private void loadFromAppDomain() @@ -214,7 +131,7 @@ namespace osu.Game.Rulesets { string? filename = Path.GetFileNameWithoutExtension(file); - if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename)) + if (LoadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename)) return; try @@ -229,17 +146,17 @@ namespace osu.Game.Rulesets private void addRuleset(Assembly assembly) { - if (loadedAssemblies.ContainsKey(assembly)) + if (LoadedAssemblies.ContainsKey(assembly)) return; // the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799). // as a failsafe, also compare by FullName. - if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName)) + if (LoadedAssemblies.Any(a => a.Key.FullName == assembly.FullName)) return; try { - loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset))); + LoadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset))); } catch (Exception e) { From 0138f22c8d12b81cc6343798c8be1661a45f8524 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 17:13:51 +0900 Subject: [PATCH 105/306] Update existing usages to point to `RealmRulesetStore` --- .../Database/BeatmapImporterTests.cs | 50 +++++++++---------- osu.Game.Tests/Database/RulesetStoreTests.cs | 8 +-- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 2 +- .../Background/TestSceneUserDimBackgrounds.cs | 2 +- .../TestSceneManageCollectionsDialog.cs | 2 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 2 +- .../TestSceneDrawableRoomPlaylist.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 2 +- .../TestSceneMultiplayerMatchSongSelect.cs | 2 +- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../TestSceneMultiplayerPlaylist.cs | 2 +- .../TestSceneMultiplayerQueueList.cs | 2 +- .../TestSceneMultiplayerReadyButton.cs | 2 +- .../TestSceneMultiplayerSpectateButton.cs | 2 +- .../TestScenePlaylistsSongSelect.cs | 2 +- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 2 +- .../TestScenePlaylistsRoomCreation.cs | 2 +- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 2 +- .../SongSelect/TestSceneFilterControl.cs | 2 +- .../SongSelect/TestScenePlaySongSelect.cs | 2 +- .../SongSelect/TestSceneTopLocalRank.cs | 2 +- .../TestSceneDeleteLocalScore.cs | 2 +- 22 files changed, 49 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 2c7d0211a0..9d67381b5a 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using (var importer = new BeatmapModelManager(realm, storage)) - using (new RulesetStore(realm, storage)) + using (new RealmRulesetStore(realm, storage)) { Live? beatmapSet; @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using (var importer = new BeatmapModelManager(realm, storage)) - using (new RulesetStore(realm, storage)) + using (new RealmRulesetStore(realm, storage)) { Live? beatmapSet; @@ -142,7 +142,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using (var importer = new BeatmapModelManager(realm, storage)) - using (new RulesetStore(realm, storage)) + using (new RealmRulesetStore(realm, storage)) { Live? imported; @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); await LoadOszIntoStore(importer, realm.Realm); }); @@ -183,7 +183,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); var imported = await LoadOszIntoStore(importer, realm.Realm); @@ -201,7 +201,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); var imported = await LoadOszIntoStore(importer, realm.Realm); @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); string? tempPath = TestResources.GetTestBeatmapForImport(); @@ -245,7 +245,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); var imported = await LoadOszIntoStore(importer, realm.Realm); var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); @@ -265,7 +265,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -314,7 +314,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -366,7 +366,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -414,7 +414,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -463,7 +463,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); var imported = await LoadOszIntoStore(importer, realm.Realm); @@ -496,7 +496,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); var progressNotification = new ImportProgressNotification(); @@ -532,7 +532,7 @@ namespace osu.Game.Tests.Database }; using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); var imported = await LoadOszIntoStore(importer, realm.Realm); @@ -582,7 +582,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); var imported = await LoadOszIntoStore(importer, realm.Realm); @@ -606,7 +606,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Realm); @@ -638,7 +638,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new NonOptimisedBeatmapImporter(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); var imported = await LoadOszIntoStore(importer, realm.Realm); @@ -662,7 +662,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); var imported = await LoadOszIntoStore(importer, realm.Realm); @@ -688,7 +688,7 @@ namespace osu.Game.Tests.Database RunTestWithRealm((realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); var metadata = new BeatmapMetadata { @@ -734,7 +734,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); using (File.OpenRead(temp)) @@ -751,7 +751,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -787,7 +787,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -829,7 +829,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -880,7 +880,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realm, storage) => { using var importer = new BeatmapModelManager(realm, storage); - using var store = new RulesetStore(realm, storage); + using var store = new RealmRulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); await importer.Import(temp); diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index 7544142b70..f48b5cba11 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, storage) => { - var rulesets = new RulesetStore(realm, storage); + var rulesets = new RealmRulesetStore(realm, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); Assert.AreEqual(4, realm.Realm.All().Count()); @@ -26,8 +26,8 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, storage) => { - var rulesets = new RulesetStore(realm, storage); - var rulesets2 = new RulesetStore(realm, storage); + var rulesets = new RealmRulesetStore(realm, storage); + var rulesets2 = new RealmRulesetStore(realm, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); Assert.AreEqual(4, rulesets2.AvailableRulesets.Count()); @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, storage) => { - var rulesets = new RulesetStore(realm, storage); + var rulesets = new RealmRulesetStore(realm, storage); Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged); Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged); diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 9aa04dda92..c4bffbc0ab 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Online [BackgroundDependencyLoader] private void load(AudioManager audio, GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API)); } diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 40e7c0a844..9f708ace70 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Background [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); Dependencies.Cache(Realm); diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index d4c13059da..e40dd58663 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Collections [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, Audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 2a3b44d619..2c994576f3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 8c10a0d0d9..4dc7dc1c42 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 3f9aec3a42..fc8c3f7f58 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 457b53ae61..381b9b58bd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 7d2ef8276d..1eebe781f1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index f84abc7443..af12046d76 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index bf06b6ad73..397c2d16d1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 609693e54b..6851dba721 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index c4ea78116a..0033a4ca74 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 3333afc88b..f8d62c9840 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 50b3f52047..05b97e333f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 578ea63b4e..a6f1762cb3 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Playlists [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 667fd08084..8c3cc02c83 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.SongSelect { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(rulesetStore = new RulesetStore(Realm)); + dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler)); Dependencies.Cache(Realm); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index b384061531..b7ec128596 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, Audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index d34aff8a23..e2b50e38c2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect { // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. // At a point we have isolated interactive test runs enough, this can likely be removed. - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(Realm); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index 8e5f76a2eb..39680d157b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler)); Dependencies.Cache(Realm); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index da4cf9c6e3..a0a1feff36 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.UserInterface { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(rulesetStore = new RulesetStore(Realm)); + dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, Scheduler)); Dependencies.Cache(Realm); From 13086541f0884a4baec68afd1ee874830d6c366f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 17:12:57 +0900 Subject: [PATCH 106/306] Add static `RulesetStore` to `LegacyBeatmapDecoder` --- osu.Game/Beatmaps/Formats/Decoder.cs | 10 ++++++++++ osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 9 ++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs index 845ac20db0..8eb238a184 100644 --- a/osu.Game/Beatmaps/Formats/Decoder.cs +++ b/osu.Game/Beatmaps/Formats/Decoder.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using osu.Game.IO; +using osu.Game.Rulesets; namespace osu.Game.Beatmaps.Formats { @@ -37,6 +38,15 @@ namespace osu.Game.Beatmaps.Formats LegacyStoryboardDecoder.Register(); } + /// + /// Register dependencies for use with static decoder classes. + /// + /// A store containing all available rulesets (used by ). + public static void RegisterDependencies(RulesetStore rulesets) + { + LegacyBeatmapDecoder.RulesetStore = rulesets; + } + /// /// Retrieves a to parse a . /// diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 07ada8ecc4..fb7882c182 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Data; using System.IO; using System.Linq; using osu.Framework.Extensions; @@ -11,12 +12,15 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.IO; +using osu.Game.Rulesets; using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Beatmaps.Formats { public class LegacyBeatmapDecoder : LegacyDecoder { + protected internal static RulesetStore RulesetStore; + private Beatmap beatmap; private ConvertHitObjectParser parser; @@ -40,6 +44,9 @@ namespace osu.Game.Beatmaps.Formats public LegacyBeatmapDecoder(int version = LATEST_VERSION) : base(version) { + if (RulesetStore == null) + throw new InvalidOperationException($"Call {nameof(Decoder)}.{nameof(RegisterDependencies)} before using {nameof(LegacyBeatmapDecoder)}."); + // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) offset = FormatVersion < 5 ? 24 : 0; } @@ -158,7 +165,7 @@ namespace osu.Game.Beatmaps.Formats case @"Mode": int rulesetID = Parsing.ParseInt(pair.Value); - beatmap.BeatmapInfo.RulesetID = rulesetID; + beatmap.BeatmapInfo.Ruleset = RulesetStore.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally."); switch (rulesetID) { From d0efecfc9cff723d92a7e9289842737152e6dc61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 17:13:25 +0900 Subject: [PATCH 107/306] Add `RulesetStore` for use where realm is not present (ie. other projects) --- osu.Game/Rulesets/AssemblyRulesetStore.cs | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 osu.Game/Rulesets/AssemblyRulesetStore.cs diff --git a/osu.Game/Rulesets/AssemblyRulesetStore.cs b/osu.Game/Rulesets/AssemblyRulesetStore.cs new file mode 100644 index 0000000000..b7378ccf61 --- /dev/null +++ b/osu.Game/Rulesets/AssemblyRulesetStore.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 System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Platform; + +#nullable enable + +namespace osu.Game.Rulesets +{ + public class AssemblyRulesetStore : RulesetStore + { + public override IEnumerable AvailableRulesets => availableRulesets; + + private readonly List availableRulesets = new List(); + + public AssemblyRulesetStore(Storage? storage = null) + : base(storage) + + { + List instances = LoadedAssemblies.Values + .Select(r => Activator.CreateInstance(r) as Ruleset) + .Where(r => r != null) + .Select(r => r.AsNonNull()) + .ToList(); + + // add all legacy rulesets first to ensure they have exclusive choice of primary key. + foreach (var r in instances.Where(r => r is ILegacyRuleset)) + availableRulesets.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); + } + } +} From 5477af08c5d972988df03b7834ff5ff0f039a9cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 17:21:06 +0900 Subject: [PATCH 108/306] Register an `AssemblyRulesetStore` in tests which don't use `OsuGameBase` --- osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs | 3 +++ osu.Game.Rulesets.Osu.Tests/StackingTest.cs | 6 ++++++ .../Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 7 +++++++ osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 7 +++++++ osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs | 7 +++++++ osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs | 3 +++ osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 1 - osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs | 6 ++++++ osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs | 6 ++++++ 9 files changed, 45 insertions(+), 1 deletion(-) diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs index 1d207d04c7..0dd66be0b4 100644 --- a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs +++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Archives; +using osu.Game.Rulesets; using osu.Game.Tests.Resources; namespace osu.Game.Benchmarks @@ -18,6 +19,8 @@ namespace osu.Game.Benchmarks public override void SetUp() { + Decoder.RegisterDependencies(new AssemblyRulesetStore()); + using (var resources = new DllResourceStore(typeof(TestResources).Assembly)) using (var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz")) using (var reader = new ZipArchiveReader(archive)) diff --git a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs index 871afdb09d..927c0c3e1e 100644 --- a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs @@ -18,6 +18,12 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class StackingTest { + [SetUp] + public void SetUp() + { + Decoder.RegisterDependencies(new AssemblyRulesetStore()); + } + [Test] public void TestStacking() { diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 468cb7683c..ad088d298b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.IO; +using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Mods; @@ -29,6 +30,12 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class LegacyBeatmapDecoderTest { + [SetUp] + public void SetUp() + { + Decoder.RegisterDependencies(new AssemblyRulesetStore()); + } + [Test] public void TestDecodeBeatmapVersion() { diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 2eb75259d9..b7989adcb8 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Serialization; +using osu.Game.Rulesets; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; @@ -22,6 +23,12 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class OsuJsonDecoderTest { + [SetUp] + public void SetUp() + { + Decoder.RegisterDependencies(new AssemblyRulesetStore()); + } + private const string normal = "Soleily - Renatus (Gamu) [Insane].osu"; private const string marathon = "Within Temptation - The Unforgiving (Armin) [Marathon].osu"; private const string with_sb = "Kozato snow - Rengetsu Ouka (_Kiva) [Yuki YukI].osu"; diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index 810ea5dbd0..64a52b2b01 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -9,12 +9,19 @@ using osu.Game.Tests.Resources; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Archives; +using osu.Game.Rulesets; namespace osu.Game.Tests.Beatmaps.IO { [TestFixture] public class OszArchiveReaderTest { + [SetUp] + public void SetUp() + { + Decoder.RegisterDependencies(new AssemblyRulesetStore()); + } + [Test] public void TestReadBeatmaps() { diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index 44a908b756..6cf760723a 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -8,6 +8,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; +using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; @@ -28,6 +29,8 @@ namespace osu.Game.Tests.Editing [SetUp] public void Setup() { + Decoder.RegisterDependencies(new AssemblyRulesetStore()); + patcher = new LegacyEditorBeatmapPatcher(current = new EditorBeatmap(new OsuBeatmap { BeatmapInfo = diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index fb7882c182..9626f39ec3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Data; using System.IO; using System.Linq; using osu.Framework.Extensions; diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 8d622955b7..d5b7e3152b 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -34,6 +34,12 @@ namespace osu.Game.Tests.Beatmaps protected abstract string ResourceAssembly { get; } + [SetUp] + public void Setup() + { + Decoder.RegisterDependencies(new AssemblyRulesetStore()); + } + protected void Test(string name, params Type[] mods) { var ourResult = convert(name, mods.Select(m => (Mod)Activator.CreateInstance(m)).ToArray()); diff --git a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs index 9f8811c7f9..78b2558640 100644 --- a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs +++ b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs @@ -22,6 +22,12 @@ namespace osu.Game.Tests.Beatmaps protected abstract string ResourceAssembly { get; } + [SetUp] + public void Setup() + { + Decoder.RegisterDependencies(new AssemblyRulesetStore()); + } + protected void Test(double expected, string name, params Mod[] mods) { // Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences. From 5ffd3ff82acbf76acc91accf666b76ab1255b3e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 17:25:16 +0900 Subject: [PATCH 109/306] Add xmldoc and allow constructing an `AssemblyRulesetStore` with a directory path --- osu.Game/Rulesets/AssemblyRulesetStore.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Rulesets/AssemblyRulesetStore.cs b/osu.Game/Rulesets/AssemblyRulesetStore.cs index b7378ccf61..7313a77aa5 100644 --- a/osu.Game/Rulesets/AssemblyRulesetStore.cs +++ b/osu.Game/Rulesets/AssemblyRulesetStore.cs @@ -11,12 +11,28 @@ using osu.Framework.Platform; namespace osu.Game.Rulesets { + /// + /// A ruleset store that populates from loaded assemblies (and optionally, assemblies in a storage). + /// public class AssemblyRulesetStore : RulesetStore { public override IEnumerable AvailableRulesets => availableRulesets; private readonly List availableRulesets = new List(); + /// + /// Create an assembly ruleset store that populates from loaded assemblies and an external location. + /// + /// An path containing ruleset DLLs. + public AssemblyRulesetStore(string path) + : this(new NativeStorage(path)) + { + } + + /// + /// Create an assembly ruleset store that populates from loaded assemblies and an optional storage source. + /// + /// An optional storage containing ruleset DLLs. public AssemblyRulesetStore(Storage? storage = null) : base(storage) From 23933fc881f07c2cc96b10589531387e6b2658bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 17:32:22 +0900 Subject: [PATCH 110/306] Update xmldoc to mention that multipliers are not applied anywhere --- osu.Game/Rulesets/Mods/Mod.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 5555770822..9a45e2458d 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -80,8 +80,11 @@ namespace osu.Game.Rulesets.Mods } /// - /// The score multiplier of this mod. + /// The (legacy) score multiplier of this mod. /// + /// + /// This is not applied for newly set scores, but may be required for display purposes when showing legacy scores. + /// [JsonIgnore] public virtual double ScoreMultiplier => 1; From 28fcad92810097c4e4fb0cce0757bea2ec43d7b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Feb 2022 17:33:55 +0900 Subject: [PATCH 111/306] Update failing test to not account for no-longer-existing multiplier --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index de795241bf..0b9db8e20a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -147,8 +147,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("player score matching expected bonus score", () => { - // multipled by 2 to nullify the score multiplier. (autoplay mod selected) - double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; + double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value; return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); From a0ee86ddd2761e0e901235939bed9e3f6d4f1334 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 19:50:27 +0900 Subject: [PATCH 112/306] Fix improperly considering rate adjustment mods --- .../Difficulty/ManiaDifficultyAttributes.cs | 4 ++-- .../Difficulty/ManiaDifficultyCalculator.cs | 13 ++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index efd5868504..5b7a460079 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -10,10 +10,10 @@ namespace osu.Game.Rulesets.Mania.Difficulty public class ManiaDifficultyAttributes : DifficultyAttributes { /// - /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). + /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). /// /// - /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. + /// Rate-adjusting mods do not affect the hit window at all in osu-stable. /// [JsonProperty("great_hit_window")] public double GreatHitWindow { get; set; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 1f82eb7ccd..d200daa7fa 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty { StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, - GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate), + GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), ScoreMultiplier = getScoreMultiplier(mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), }; @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty } } - private int getHitWindow300(Mod[] mods) + private double getHitWindow300(Mod[] mods) { if (isForCurrentRuleset) { @@ -121,19 +121,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty return applyModAdjustments(47, mods); - static int applyModAdjustments(double value, Mod[] mods) + static double applyModAdjustments(double value, Mod[] mods) { if (mods.Any(m => m is ManiaModHardRock)) value /= 1.4; else if (mods.Any(m => m is ManiaModEasy)) value *= 1.4; - if (mods.Any(m => m is ManiaModDoubleTime)) - value *= 1.5; - else if (mods.Any(m => m is ManiaModHalfTime)) - value *= 0.75; - - return (int)value; + return value; } } From 0b37efc9850f57a6ddf456b0e68fc879a536a2e0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 20:07:26 +0900 Subject: [PATCH 113/306] Add explanatory note --- osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index d200daa7fa..b17aa7fc4d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty { StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, + // In osu-stable mania, rate-adjustment mods don't affect the hit window. + // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), ScoreMultiplier = getScoreMultiplier(mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), From c124034cf3f671cc44b950851bb6a2f3900524cc Mon Sep 17 00:00:00 2001 From: dekrain Date: Wed, 16 Feb 2022 23:18:14 +0100 Subject: [PATCH 114/306] Add text displaying recent score time --- .../Online/Leaderboards/LeaderboardScore.cs | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index c2393a5de5..f77982535d 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.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; @@ -29,6 +30,7 @@ using osuTK; using osuTK.Graphics; using osu.Game.Online.API; using osu.Game.Utils; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Online.Leaderboards { @@ -42,6 +44,7 @@ namespace osu.Game.Online.Leaderboards private const float edge_margin = 5; private const float background_alpha = 0.25f; private const float rank_width = 35; + private const float date_width = 35; protected Container RankContainer { get; private set; } @@ -100,10 +103,18 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Y, Width = rank_width, }, + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = date_width, + Child = new DateLabel(Score.Date), + }, content = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = rank_width, }, + Padding = new MarginPadding { Left = rank_width, Right = date_width, }, Children = new Drawable[] { new Container @@ -377,6 +388,36 @@ namespace osu.Game.Online.Leaderboards public LocalisableString TooltipText { get; } } + private class DateLabel : DrawableDate + { + public DateLabel(DateTimeOffset date) + : base(date, 20) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + protected override string Format() + { + var now = DateTime.Now; + var difference = now - Date; + + // TODO(optional): support localisation (probably via `CommonStrings.CountHours()` etc.) + // requires pluralisable string support framework-side + + if (difference.TotalMinutes < 1) + return CommonStrings.TimeNow.ToString(); + if (difference.TotalHours < 1) + return $@"{Math.Ceiling(difference.TotalMinutes)}min"; + if (difference.TotalDays < 1) + return $@"{Math.Ceiling(difference.TotalHours)}h"; + if (difference.TotalDays < 7) + return $@"{Math.Ceiling(difference.TotalDays)}d"; + + return string.Empty; + } + } + public class LeaderboardScoreStatistic { public IconUsage Icon; From 333a305af3dcabba5591b17140be49ba4c5c14f3 Mon Sep 17 00:00:00 2001 From: dekrain Date: Thu, 17 Feb 2022 00:09:17 +0100 Subject: [PATCH 115/306] Use floor instead of ceiling --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index f77982535d..9ac1ab8075 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -408,11 +408,11 @@ namespace osu.Game.Online.Leaderboards if (difference.TotalMinutes < 1) return CommonStrings.TimeNow.ToString(); if (difference.TotalHours < 1) - return $@"{Math.Ceiling(difference.TotalMinutes)}min"; + return $@"{Math.Floor(difference.TotalMinutes)}min"; if (difference.TotalDays < 1) - return $@"{Math.Ceiling(difference.TotalHours)}h"; + return $@"{Math.Floor(difference.TotalHours)}h"; if (difference.TotalDays < 7) - return $@"{Math.Ceiling(difference.TotalDays)}d"; + return $@"{Math.Floor(difference.TotalDays)}d"; return string.Empty; } From cb9ffc655acf4509ce174c770788b9bfeb715339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Feb 2022 22:32:42 +0100 Subject: [PATCH 116/306] Add tests showing expected behaviour of naming helper --- osu.Game.Tests/Utils/NamingUtilsTest.cs | 132 ++++++++++++++++++++++++ osu.Game/Utils/NamingUtils.cs | 15 +++ 2 files changed, 147 insertions(+) create mode 100644 osu.Game.Tests/Utils/NamingUtilsTest.cs create mode 100644 osu.Game/Utils/NamingUtils.cs diff --git a/osu.Game.Tests/Utils/NamingUtilsTest.cs b/osu.Game.Tests/Utils/NamingUtilsTest.cs new file mode 100644 index 0000000000..62e688db90 --- /dev/null +++ b/osu.Game.Tests/Utils/NamingUtilsTest.cs @@ -0,0 +1,132 @@ +// 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.Utils; + +namespace osu.Game.Tests.Utils +{ + [TestFixture] + public class NamingUtilsTest + { + [Test] + public void TestEmptySet() + { + string nextBestName = NamingUtils.GetNextBestName(Enumerable.Empty(), "New Difficulty"); + + Assert.AreEqual("New Difficulty", nextBestName); + } + + [Test] + public void TestNotTaken() + { + string[] existingNames = + { + "Something", + "Entirely", + "Different" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty", nextBestName); + } + + [Test] + public void TestNotTakenButClose() + { + string[] existingNames = + { + "New Difficulty(1)", + "New Difficulty (abcd)", + "New Difficulty but not really" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty", nextBestName); + } + + [Test] + public void TestAlreadyTaken() + { + string[] existingNames = + { + "New Difficulty" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty (1)", nextBestName); + } + + [Test] + public void TestAlreadyTakenWithDifferentCase() + { + string[] existingNames = + { + "new difficulty" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty (1)", nextBestName); + } + + [Test] + public void TestAlreadyTakenWithBrackets() + { + string[] existingNames = + { + "new difficulty (copy)" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty (copy)"); + + Assert.AreEqual("New Difficulty (copy) (1)", nextBestName); + } + + [Test] + public void TestMultipleAlreadyTaken() + { + string[] existingNames = + { + "New Difficulty", + "New difficulty (1)", + "new Difficulty (2)", + "New DIFFICULTY (3)" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty (4)", nextBestName); + } + + [Test] + public void TestEvenMoreAlreadyTaken() + { + string[] existingNames = Enumerable.Range(1, 30).Select(i => $"New Difficulty ({i})").Append("New Difficulty").ToArray(); + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty (31)", nextBestName); + } + + [Test] + public void TestMultipleAlreadyTakenWithGaps() + { + string[] existingNames = + { + "New Difficulty", + "New Difficulty (1)", + "New Difficulty (4)", + "New Difficulty (9)" + }; + + string nextBestName = NamingUtils.GetNextBestName(existingNames, "New Difficulty"); + + Assert.AreEqual("New Difficulty (2)", nextBestName); + } + } +} diff --git a/osu.Game/Utils/NamingUtils.cs b/osu.Game/Utils/NamingUtils.cs new file mode 100644 index 0000000000..1b48f57932 --- /dev/null +++ b/osu.Game/Utils/NamingUtils.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 System.Collections.Generic; + +namespace osu.Game.Utils +{ + public static class NamingUtils + { + public static string GetNextBestName(IEnumerable existingNames, string desiredName) + { + return null; + } + } +} From e09570c31bf18af340d529fc59448b1b694db270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Feb 2022 23:00:45 +0100 Subject: [PATCH 117/306] Implement best-name-finding helper method --- osu.Game/Utils/NamingUtils.cs | 48 ++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/osu.Game/Utils/NamingUtils.cs b/osu.Game/Utils/NamingUtils.cs index 1b48f57932..482e3d0954 100644 --- a/osu.Game/Utils/NamingUtils.cs +++ b/osu.Game/Utils/NamingUtils.cs @@ -2,14 +2,60 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Text.RegularExpressions; namespace osu.Game.Utils { public static class NamingUtils { + /// + /// Given a set of and a target , + /// finds a "best" name closest to that is not in . + /// + /// + /// + /// This helper is most useful in scenarios when creating new objects in a set + /// (such as adding new difficulties to a beatmap set, or creating a clone of an existing object that needs a unique name). + /// If is already present in , + /// this method will append the lowest possible number in brackets that doesn't conflict with + /// to and return that. + /// See osu.Game.Tests.Utils.NamingUtilsTest for concrete examples of behaviour. + /// + /// + /// and are compared in a case-insensitive manner, + /// so this method is safe to use for naming files in a platform-invariant manner. + /// + /// public static string GetNextBestName(IEnumerable existingNames, string desiredName) { - return null; + string pattern = $@"^(?i){Regex.Escape(desiredName)}(?-i)( \((?[1-9][0-9]*)\))?$"; + var regex = new Regex(pattern, RegexOptions.Compiled); + var takenNumbers = new HashSet(); + + foreach (string name in existingNames) + { + var match = regex.Match(name); + if (!match.Success) + continue; + + string copyNumberString = match.Groups[@"copyNumber"].Value; + + if (string.IsNullOrEmpty(copyNumberString)) + { + takenNumbers.Add(0); + continue; + } + + takenNumbers.Add(int.Parse(copyNumberString)); + } + + int bestNumber = 0; + while (takenNumbers.Contains(bestNumber)) + bestNumber += 1; + + return bestNumber == 0 + ? desiredName + : $"{desiredName} ({bestNumber})"; } } } From 8a08bb7aaf88ec33c64c01659cd0ea9c42f93f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Feb 2022 23:12:13 +0100 Subject: [PATCH 118/306] Use best-name-finding helper in new difficulty creation flow --- osu.Game/Beatmaps/BeatmapManager.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 777d5db2ad..5f7de0d762 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -22,6 +22,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Stores; +using osu.Game.Utils; #nullable enable @@ -123,7 +124,10 @@ namespace osu.Game.Beatmaps { var playableBeatmap = referenceWorkingBeatmap.GetPlayableBeatmap(rulesetInfo); - var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), playableBeatmap.Metadata.DeepClone()); + var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), playableBeatmap.Metadata.DeepClone()) + { + DifficultyName = NamingUtils.GetNextBestName(targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), "New Difficulty") + }; var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; foreach (var timingPoint in playableBeatmap.ControlPointInfo.TimingPoints) newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); @@ -150,8 +154,10 @@ namespace osu.Game.Beatmaps newBeatmap.BeatmapInfo = newBeatmapInfo = referenceWorkingBeatmap.BeatmapInfo.Clone(); // assign a new ID to the clone. newBeatmapInfo.ID = Guid.NewGuid(); - // add "(copy)" suffix to difficulty name to avoid clashes on save. - newBeatmapInfo.DifficultyName += " (copy)"; + // add "(copy)" suffix to difficulty name, and additionally ensure that it doesn't conflict with any other potentially pre-existing copies. + newBeatmapInfo.DifficultyName = NamingUtils.GetNextBestName( + targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), + $"{newBeatmapInfo.DifficultyName} (copy)"); // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps. newBeatmapInfo.Hash = string.Empty; // clear online properties. From e459523afe5cd351e98a8558f41ca933ef3374b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Feb 2022 23:13:43 +0100 Subject: [PATCH 119/306] Adjust beatmap creation test cases to new behaviour --- .../Editing/TestSceneEditorBeatmapCreation.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 0a2f622da1..ecd4035edd 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -269,11 +269,12 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestCreateNewBeatmapFailsWithBlankNamedDifficulties() + public void TestCreateMultipleNewDifficultiesSucceeds() { Guid setId = Guid.Empty; AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = "New Difficulty"); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => { @@ -282,15 +283,24 @@ namespace osu.Game.Tests.Visual.Editing }); AddStep("try to create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); - AddAssert("beatmap set unchanged", () => + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction()); + + AddUntilStep("wait for created", () => + { + string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != "New Difficulty"; + }); + AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)"); + AddAssert("new difficulty persisted", () => { var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); - return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Files.Count == 2); }); } [Test] - public void TestCreateNewBeatmapFailsWithSameNamedDifficulties([Values] bool sameRuleset) + public void TestSavingBeatmapFailsWithSameNamedDifficulties([Values] bool sameRuleset) { Guid setId = Guid.Empty; const string duplicate_difficulty_name = "duplicate"; From aac1c53b060cc1885297baa3fc5709b6a5749097 Mon Sep 17 00:00:00 2001 From: Stedoss <29103029+Stedoss@users.noreply.github.com> Date: Thu, 17 Feb 2022 03:04:16 +0000 Subject: [PATCH 120/306] Remove creator name from playlist item panel beatmap text --- .../Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index dcf2a5a480..7533bfc8b6 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -291,10 +291,14 @@ namespace osu.Game.Screens.OnlinePlay if (Item.Beatmap.Value != null) { - beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineID.ToString(), null, text => - { - text.Truncate = true; - }); + beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(includeCreator: false), + LinkAction.OpenBeatmap, + Item.Beatmap.Value.OnlineID.ToString(), + null, + text => + { + text.Truncate = true; + }); } authorText.Clear(); From 7307e68e9c55bf1be4bde2e94d912e6a9df24f63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Feb 2022 13:26:12 +0900 Subject: [PATCH 121/306] Revert "Merge pull request #16889 from smoogipoo/remove-mod-multiplier" This reverts commit 252b945d3b0cd4e6f33ac334f23bcf119a119c2a, reversing changes made to a1b39a96cfe3f4d0f02652ded5ebca09ba1d24e1. --- .../Difficulty/ManiaPerformanceCalculator.cs | 16 +++++------ .../TestSceneSpinnerRotation.cs | 3 ++- .../TestSceneFooterButtonMods.cs | 27 +++++++++++++++++-- osu.Game/Rulesets/Mods/Mod.cs | 7 ++--- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 13 ++++++++- osu.Game/Screens/Select/FooterButtonMods.cs | 25 +++++++++++++++++ 6 files changed, 74 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index beba29b8bb..8a8c41bb8a 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -43,11 +43,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - if (Attributes.ScoreMultiplier > 0) - { - // Scale score up, so it's comparable to other keymods - scaledScore *= 1.0 / Attributes.ScoreMultiplier; - } + IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); + + double scoreMultiplier = 1.0; + foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) + scoreMultiplier *= m.ScoreMultiplier; + + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / scoreMultiplier; // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // The specific number has no intrinsic meaning and can be adjusted as needed. @@ -77,9 +80,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeDifficultyValue() { - if (Attributes.ScoreMultiplier <= 0) - return 0; - double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 0b9db8e20a..de795241bf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -147,7 +147,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("player score matching expected bonus score", () => { - double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value; + // multipled by 2 to nullify the score multiplier. (autoplay mod selected) + double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index e9014c0941..0631059d1a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; +using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select; @@ -12,11 +14,11 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneFooterButtonMods : OsuTestScene { - private readonly FooterButtonMods footerButtonMods; + private readonly TestFooterButtonMods footerButtonMods; public TestSceneFooterButtonMods() { - Add(footerButtonMods = new FooterButtonMods()); + Add(footerButtonMods = new TestFooterButtonMods()); } [Test] @@ -24,15 +26,19 @@ namespace osu.Game.Tests.Visual.UserInterface { var hiddenMod = new Mod[] { new OsuModHidden() }; AddStep(@"Add Hidden", () => changeMods(hiddenMod)); + AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod)); var hardRockMod = new Mod[] { new OsuModHardRock() }; AddStep(@"Add HardRock", () => changeMods(hardRockMod)); + AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod)); var doubleTimeMod = new Mod[] { new OsuModDoubleTime() }; AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); + AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod)); var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods)); } [Test] @@ -40,12 +46,15 @@ namespace osu.Game.Tests.Visual.UserInterface { var easyMod = new Mod[] { new OsuModEasy() }; AddStep(@"Add Easy", () => changeMods(easyMod)); + AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod)); var noFailMod = new Mod[] { new OsuModNoFail() }; AddStep(@"Add NoFail", () => changeMods(noFailMod)); + AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod)); var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() }; AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods)); } [Test] @@ -54,11 +63,25 @@ namespace osu.Game.Tests.Visual.UserInterface var multipleMods = new Mod[] { new OsuModDoubleTime(), new OsuModFlashlight() }; AddStep(@"Add mods", () => changeMods(multipleMods)); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); + AddAssert(@"Check empty multiplier", () => assertModsMultiplier(Array.Empty())); } private void changeMods(IReadOnlyList mods) { footerButtonMods.Current.Value = mods; } + + private bool assertModsMultiplier(IEnumerable mods) + { + double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + string expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + + return expectedValue == footerButtonMods.MultiplierText.Current.Value; + } + + private class TestFooterButtonMods : FooterButtonMods + { + public new OsuSpriteText MultiplierText => base.MultiplierText; + } } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 9a45e2458d..7136795461 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -80,13 +80,10 @@ namespace osu.Game.Rulesets.Mods } /// - /// The (legacy) score multiplier of this mod. + /// The score multiplier of this mod. /// - /// - /// This is not applied for newly set scores, but may be required for display purposes when showing legacy scores. - /// [JsonIgnore] - public virtual double ScoreMultiplier => 1; + public abstract double ScoreMultiplier { get; } /// /// Returns true if this mod is implemented (and playable). diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index ce1486b02f..79861c0ecc 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -92,6 +92,8 @@ namespace osu.Game.Rulesets.Scoring private readonly List hitEvents = new List(); private HitObject lastHitObject; + private double scoreMultiplier = 1; + public ScoreProcessor() { accuracyPortion = DefaultAccuracyPortion; @@ -109,6 +111,15 @@ namespace osu.Game.Rulesets.Scoring }; Mode.ValueChanged += _ => updateScore(); + Mods.ValueChanged += mods => + { + scoreMultiplier = 1; + + foreach (var m in mods.NewValue) + scoreMultiplier *= m.ScoreMultiplier; + + updateScore(); + }; } private readonly Dictionary scoreResultCounts = new Dictionary(); @@ -224,7 +235,7 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Standardised: double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)); + return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; case ScoringMode.Classic: // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 1f1aa4c4b3..5bbca5ca1a 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -6,11 +6,14 @@ using osu.Framework.Graphics; using osu.Game.Screens.Play.HUD; using osu.Game.Rulesets.Mods; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osuTK; +using osuTK.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Screens.Select @@ -23,7 +26,10 @@ namespace osu.Game.Screens.Select set => modDisplay.Current = value; } + protected readonly OsuSpriteText MultiplierText; private readonly ModDisplay modDisplay; + private Color4 lowMultiplierColour; + private Color4 highMultiplierColour; public FooterButtonMods() { @@ -34,6 +40,12 @@ namespace osu.Game.Screens.Select Scale = new Vector2(0.8f), ExpansionMode = ExpansionMode.AlwaysContracted, }); + ButtonContentContainer.Add(MultiplierText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + }); } [BackgroundDependencyLoader] @@ -41,6 +53,8 @@ namespace osu.Game.Screens.Select { SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); + lowMultiplierColour = colours.Red; + highMultiplierColour = colours.Green; Text = @"mods"; Hotkey = GlobalAction.ToggleModSelection; } @@ -54,6 +68,17 @@ namespace osu.Game.Screens.Select private void updateMultiplierText() { + double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; + + MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + + if (multiplier > 1.0) + MultiplierText.FadeColour(highMultiplierColour, 200); + else if (multiplier < 1.0) + MultiplierText.FadeColour(lowMultiplierColour, 200); + else + MultiplierText.FadeColour(Color4.White, 200); + if (Current.Value?.Count > 0) modDisplay.FadeIn(); else From 39a7bbdb9ad0cd23ccffc45b0760b228a078393d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Feb 2022 14:03:38 +0900 Subject: [PATCH 122/306] Fix mania PP calculator applying incorrect score multiplier --- .../Difficulty/ManiaPerformanceCalculator.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 8a8c41bb8a..722cb55036 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -43,14 +43,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); - - double scoreMultiplier = 1.0; - foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) - scoreMultiplier *= m.ScoreMultiplier; - - // Scale score up, so it's comparable to other keymods - scaledScore *= 1.0 / scoreMultiplier; + if (Attributes.ScoreMultiplier > 0) + { + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / Attributes.ScoreMultiplier; + } // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // The specific number has no intrinsic meaning and can be adjusted as needed. From dc74d17478605d1fbb03b434623a49a2f72970b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Feb 2022 16:45:20 +0900 Subject: [PATCH 123/306] 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 eab2be1b72..24a0d20874 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 7b50c804ff..4c6f81defa 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 03a105673c..99b9de3fe2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From 7bd731ae08210e58d2101aa3afef7807e15e4fa0 Mon Sep 17 00:00:00 2001 From: dekrain Date: Thu, 17 Feb 2022 10:12:35 +0100 Subject: [PATCH 124/306] Move the date next to the flag icon --- .../Online/Leaderboards/LeaderboardScore.cs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 9ac1ab8075..a13d8eeffd 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -44,7 +44,6 @@ namespace osu.Game.Online.Leaderboards private const float edge_margin = 5; private const float background_alpha = 0.25f; private const float rank_width = 35; - private const float date_width = 35; protected Container RankContainer { get; private set; } @@ -59,7 +58,7 @@ namespace osu.Game.Online.Leaderboards public GlowingSpriteText ScoreText { get; private set; } - private Container flagBadgeContainer; + private FillFlowContainer flagBadgeAndDateContainer; private FillFlowContainer modsContainer; private List statisticsLabels; @@ -103,18 +102,10 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Y, Width = rank_width, }, - new Container - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = date_width, - Child = new DateLabel(Score.Date), - }, content = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = rank_width, Right = date_width, }, + Padding = new MarginPadding { Left = rank_width }, Children = new Drawable[] { new Container @@ -176,10 +167,12 @@ namespace osu.Game.Online.Leaderboards Spacing = new Vector2(10f, 0f), Children = new Drawable[] { - flagBadgeContainer = new Container + flagBadgeAndDateContainer = new FillFlowContainer { Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5f, 0f), Size = new Vector2(87f, 20f), Masking = true, Children = new Drawable[] @@ -189,6 +182,10 @@ namespace osu.Game.Online.Leaderboards Width = 30, RelativeSizeAxes = Axes.Y, }, + new DateLabel(Score.Date) + { + RelativeSizeAxes = Axes.Y, + }, }, }, new FillFlowContainer @@ -254,7 +251,7 @@ namespace osu.Game.Online.Leaderboards public override void Show() { - foreach (var d in new[] { avatar, nameLabel, ScoreText, scoreRank, flagBadgeContainer, modsContainer }.Concat(statisticsLabels)) + foreach (var d in new[] { avatar, nameLabel, ScoreText, scoreRank, flagBadgeAndDateContainer, modsContainer }.Concat(statisticsLabels)) d.FadeOut(); Alpha = 0; @@ -281,7 +278,7 @@ namespace osu.Game.Online.Leaderboards using (BeginDelayedSequence(50)) { - var drawables = new Drawable[] { flagBadgeContainer, modsContainer }.Concat(statisticsLabels).ToArray(); + var drawables = new Drawable[] { flagBadgeAndDateContainer, modsContainer }.Concat(statisticsLabels).ToArray(); for (int i = 0; i < drawables.Length; i++) drawables[i].FadeIn(100 + i * 50); } @@ -391,10 +388,9 @@ namespace osu.Game.Online.Leaderboards private class DateLabel : DrawableDate { public DateLabel(DateTimeOffset date) - : base(date, 20) + : base(date) { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, italics: true); } protected override string Format() @@ -411,7 +407,7 @@ namespace osu.Game.Online.Leaderboards return $@"{Math.Floor(difference.TotalMinutes)}min"; if (difference.TotalDays < 1) return $@"{Math.Floor(difference.TotalHours)}h"; - if (difference.TotalDays < 7) + if (difference.TotalDays < 3) return $@"{Math.Floor(difference.TotalDays)}d"; return string.Empty; From f4d1e6f600343fda0ca9446297e9f8d0e5073ce6 Mon Sep 17 00:00:00 2001 From: dekrain Date: Thu, 17 Feb 2022 10:38:29 +0100 Subject: [PATCH 125/306] Add tests for timerefs --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 8 ++++++++ osu.Game/Online/Leaderboards/LeaderboardScore.cs | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 667fd08084..2f44633f4d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -197,6 +197,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, + Date = DateTime.Now, Mods = new Mod[] { new OsuModHidden(), @@ -234,6 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, + Date = DateTime.Now.AddSeconds(-30), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -254,6 +256,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, + Date = DateTime.Now.AddSeconds(-70), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -275,6 +278,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, + Date = DateTime.Now.AddMinutes(-40), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -296,6 +300,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, + Date = DateTime.Now.AddHours(-2), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -317,6 +322,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.9826, MaxCombo = 244, TotalScore = 1707827, + Date = DateTime.Now.AddHours(-25), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -338,6 +344,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.9654, MaxCombo = 244, TotalScore = 1707827, + Date = DateTime.Now.AddHours(-50), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -359,6 +366,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.6025, MaxCombo = 244, TotalScore = 1707827, + Date = DateTime.Now.AddHours(-72), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index a13d8eeffd..8cfba68ea6 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -401,8 +401,10 @@ namespace osu.Game.Online.Leaderboards // TODO(optional): support localisation (probably via `CommonStrings.CountHours()` etc.) // requires pluralisable string support framework-side - if (difference.TotalMinutes < 1) + if (difference.TotalSeconds < 10) return CommonStrings.TimeNow.ToString(); + if (difference.TotalMinutes < 1) + return $@"{Math.Floor(difference.TotalSeconds)}s"; if (difference.TotalHours < 1) return $@"{Math.Floor(difference.TotalMinutes)}min"; if (difference.TotalDays < 1) From 3d5ed24e206a43990f36880464917045e8cdf2be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Feb 2022 21:04:59 +0900 Subject: [PATCH 126/306] Fix beatmap overlay leaderboards and links not working Completely aware that this isn't how it should be done, but would like to get this out in a hotfix release today. Maybe changes opinions on https://github.com/ppy/osu/pull/16890 structure? --- .../API/Requests/Responses/APIBeatmap.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index dca60e54cb..c53fab48ae 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -112,7 +112,27 @@ namespace osu.Game.Online.API.Requests.Responses public int OnlineID { get; set; } = -1; public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})"; - public string ShortName => nameof(APIRuleset); + + public string ShortName + { + get + { + // TODO: this should really not exist. + switch (OnlineID) + { + case 0: return "osu"; + + case 1: return "taiko"; + + case 2: return "catch"; + + case 3: return "fruits"; + + default: throw new ArgumentOutOfRangeException(); + } + } + } + public string InstantiationInfo => string.Empty; public Ruleset CreateInstance() => throw new NotImplementedException(); From 9d0023c750a85c2cc78a6d6d5004f50231916a92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Feb 2022 21:12:51 +0900 Subject: [PATCH 127/306] Fix incorrect mappings --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index c53fab48ae..f5795141c5 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -124,9 +124,9 @@ namespace osu.Game.Online.API.Requests.Responses case 1: return "taiko"; - case 2: return "catch"; + case 2: return "fruits"; - case 3: return "fruits"; + case 3: return "mania"; default: throw new ArgumentOutOfRangeException(); } From 08317b4265c94a6f6b30a6629d002175a7945309 Mon Sep 17 00:00:00 2001 From: OctopuSSX Date: Thu, 17 Feb 2022 20:43:36 +0300 Subject: [PATCH 128/306] Update ScreenshotManager.cs --- osu.Game/Graphics/ScreenshotManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index a39d7bfb47..b0f20de685 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -112,6 +112,8 @@ namespace osu.Game.Graphics if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false) cursorVisibility.Value = true; + host.GetClipboard()?.SetImage(image); + string filename = getFilename(); if (filename == null) return; From 1abbb9ab39675f4caeba0b9700aa9fb4fd9ca0be Mon Sep 17 00:00:00 2001 From: dekrain Date: Thu, 17 Feb 2022 21:26:59 +0100 Subject: [PATCH 129/306] Align the bar to be on baseline of score components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Online/Leaderboards/LeaderboardScore.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 8cfba68ea6..4f7670f098 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -160,8 +160,8 @@ namespace osu.Game.Online.Leaderboards }, new FillFlowContainer { - Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0f), @@ -169,29 +169,32 @@ namespace osu.Game.Online.Leaderboards { flagBadgeAndDateContainer = new FillFlowContainer { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(5f, 0f), - Size = new Vector2(87f, 20f), + Width = 87f, Masking = true, Children = new Drawable[] { new UpdateableFlag(user.Country) { - Width = 30, - RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30f, 20f), }, new DateLabel(Score.Date) { - RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }, }, }, new FillFlowContainer { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Margin = new MarginPadding { Left = edge_margin }, From 98aaf83177ee1cb8777619936e31926b2167b399 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Feb 2022 15:57:37 +0900 Subject: [PATCH 130/306] Add a centralised constant for the osu URL schema protocol --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 12 ++++++------ osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 4 ++-- osu.Game/Online/Chat/MessageFormatter.cs | 4 ++-- osu.Game/OsuGameBase.cs | 2 ++ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 8def8005f1..cea4d510c1 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -409,26 +409,26 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(result.Content, result.DisplayContent); Assert.AreEqual(2, result.Links.Count); - Assert.AreEqual("osu://chan/#english", result.Links[0].Url); - Assert.AreEqual("osu://chan/#japanese", result.Links[1].Url); + Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#english", result.Links[0].Url); + Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#japanese", result.Links[1].Url); } [Test] public void TestOsuProtocol() { - Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a custom protocol osu://chan/#english." }); + Message result = MessageFormatter.FormatMessage(new Message { Content = $"This is a custom protocol {OsuGameBase.OSU_PROTOCOL}chan/#english." }); Assert.AreEqual(result.Content, result.DisplayContent); Assert.AreEqual(1, result.Links.Count); - Assert.AreEqual("osu://chan/#english", result.Links[0].Url); + Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#english", result.Links[0].Url); Assert.AreEqual(26, result.Links[0].Index); Assert.AreEqual(19, result.Links[0].Length); - result = MessageFormatter.FormatMessage(new Message { Content = "This is a [custom protocol](osu://chan/#english)." }); + result = MessageFormatter.FormatMessage(new Message { Content = $"This is a [custom protocol]({OsuGameBase.OSU_PROTOCOL}chan/#english)." }); Assert.AreEqual("This is a custom protocol.", result.DisplayContent); Assert.AreEqual(1, result.Links.Count); - Assert.AreEqual("osu://chan/#english", result.Links[0].Url); + Assert.AreEqual($"{OsuGameBase.OSU_PROTOCOL}chan/#english", result.Links[0].Url); Assert.AreEqual("#english", result.Links[0].Argument); Assert.AreEqual(10, result.Links[0].Index); Assert.AreEqual(15, result.Links[0].Length); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 12b5f64559..d077868175 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -87,8 +87,8 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External); addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch); - addMessageWithChecks("Join my [#english](osu://chan/#english).", 1, expectedActions: LinkAction.OpenChannel); - addMessageWithChecks("Join my osu://chan/#english.", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel); addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel }); addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel); diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index d7974004b1..4c477d58b6 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -269,10 +269,10 @@ namespace osu.Game.Online.Chat handleAdvanced(advanced_link_regex, result, startIndex); // handle editor times - handleMatches(time_regex, "{0}", "osu://edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp); + handleMatches(time_regex, "{0}", $"{OsuGameBase.OSU_PROTOCOL}edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp); // handle channels - handleMatches(channel_regex, "{0}", "osu://chan/{0}", result, startIndex, LinkAction.OpenChannel); + handleMatches(channel_regex, "{0}", $"{OsuGameBase.OSU_PROTOCOL}chan/{0}", result, startIndex, LinkAction.OpenChannel); string empty = ""; while (space-- > 0) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0b2644d5ba..86390e7630 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -52,6 +52,8 @@ namespace osu.Game /// public partial class OsuGameBase : Framework.Game, ICanAcceptFiles { + public const string OSU_PROTOCOL = "osu://"; + public const string CLIENT_STREAM_NAME = @"lazer"; public const int SAMPLE_CONCURRENCY = 6; From 29c5683ba3ce2e7de362cac68949b3a46424cd12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Feb 2022 16:06:38 +0900 Subject: [PATCH 131/306] Add handling of beatmap links on startup --- .../TestSceneStartupBeatmapDisplay.cs | 22 ++++++++++++ .../TestSceneStartupBeatmapSetDisplay.cs | 22 ++++++++++++ osu.Game/OsuGame.cs | 36 ++++++++++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs create mode 100644 osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs new file mode 100644 index 0000000000..1efa24435e --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs @@ -0,0 +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.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneStartupBeatmapDisplay : OsuGameTestScene + { + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { "osu://b/75" }); + + [Test] + public void TestBeatmapLink() + { + AddUntilStep("Beatmap overlay displayed", () => Game.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + } + } +} diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs new file mode 100644 index 0000000000..1339c514e4 --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs @@ -0,0 +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.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneStartupBeatmapSetDisplay : OsuGameTestScene + { + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { "osu://s/1" }); + + [Test] + public void TestBeatmapSetLink() + { + AddUntilStep("Beatmap overlay displayed", () => Game.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5b58dec0c3..390e96d768 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -150,6 +150,7 @@ namespace osu.Game protected SettingsOverlay Settings; private VolumeOverlay volume; + private OsuLogo osuLogo; private MainMenu menuScreen; @@ -898,8 +899,41 @@ namespace osu.Game if (args?.Length > 0) { string[] paths = args.Where(a => !a.StartsWith('-')).ToArray(); + if (paths.Length > 0) - Task.Run(() => Import(paths)); + { + string firstPath = paths.First(); + + if (firstPath.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) + { + handleOsuProtocolUrl(firstPath); + } + else + { + Task.Run(() => Import(paths)); + } + } + } + } + + private void handleOsuProtocolUrl(string url) + { + if (!url.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) + throw new ArgumentException("Invalid osu URL provided.", nameof(url)); + + string[] pieces = url.Split('/'); + + switch (pieces[2]) + { + case "s": + if (int.TryParse(pieces[3], out int beatmapSetId)) + ShowBeatmapSet(beatmapSetId); + break; + + case "b": + if (int.TryParse(pieces[3], out int beatmapId)) + ShowBeatmap(beatmapId); + break; } } From e49da2948d15d4d175d3de7ca7e47656e85e41b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Feb 2022 16:24:18 +0900 Subject: [PATCH 132/306] Fix storyboard background replacement logic not working for beatmaps with multiple backgrounds In the case where the background image of individual difficulties is different, querying the beatmap *set*'s metadata as we were will cause issues. I haven't added test coverage for this but can if required. Can be manually tested using https://osu.ppy.sh/beatmapsets/1595773#osu/3377474 (specifically the highest difficulty). Closes https://github.com/ppy/osu/discussions/16873. --- osu.Game/Storyboards/Storyboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index b86deeab89..c4864c0334 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -78,7 +78,7 @@ namespace osu.Game.Storyboards { get { - string backgroundPath = BeatmapInfo.BeatmapSet?.Metadata.BackgroundFile; + string backgroundPath = BeatmapInfo.Metadata.BackgroundFile; if (string.IsNullOrEmpty(backgroundPath)) return false; From 420e2c538f01b1a1f387b7401a39d828698f83aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Feb 2022 16:48:30 +0900 Subject: [PATCH 133/306] Automatically use an `AssemblyRulesetStore` if no custom store is registered --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 9626f39ec3..9dd2f27be5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using osu.Framework.Extensions; using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Logging; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; @@ -44,7 +45,10 @@ namespace osu.Game.Beatmaps.Formats : base(version) { if (RulesetStore == null) - throw new InvalidOperationException($"Call {nameof(Decoder)}.{nameof(RegisterDependencies)} before using {nameof(LegacyBeatmapDecoder)}."); + { + Logger.Log($"A {nameof(RulesetStore)} was not provided via {nameof(Decoder)}.{nameof(RegisterDependencies)}; falling back to default {nameof(AssemblyRulesetStore)}."); + RulesetStore = new AssemblyRulesetStore(); + } // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) offset = FormatVersion < 5 ? 24 : 0; From cf1dd1ebd3fd4b849ed5249017363949f2112ea7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Feb 2022 16:48:42 +0900 Subject: [PATCH 134/306] Disallow registering a null `RulesetStore` --- osu.Game/Beatmaps/Formats/Decoder.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs index 8eb238a184..c1537d7240 100644 --- a/osu.Game/Beatmaps/Formats/Decoder.cs +++ b/osu.Game/Beatmaps/Formats/Decoder.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using JetBrains.Annotations; using osu.Game.IO; using osu.Game.Rulesets; @@ -42,9 +43,9 @@ namespace osu.Game.Beatmaps.Formats /// Register dependencies for use with static decoder classes. /// /// A store containing all available rulesets (used by ). - public static void RegisterDependencies(RulesetStore rulesets) + public static void RegisterDependencies([NotNull] RulesetStore rulesets) { - LegacyBeatmapDecoder.RulesetStore = rulesets; + LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets)); } /// From 3abbf07fb3391d13e617409e1bfe16b5919243d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Feb 2022 16:52:14 +0900 Subject: [PATCH 135/306] Revert local registrations in test scenes --- osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs | 3 --- osu.Game.Rulesets.Osu.Tests/StackingTest.cs | 6 ------ .../Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 7 ------- osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 7 ------- osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs | 7 ------- osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs | 3 --- osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs | 6 ------ osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs | 6 ------ 8 files changed, 45 deletions(-) diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs index 0dd66be0b4..1d207d04c7 100644 --- a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs +++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs @@ -8,7 +8,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Archives; -using osu.Game.Rulesets; using osu.Game.Tests.Resources; namespace osu.Game.Benchmarks @@ -19,8 +18,6 @@ namespace osu.Game.Benchmarks public override void SetUp() { - Decoder.RegisterDependencies(new AssemblyRulesetStore()); - using (var resources = new DllResourceStore(typeof(TestResources).Assembly)) using (var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz")) using (var reader = new ZipArchiveReader(archive)) diff --git a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs index 927c0c3e1e..871afdb09d 100644 --- a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs @@ -18,12 +18,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class StackingTest { - [SetUp] - public void SetUp() - { - Decoder.RegisterDependencies(new AssemblyRulesetStore()); - } - [Test] public void TestStacking() { diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index ad088d298b..468cb7683c 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.IO; -using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Mods; @@ -30,12 +29,6 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class LegacyBeatmapDecoderTest { - [SetUp] - public void SetUp() - { - Decoder.RegisterDependencies(new AssemblyRulesetStore()); - } - [Test] public void TestDecodeBeatmapVersion() { diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index b7989adcb8..2eb75259d9 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -10,7 +10,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Serialization; -using osu.Game.Rulesets; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; @@ -23,12 +22,6 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class OsuJsonDecoderTest { - [SetUp] - public void SetUp() - { - Decoder.RegisterDependencies(new AssemblyRulesetStore()); - } - private const string normal = "Soleily - Renatus (Gamu) [Insane].osu"; private const string marathon = "Within Temptation - The Unforgiving (Armin) [Marathon].osu"; private const string with_sb = "Kozato snow - Rengetsu Ouka (_Kiva) [Yuki YukI].osu"; diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index 64a52b2b01..810ea5dbd0 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -9,19 +9,12 @@ using osu.Game.Tests.Resources; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Archives; -using osu.Game.Rulesets; namespace osu.Game.Tests.Beatmaps.IO { [TestFixture] public class OszArchiveReaderTest { - [SetUp] - public void SetUp() - { - Decoder.RegisterDependencies(new AssemblyRulesetStore()); - } - [Test] public void TestReadBeatmaps() { diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index 6cf760723a..44a908b756 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -8,7 +8,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; -using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; @@ -29,8 +28,6 @@ namespace osu.Game.Tests.Editing [SetUp] public void Setup() { - Decoder.RegisterDependencies(new AssemblyRulesetStore()); - patcher = new LegacyEditorBeatmapPatcher(current = new EditorBeatmap(new OsuBeatmap { BeatmapInfo = diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index d5b7e3152b..8d622955b7 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -34,12 +34,6 @@ namespace osu.Game.Tests.Beatmaps protected abstract string ResourceAssembly { get; } - [SetUp] - public void Setup() - { - Decoder.RegisterDependencies(new AssemblyRulesetStore()); - } - protected void Test(string name, params Type[] mods) { var ourResult = convert(name, mods.Select(m => (Mod)Activator.CreateInstance(m)).ToArray()); diff --git a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs index 78b2558640..9f8811c7f9 100644 --- a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs +++ b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs @@ -22,12 +22,6 @@ namespace osu.Game.Tests.Beatmaps protected abstract string ResourceAssembly { get; } - [SetUp] - public void Setup() - { - Decoder.RegisterDependencies(new AssemblyRulesetStore()); - } - protected void Test(double expected, string name, params Mod[] mods) { // Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences. From a029e418cfef55371f956f3b4e39be6126596e99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Feb 2022 17:06:04 +0900 Subject: [PATCH 136/306] Use `internal` instead of `protected internal` --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 9dd2f27be5..e2a043490f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps.Formats { public class LegacyBeatmapDecoder : LegacyDecoder { - protected internal static RulesetStore RulesetStore; + internal static RulesetStore RulesetStore; private Beatmap beatmap; From c869be87d1c817af8a4ae5382d5c17e124fb40ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 19 Feb 2022 20:53:04 +0900 Subject: [PATCH 137/306] Update `FlatFileWorkingBeatmap` to not require a ruleset store --- osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs | 5 ++--- osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs | 9 +++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs index 97a4c57bf0..10761bc315 100644 --- a/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs +++ b/osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs @@ -77,10 +77,9 @@ namespace osu.Desktop.LegacyIpc case LegacyIpcDifficultyCalculationRequest req: try { - var ruleset = getLegacyRulesetFromID(req.RulesetId); - + WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile); + var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray(); - WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset); return new LegacyIpcDifficultyCalculationResponse { diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs index 163da12b2e..cd8aa31ead 100644 --- a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs @@ -7,7 +7,6 @@ using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.Formats; using osu.Game.IO; -using osu.Game.Rulesets; using osu.Game.Skinning; namespace osu.Game.Beatmaps @@ -20,18 +19,16 @@ namespace osu.Game.Beatmaps { private readonly Beatmap beatmap; - public FlatFileWorkingBeatmap(string file, Func rulesetProvider, int? beatmapId = null) - : this(readFromFile(file), rulesetProvider, beatmapId) + public FlatFileWorkingBeatmap(string file, int? beatmapId = null) + : this(readFromFile(file), beatmapId) { } - private FlatFileWorkingBeatmap(Beatmap beatmap, Func rulesetProvider, int? beatmapId = null) + private FlatFileWorkingBeatmap(Beatmap beatmap, int? beatmapId = null) : base(beatmap.BeatmapInfo, null) { this.beatmap = beatmap; - beatmap.BeatmapInfo.Ruleset = rulesetProvider(beatmap.BeatmapInfo.Ruleset.OnlineID).RulesetInfo; - if (beatmapId.HasValue) beatmap.BeatmapInfo.OnlineID = beatmapId.Value; } From 7ef710de2205e1af671fa5f4f0af76efc16373c5 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Sat, 19 Feb 2022 18:14:56 +0100 Subject: [PATCH 138/306] Allow exiting/minimizing on Android when on the initial cookie screen --- osu.Game/Screens/Menu/MainMenu.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 8b1bab52b3..2391903861 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -70,12 +70,16 @@ namespace osu.Game.Screens.Menu private ParallaxContainer buttonsContainer; private SongTicker songTicker; + private readonly BindableBool allowExitingAndroid = new BindableBool(true); + [BackgroundDependencyLoader(true)] private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) { holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); loginDisplayed = statics.GetBindable(Static.LoginOverlayDisplayed); + host.AllowExitingAndroid.AddSource(allowExitingAndroid); + if (host.CanExit) { AddInternal(exitConfirmOverlay = new ExitConfirmOverlay @@ -134,6 +138,8 @@ namespace osu.Game.Screens.Menu ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine)); break; } + + allowExitingAndroid.Value = state == ButtonSystemState.Initial; }; buttons.OnSettings = () => settings?.ToggleVisibility(); @@ -297,5 +303,11 @@ namespace osu.Game.Screens.Menu Schedule(loadSoloSongSelect); } + + protected override void Dispose(bool isDisposing) + { + host.AllowExitingAndroid.RemoveSource(allowExitingAndroid); + base.Dispose(isDisposing); + } } } From 11a11802edf98e666b25182e6740c31bc9c13981 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Sat, 19 Feb 2022 19:28:17 +0100 Subject: [PATCH 139/306] Ensure exiting is disallowed if we're not at the main menu --- osu.Android/OsuGameAndroid.cs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 050bf2b787..914a6f7502 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -5,7 +5,11 @@ using System; using Android.App; using Android.OS; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Platform; +using osu.Framework.Screens; using osu.Game; +using osu.Game.Screens.Menu; using osu.Game.Updater; using osu.Game.Utils; using Xamarin.Essentials; @@ -17,6 +21,8 @@ namespace osu.Android [Cached] private readonly OsuGameActivity gameActivity; + private readonly BindableBool allowExiting = new BindableBool(); + public OsuGameAndroid(OsuGameActivity activity) : base(null) { @@ -67,16 +73,45 @@ namespace osu.Android } } + public override void SetHost(GameHost host) + { + base.SetHost(host); + host.AllowExitingAndroid.AddSource(allowExiting); + } + protected override void LoadComplete() { base.LoadComplete(); LoadComponentAsync(new GameplayScreenRotationLocker(), Add); } + protected override void ScreenChanged(IScreen current, IScreen newScreen) + { + base.ScreenChanged(current, newScreen); + + switch (newScreen) + { + case MainMenu _: + // allow the MainMenu to (dis)allow exiting based on its ButtonSystemState. + allowExiting.Value = true; + break; + + default: + allowExiting.Value = false; + break; + } + } + protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo(); + protected override void Dispose(bool isDisposing) + { + Host.AllowExitingAndroid.RemoveSource(allowExiting); + base.Dispose(isDisposing); + } + private class AndroidBatteryInfo : BatteryInfo { public override double ChargeLevel => Battery.ChargeLevel; From 15ed9ec4fa725f08c6e49ca0192542f2b7aee6ac Mon Sep 17 00:00:00 2001 From: dekrain Date: Sat, 19 Feb 2022 20:47:02 +0100 Subject: [PATCH 140/306] Merge scoreboard and leaderboard implementations together --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 2 + .../Online/Leaderboards/LeaderboardScore.cs | 23 +------ .../BeatmapSet/Scores/ScoreboardTime.cs | 40 +---------- osu.Game/Utils/ScoreboardTimeUtils.cs | 66 +++++++++++++++++++ 4 files changed, 73 insertions(+), 58 deletions(-) create mode 100644 osu.Game/Utils/ScoreboardTimeUtils.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 2f44633f4d..c4178143f8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -388,6 +388,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.5140, MaxCombo = 244, TotalScore = 1707827, + Date = DateTime.Now.AddMonths(-3), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, @@ -409,6 +410,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.4222, MaxCombo = 244, TotalScore = 1707827, + Date = DateTime.Now.AddYears(-2), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, Ruleset = new OsuRuleset().RulesetInfo, diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 4f7670f098..faec0abce6 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -30,7 +30,7 @@ using osuTK; using osuTK.Graphics; using osu.Game.Online.API; using osu.Game.Utils; -using osu.Game.Resources.Localisation.Web; +using osu.Framework.Utils; namespace osu.Game.Online.Leaderboards { @@ -398,24 +398,7 @@ namespace osu.Game.Online.Leaderboards protected override string Format() { - var now = DateTime.Now; - var difference = now - Date; - - // TODO(optional): support localisation (probably via `CommonStrings.CountHours()` etc.) - // requires pluralisable string support framework-side - - if (difference.TotalSeconds < 10) - return CommonStrings.TimeNow.ToString(); - if (difference.TotalMinutes < 1) - return $@"{Math.Floor(difference.TotalSeconds)}s"; - if (difference.TotalHours < 1) - return $@"{Math.Floor(difference.TotalMinutes)}min"; - if (difference.TotalDays < 1) - return $@"{Math.Floor(difference.TotalHours)}h"; - if (difference.TotalDays < 3) - return $@"{Math.Floor(difference.TotalDays)}d"; - - return string.Empty; + return ScoreboardTimeUtils.FormatDate(Date, TimeSpan.FromSeconds(30)); } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs index ff1d3490b4..ceb446f310 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs @@ -2,9 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using Humanizer; using osu.Game.Graphics; -using osu.Game.Resources.Localisation.Web; +using osu.Game.Utils; namespace osu.Game.Overlays.BeatmapSet.Scores { @@ -16,41 +15,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } protected override string Format() - { - var now = DateTime.Now; - var difference = now - Date; - - // web uses momentjs's custom locales to format the date for the purposes of the scoreboard. - // this is intended to be a best-effort, more legible approximation of that. - // compare: - // * https://github.com/ppy/osu-web/blob/a8f5a68fb435cb19a4faa4c7c4bce08c4f096933/resources/assets/lib/scoreboard-time.tsx - // * https://momentjs.com/docs/#/customization/ (reference for the customisation format) - - // TODO: support localisation (probably via `CommonStrings.CountHours()` etc.) - // requires pluralisable string support framework-side - - if (difference.TotalHours < 1) - return CommonStrings.TimeNow.ToString(); - if (difference.TotalDays < 1) - return "hr".ToQuantity((int)difference.TotalHours); - - // this is where this gets more complicated because of how the calendar works. - // since there's no `TotalMonths` / `TotalYears`, we have to iteratively add months/years - // and test against cutoff dates to determine how many months/years to show. - - if (Date > now.AddMonths(-1)) - return difference.TotalDays < 2 ? "1dy" : $"{(int)difference.TotalDays}dys"; - - for (int months = 1; months <= 11; ++months) - { - if (Date > now.AddMonths(-(months + 1))) - return months == 1 ? "1mo" : $"{months}mos"; - } - - int years = 1; - while (Date <= now.AddYears(-(years + 1))) - years += 1; - return years == 1 ? "1yr" : $"{years}yrs"; - } + => ScoreboardTimeUtils.FormatDate(Date, TimeSpan.FromHours(1)); } } diff --git a/osu.Game/Utils/ScoreboardTimeUtils.cs b/osu.Game/Utils/ScoreboardTimeUtils.cs new file mode 100644 index 0000000000..7e84b8f2d0 --- /dev/null +++ b/osu.Game/Utils/ScoreboardTimeUtils.cs @@ -0,0 +1,66 @@ +// 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.Resources.Localisation.Web; + +#nullable enable + +namespace osu.Game.Utils +{ + public static class ScoreboardTimeUtils + { + public static string FormatQuantity(string template, int quantity) + { + if (quantity <= 1) + return $@"{quantity}{template}"; + return $@"{quantity}{template}s"; + } + + public static string FormatDate(DateTimeOffset time, TimeSpan lowerCutoff) + { + // This function fails if the passed in time is something close to an epoch + + // web uses momentjs's custom locales to format the date for the purposes of the scoreboard. + // this is intended to be a best-effort, more legible approximation of that. + // compare: + // * https://github.com/ppy/osu-web/blob/a8f5a68fb435cb19a4faa4c7c4bce08c4f096933/resources/assets/lib/scoreboard-time.tsx + // * https://momentjs.com/docs/#/customization/ (reference for the customisation format) + + // TODO: support localisation (probably via `CommonStrings.CountHours()` etc.) + // requires pluralisable string support framework-side + + var now = DateTime.Now; + var span = now - time; + + if (span < lowerCutoff) + return CommonStrings.TimeNow.ToString(); + + if (span.TotalMinutes < 1) + return FormatQuantity("sec", (int)span.TotalSeconds); + if (span.TotalHours < 1) + return FormatQuantity("min", (int)span.TotalMinutes); + if (span.TotalDays < 1) + return FormatQuantity("hr", (int)span.TotalHours); + + // this is where this gets more complicated because of how the calendar works. + // since there's no `TotalMonths` / `TotalYears`, we have to iteratively add months/years + // and test against cutoff dates to determine how many months/years to show. + + if (time > now.AddMonths(-1)) + return FormatQuantity("dy", (int)span.TotalDays); + + for (int months = 1; months <= 11; ++months) + { + if (time > now.AddMonths(-(months + 1))) + return FormatQuantity("mo", months); + } + + int years = 1; + // DateTime causes a crash here for epoch + while (time <= now.AddYears(-(years + 1))) + years += 1; + return FormatQuantity("yr", years); + } + } +} From 0d83c5a39a02581c3eb5d702c143ec55c904f461 Mon Sep 17 00:00:00 2001 From: dekrain Date: Sat, 19 Feb 2022 20:47:30 +0100 Subject: [PATCH 141/306] Add colour highlighting recent scores --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index faec0abce6..217be5a024 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -390,6 +390,9 @@ namespace osu.Game.Online.Leaderboards private class DateLabel : DrawableDate { + public static readonly Colour4 COLOUR_SATURATED = Colour4.Lime; + public static readonly Colour4 COLOUR_UNSATURATED = Colour4.White; + public DateLabel(DateTimeOffset date) : base(date) { @@ -398,6 +401,15 @@ namespace osu.Game.Online.Leaderboards protected override string Format() { + var now = DateTime.Now; + var difference = now - Date; + + const double seconds_to_blank = 60*45; + const double tense_factor = 2.325; + + double tf = Math.Pow(difference.TotalSeconds / seconds_to_blank, tense_factor); + Colour = Interpolation.ValueAt(tf, COLOUR_SATURATED, COLOUR_UNSATURATED, 0, 1); + return ScoreboardTimeUtils.FormatDate(Date, TimeSpan.FromSeconds(30)); } } From 31b7ce053d24cef175f3b4b399d1a9e79b86fc32 Mon Sep 17 00:00:00 2001 From: dekrain Date: Sat, 19 Feb 2022 21:18:26 +0100 Subject: [PATCH 142/306] Fix CI issues --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- osu.Game/Utils/ScoreboardTimeUtils.cs | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 217be5a024..aaf889d06c 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -404,7 +404,7 @@ namespace osu.Game.Online.Leaderboards var now = DateTime.Now; var difference = now - Date; - const double seconds_to_blank = 60*45; + const double seconds_to_blank = 60 * 45; const double tense_factor = 2.325; double tf = Math.Pow(difference.TotalSeconds / seconds_to_blank, tense_factor); diff --git a/osu.Game/Utils/ScoreboardTimeUtils.cs b/osu.Game/Utils/ScoreboardTimeUtils.cs index 7e84b8f2d0..86639c1034 100644 --- a/osu.Game/Utils/ScoreboardTimeUtils.cs +++ b/osu.Game/Utils/ScoreboardTimeUtils.cs @@ -14,13 +14,12 @@ namespace osu.Game.Utils { if (quantity <= 1) return $@"{quantity}{template}"; + return $@"{quantity}{template}s"; } public static string FormatDate(DateTimeOffset time, TimeSpan lowerCutoff) { - // This function fails if the passed in time is something close to an epoch - // web uses momentjs's custom locales to format the date for the purposes of the scoreboard. // this is intended to be a best-effort, more legible approximation of that. // compare: @@ -57,10 +56,13 @@ namespace osu.Game.Utils } int years = 1; - // DateTime causes a crash here for epoch - while (time <= now.AddYears(-(years + 1))) + // Add upper bound to prevent a crash + while (years < 20 && time <= now.AddYears(-(years + 1))) years += 1; - return FormatQuantity("yr", years); + if (years < 20) + return FormatQuantity("yr", years); + + return "never"; } } } From 262751a98a22d87ef5406203d5b38f94e65d30e0 Mon Sep 17 00:00:00 2001 From: dekrain Date: Sat, 19 Feb 2022 21:23:35 +0100 Subject: [PATCH 143/306] Revert highlighting recent scores --- .../Online/Leaderboards/LeaderboardScore.cs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index aaf889d06c..a8b508829d 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -390,28 +390,13 @@ namespace osu.Game.Online.Leaderboards private class DateLabel : DrawableDate { - public static readonly Colour4 COLOUR_SATURATED = Colour4.Lime; - public static readonly Colour4 COLOUR_UNSATURATED = Colour4.White; - public DateLabel(DateTimeOffset date) : base(date) { Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, italics: true); } - protected override string Format() - { - var now = DateTime.Now; - var difference = now - Date; - - const double seconds_to_blank = 60 * 45; - const double tense_factor = 2.325; - - double tf = Math.Pow(difference.TotalSeconds / seconds_to_blank, tense_factor); - Colour = Interpolation.ValueAt(tf, COLOUR_SATURATED, COLOUR_UNSATURATED, 0, 1); - - return ScoreboardTimeUtils.FormatDate(Date, TimeSpan.FromSeconds(30)); - } + protected override string Format() => ScoreboardTimeUtils.FormatDate(Date, TimeSpan.FromSeconds(30)); } public class LeaderboardScoreStatistic From e20ae5b87100a387045471902547cd2296841519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Feb 2022 23:54:29 +0100 Subject: [PATCH 144/306] Add all colour constants for "basic" colour theme to `OsuColour` --- osu.Game/Graphics/OsuColour.cs | 57 +++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index f63bd53549..567d665e81 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -264,32 +264,53 @@ namespace osu.Game.Graphics public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee"); public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff"); - /// - /// Equivalent to 's . - /// - public readonly Color4 Pink3 = Color4Extensions.FromHex(@"cc3378"); + #region "Basic" colour theme - /// - /// Equivalent to 's . - /// + // Reference: https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Asset%2FColours?node-id=1838%3A3 + + public readonly Color4 Pink0 = Color4Extensions.FromHex(@"ff99c7"); + public readonly Color4 Pink1 = Color4Extensions.FromHex(@"ff66ab"); + public readonly Color4 Pink2 = Color4Extensions.FromHex(@"eb4791"); + public readonly Color4 Pink3 = Color4Extensions.FromHex(@"cc3378"); + public readonly Color4 Pink4 = Color4Extensions.FromHex(@"6b2e49"); + + public readonly Color4 Purple0 = Color4Extensions.FromHex(@"b299ff"); + public readonly Color4 Purple1 = Color4Extensions.FromHex(@"8c66ff"); + public readonly Color4 Purple2 = Color4Extensions.FromHex(@"7047eb"); + public readonly Color4 Purple3 = Color4Extensions.FromHex(@"5933cc"); + public readonly Color4 Purple4 = Color4Extensions.FromHex(@"3d2e6b"); + + public readonly Color4 Blue0 = Color4Extensions.FromHex(@"99ddff"); + public readonly Color4 Blue1 = Color4Extensions.FromHex(@"66ccff"); + public readonly Color4 Blue2 = Color4Extensions.FromHex(@"47b4eb"); public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc"); + public readonly Color4 Blue4 = Color4Extensions.FromHex(@"2e576b"); + + public readonly Color4 Green0 = Color4Extensions.FromHex(@"99ffa2"); + public readonly Color4 Green1 = Color4Extensions.FromHex(@"66ff73"); + public readonly Color4 Green2 = Color4Extensions.FromHex(@"47eb55"); + public readonly Color4 Green3 = Color4Extensions.FromHex(@"33cc40"); + public readonly Color4 Green4 = Color4Extensions.FromHex(@"2e6b33"); public readonly Color4 Lime0 = Color4Extensions.FromHex(@"ccff99"); - - /// - /// Equivalent to 's . - /// public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66"); - - /// - /// Equivalent to 's . - /// + public readonly Color4 Lime2 = Color4Extensions.FromHex(@"99eb47"); public readonly Color4 Lime3 = Color4Extensions.FromHex(@"7fcc33"); + public readonly Color4 Lime4 = Color4Extensions.FromHex(@"4c6b2e"); - /// - /// Equivalent to 's . - /// + public readonly Color4 Orange0 = Color4Extensions.FromHex(@"ffe699"); public readonly Color4 Orange1 = Color4Extensions.FromHex(@"ffd966"); + public readonly Color4 Orange2 = Color4Extensions.FromHex(@"ebc247"); + public readonly Color4 Orange3 = Color4Extensions.FromHex(@"cca633"); + public readonly Color4 Orange4 = Color4Extensions.FromHex(@"6b5c2e"); + + public readonly Color4 Red0 = Color4Extensions.FromHex(@"ff9b9b"); + public readonly Color4 Red1 = Color4Extensions.FromHex(@"ff6666"); + public readonly Color4 Red2 = Color4Extensions.FromHex(@"eb4747"); + public readonly Color4 Red3 = Color4Extensions.FromHex(@"cc3333"); + public readonly Color4 Red4 = Color4Extensions.FromHex(@"6b2e2e"); + + #endregion // Content Background public readonly Color4 B5 = Color4Extensions.FromHex(@"222a28"); From 2592f0900d384e958cfbb6b99a638cb8089be134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 20:40:08 +0100 Subject: [PATCH 145/306] Add comments about `OverlayColourProvider` vs `OsuColour` distinction --- osu.Game/Graphics/OsuColour.cs | 5 +++++ osu.Game/Overlays/OverlayColourProvider.cs | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 567d665e81..886ba7ef92 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -268,6 +268,11 @@ namespace osu.Game.Graphics // Reference: https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Asset%2FColours?node-id=1838%3A3 + // Note that the colours in this region are also defined in `OverlayColourProvider` as `Colour{0,1,2,3,4}`. + // The difference as to which should be used where comes down to context. + // If the colour in question is supposed to always match the view in which it is displayed theme-wise, use `OverlayColourProvider`. + // If the colour usage is special and in general differs from the surrounding view in choice of hue, use the `OsuColour` constants. + public readonly Color4 Pink0 = Color4Extensions.FromHex(@"ff99c7"); public readonly Color4 Pink1 = Color4Extensions.FromHex(@"ff66ab"); public readonly Color4 Pink2 = Color4Extensions.FromHex(@"eb4791"); diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index e7b3e6d873..730ca09411 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.cs @@ -25,6 +25,10 @@ namespace osu.Game.Overlays this.colourScheme = colourScheme; } + // Note that the following five colours are also defined in `OsuColour` as `{colourScheme}{0,1,2,3,4}`. + // The difference as to which should be used where comes down to context. + // If the colour in question is supposed to always match the view in which it is displayed theme-wise, use `OverlayColourProvider`. + // If the colour usage is special and in general differs from the surrounding view in choice of hue, use the `OsuColour` constants. public Color4 Colour1 => getColour(1, 0.7f); public Color4 Colour2 => getColour(0.8f, 0.6f); public Color4 Colour3 => getColour(0.6f, 0.5f); From 79ba37bbabc1fdd1acffcef16c4caac092066665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 20:40:39 +0100 Subject: [PATCH 146/306] Add `Colour0` to `OverlayColourProvider` --- osu.Game/Overlays/OverlayColourProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index 730ca09411..c2ceef57e8 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.cs @@ -29,6 +29,7 @@ namespace osu.Game.Overlays // The difference as to which should be used where comes down to context. // If the colour in question is supposed to always match the view in which it is displayed theme-wise, use `OverlayColourProvider`. // If the colour usage is special and in general differs from the surrounding view in choice of hue, use the `OsuColour` constants. + public Color4 Colour0 => getColour(1, 0.8f); public Color4 Colour1 => getColour(1, 0.7f); public Color4 Colour2 => getColour(0.8f, 0.6f); public Color4 Colour3 => getColour(0.6f, 0.5f); From ce0db9d4dbd7a0b030558002331dea4312be728a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 20:46:12 +0100 Subject: [PATCH 147/306] Remove all references to static `OverlayColourProvider`s --- .../BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 7 ++++++- osu.Game/Overlays/BeatmapListing/FilterTabItem.cs | 7 +++++++ osu.Game/Overlays/BeatmapSet/ExplicitContentBeatmapPill.cs | 2 +- osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapPill.cs | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index fb9e1c0420..51dad100c2 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.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.Allocation; +using osu.Game.Graphics; using osu.Game.Resources.Localisation.Web; using osuTK.Graphics; @@ -33,7 +35,10 @@ namespace osu.Game.Overlays.BeatmapListing { } - protected override Color4 GetStateColour() => OverlayColourProvider.Orange.Colour1; + [Resolved] + private OsuColour colours { get; set; } + + protected override Color4 GetStateColour() => colours.Orange1; } } } diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 9274cf20aa..52dfcad2cc 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -44,7 +44,14 @@ namespace osu.Game.Overlays.BeatmapListing }); Enabled.Value = true; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateState(); + FinishTransforms(true); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Overlays/BeatmapSet/ExplicitContentBeatmapPill.cs b/osu.Game/Overlays/BeatmapSet/ExplicitContentBeatmapPill.cs index ba78592ed2..21d1d1172c 100644 --- a/osu.Game/Overlays/BeatmapSet/ExplicitContentBeatmapPill.cs +++ b/osu.Game/Overlays/BeatmapSet/ExplicitContentBeatmapPill.cs @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.BeatmapSet Margin = new MarginPadding { Horizontal = 10f, Vertical = 2f }, Text = BeatmapsetsStrings.NsfwBadgeLabel.ToUpper(), Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), - Colour = OverlayColourProvider.Orange.Colour2, + Colour = colours.Orange2 } } }; diff --git a/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapPill.cs b/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapPill.cs index fdee0799ff..1be987cde2 100644 --- a/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapPill.cs +++ b/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapPill.cs @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.BeatmapSet Margin = new MarginPadding { Horizontal = 10f, Vertical = 2f }, Text = BeatmapsetsStrings.FeaturedArtistBadgeLabel.ToUpper(), Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), - Colour = OverlayColourProvider.Blue.Colour1, + Colour = colours.Blue1 } } }; From 36a00c1ee238b13cfe857882110e1917cef5c49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 20:46:33 +0100 Subject: [PATCH 148/306] Remove static `OverlayColourProvider`s --- osu.Game/Overlays/OverlayColourProvider.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index c2ceef57e8..7bddb924a0 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.cs @@ -11,15 +11,6 @@ namespace osu.Game.Overlays { private readonly OverlayColourScheme colourScheme; - public static OverlayColourProvider Red { get; } = new OverlayColourProvider(OverlayColourScheme.Red); - public static OverlayColourProvider Pink { get; } = new OverlayColourProvider(OverlayColourScheme.Pink); - public static OverlayColourProvider Orange { get; } = new OverlayColourProvider(OverlayColourScheme.Orange); - public static OverlayColourProvider Lime { get; } = new OverlayColourProvider(OverlayColourScheme.Lime); - public static OverlayColourProvider Green { get; } = new OverlayColourProvider(OverlayColourScheme.Green); - public static OverlayColourProvider Purple { get; } = new OverlayColourProvider(OverlayColourScheme.Purple); - public static OverlayColourProvider Blue { get; } = new OverlayColourProvider(OverlayColourScheme.Blue); - public static OverlayColourProvider Plum { get; } = new OverlayColourProvider(OverlayColourScheme.Plum); - public OverlayColourProvider(OverlayColourScheme colourScheme) { this.colourScheme = colourScheme; From 2ded7d281b4b36e8d92ec12d72f1ba19ee33f935 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 13:17:19 +0900 Subject: [PATCH 149/306] Remove unused using statement --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index a8b508829d..0e9968f27f 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -30,7 +30,6 @@ using osuTK; using osuTK.Graphics; using osu.Game.Online.API; using osu.Game.Utils; -using osu.Framework.Utils; namespace osu.Game.Online.Leaderboards { From 79408f6afcfd778e84c59ae329c73faf253dfa56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 13:30:58 +0900 Subject: [PATCH 150/306] Add xmldoc and clean up `ScoreboardTimeUtils` extension methods a touch --- .../Online/Leaderboards/LeaderboardScore.cs | 2 +- .../BeatmapSet/Scores/ScoreboardTime.cs | 2 +- osu.Game/Utils/ScoreboardTimeUtils.cs | 36 +++++++++++-------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0e9968f27f..a9f03e5c9f 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -395,7 +395,7 @@ namespace osu.Game.Online.Leaderboards Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, italics: true); } - protected override string Format() => ScoreboardTimeUtils.FormatDate(Date, TimeSpan.FromSeconds(30)); + protected override string Format() => ScoreboardTimeUtils.FormatRelativeTime(Date, TimeSpan.FromSeconds(30)); } public class LeaderboardScoreStatistic diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs index ceb446f310..1927e66edb 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs @@ -15,6 +15,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } protected override string Format() - => ScoreboardTimeUtils.FormatDate(Date, TimeSpan.FromHours(1)); + => ScoreboardTimeUtils.FormatRelativeTime(Date, TimeSpan.FromHours(1)); } } diff --git a/osu.Game/Utils/ScoreboardTimeUtils.cs b/osu.Game/Utils/ScoreboardTimeUtils.cs index 86639c1034..4e079ff39f 100644 --- a/osu.Game/Utils/ScoreboardTimeUtils.cs +++ b/osu.Game/Utils/ScoreboardTimeUtils.cs @@ -10,15 +10,13 @@ namespace osu.Game.Utils { public static class ScoreboardTimeUtils { - public static string FormatQuantity(string template, int quantity) - { - if (quantity <= 1) - return $@"{quantity}{template}"; - - return $@"{quantity}{template}s"; - } - - public static string FormatDate(DateTimeOffset time, TimeSpan lowerCutoff) + /// + /// Formats a provided date to a short relative string version for compact display. + /// + /// The time to be displayed. + /// A timespan denoting the time length beneath which "now" should be displayed. + /// A short relative string representing the input time. + public static string FormatRelativeTime(DateTimeOffset time, TimeSpan lowerCutoff) { // web uses momentjs's custom locales to format the date for the purposes of the scoreboard. // this is intended to be a best-effort, more legible approximation of that. @@ -36,23 +34,23 @@ namespace osu.Game.Utils return CommonStrings.TimeNow.ToString(); if (span.TotalMinutes < 1) - return FormatQuantity("sec", (int)span.TotalSeconds); + return formatQuantity("sec", (int)span.TotalSeconds); if (span.TotalHours < 1) - return FormatQuantity("min", (int)span.TotalMinutes); + return formatQuantity("min", (int)span.TotalMinutes); if (span.TotalDays < 1) - return FormatQuantity("hr", (int)span.TotalHours); + return formatQuantity("hr", (int)span.TotalHours); // this is where this gets more complicated because of how the calendar works. // since there's no `TotalMonths` / `TotalYears`, we have to iteratively add months/years // and test against cutoff dates to determine how many months/years to show. if (time > now.AddMonths(-1)) - return FormatQuantity("dy", (int)span.TotalDays); + return formatQuantity("dy", (int)span.TotalDays); for (int months = 1; months <= 11; ++months) { if (time > now.AddMonths(-(months + 1))) - return FormatQuantity("mo", months); + return formatQuantity("mo", months); } int years = 1; @@ -60,9 +58,17 @@ namespace osu.Game.Utils while (years < 20 && time <= now.AddYears(-(years + 1))) years += 1; if (years < 20) - return FormatQuantity("yr", years); + return formatQuantity("yr", years); return "never"; } + + private static string formatQuantity(string template, int quantity) + { + if (quantity <= 1) + return $@"{quantity}{template}"; + + return $@"{quantity}{template}s"; + } } } From c3b365cf6b79c8cfd618982fb163e6b7e72ed21e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 21 Feb 2022 13:31:02 +0900 Subject: [PATCH 151/306] Scale classic score by hitobject count --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 15 ++++++++------- osu.Game/Scoring/ScoreManager.cs | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 79861c0ecc..42ab30d4d0 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Scoring private double getScore(ScoringMode mode) { - return GetScore(mode, maxAchievableCombo, + return GetScore(mode, calculateAccuracyRatio(baseScore), calculateComboRatio(HighestCombo.Value), scoreResultCounts); @@ -222,12 +222,11 @@ namespace osu.Game.Rulesets.Scoring /// Computes the total score. /// /// The to compute the total score in. - /// The maximum combo achievable in the beatmap. /// The accuracy percentage achieved by the player. - /// The proportion of achieved by the player. + /// The proportion of the max combo achieved by the player. /// Any statistics to be factored in. /// The total score. - public double GetScore(ScoringMode mode, int maxCombo, double accuracyRatio, double comboRatio, Dictionary statistics) + public double GetScore(ScoringMode mode, double accuracyRatio, double comboRatio, Dictionary statistics) { switch (mode) { @@ -238,10 +237,12 @@ namespace osu.Game.Rulesets.Scoring return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; case ScoringMode.Classic: + int totalHitObjects = statistics.Where(k => k.Key >= HitResult.Miss && k.Key <= HitResult.Perfect).Sum(k => k.Value); + // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. - double scaledStandardised = GetScore(ScoringMode.Standardised, maxCombo, accuracyRatio, comboRatio, statistics) / max_score; - return Math.Pow(scaledStandardised * (maxCombo + 1), 2) * 18; + double scaledStandardised = GetScore(ScoringMode.Standardised, accuracyRatio, comboRatio, statistics) / max_score; + return Math.Pow(scaledStandardised * totalHitObjects, 2) * 36; } } @@ -265,7 +266,7 @@ namespace osu.Game.Rulesets.Scoring computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; } - return GetScore(mode, maxAchievableCombo, calculateAccuracyRatio(computedBaseScore), calculateComboRatio(maxCombo), statistics); + return GetScore(mode, calculateAccuracyRatio(computedBaseScore), calculateComboRatio(maxCombo), statistics); } /// diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 532c6b42a3..963c4a77ca 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -184,7 +184,7 @@ namespace osu.Game.Scoring var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = score.Mods; - return (long)Math.Round(scoreProcessor.GetScore(mode, beatmapMaxCombo, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics)); + return (long)Math.Round(scoreProcessor.GetScore(mode, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics)); } /// From fc1877b6faea3ad3bb9a476fc744d85dd6a463c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 13:42:26 +0900 Subject: [PATCH 152/306] Move to extension method and revert logic to match previous implementation --- osu.Game/Extensions/TimeDisplayExtensions.cs | 51 ++++++++++++++++++ .../Online/Leaderboards/LeaderboardScore.cs | 3 +- .../BeatmapSet/Scores/ScoreboardTime.cs | 4 +- osu.Game/Utils/ScoreboardTimeUtils.cs | 52 ------------------- 4 files changed, 55 insertions(+), 55 deletions(-) diff --git a/osu.Game/Extensions/TimeDisplayExtensions.cs b/osu.Game/Extensions/TimeDisplayExtensions.cs index dc05482a05..ee6f458398 100644 --- a/osu.Game/Extensions/TimeDisplayExtensions.cs +++ b/osu.Game/Extensions/TimeDisplayExtensions.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Humanizer; using osu.Framework.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Extensions { @@ -47,5 +49,54 @@ namespace osu.Game.Extensions return new LocalisableFormattableString(timeSpan, @"mm\:ss"); } + + /// + /// Formats a provided date to a short relative string version for compact display. + /// + /// The time to be displayed. + /// A timespan denoting the time length beneath which "now" should be displayed. + /// A short relative string representing the input time. + public static string ToShortRelativeTime(this DateTimeOffset time, TimeSpan lowerCutoff) + { + var now = DateTime.Now; + var difference = now - time; + + // web uses momentjs's custom locales to format the date for the purposes of the scoreboard. + // this is intended to be a best-effort, more legible approximation of that. + // compare: + // * https://github.com/ppy/osu-web/blob/a8f5a68fb435cb19a4faa4c7c4bce08c4f096933/resources/assets/lib/scoreboard-time.tsx + // * https://momentjs.com/docs/#/customization/ (reference for the customisation format) + + // TODO: support localisation (probably via `CommonStrings.CountHours()` etc.) + // requires pluralisable string support framework-side + + if (difference < lowerCutoff) + return CommonStrings.TimeNow.ToString(); + + if (difference.TotalMinutes < 1) + return "sec".ToQuantity((int)difference.TotalSeconds); + if (difference.TotalHours < 1) + return "min".ToQuantity((int)difference.TotalMinutes); + if (difference.TotalDays < 1) + return "hr".ToQuantity((int)difference.TotalHours); + + // this is where this gets more complicated because of how the calendar works. + // since there's no `TotalMonths` / `TotalYears`, we have to iteratively add months/years + // and test against cutoff dates to determine how many months/years to show. + + if (time > now.AddMonths(-1)) + return difference.TotalDays < 2 ? "1dy" : $"{(int)difference.TotalDays}dys"; + + for (int months = 1; months <= 11; ++months) + { + if (time > now.AddMonths(-(months + 1))) + return months == 1 ? "1mo" : $"{months}mos"; + } + + int years = 1; + while (time <= now.AddYears(-(years + 1))) + years += 1; + return years == 1 ? "1yr" : $"{years}yrs"; + } } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index a9f03e5c9f..ddd9d9a2b2 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -17,6 +17,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -395,7 +396,7 @@ namespace osu.Game.Online.Leaderboards Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, italics: true); } - protected override string Format() => ScoreboardTimeUtils.FormatRelativeTime(Date, TimeSpan.FromSeconds(30)); + protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); } public class LeaderboardScoreStatistic diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs index 1927e66edb..5018fb8c70 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreboardTime.cs @@ -2,8 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Game.Extensions; using osu.Game.Graphics; -using osu.Game.Utils; namespace osu.Game.Overlays.BeatmapSet.Scores { @@ -15,6 +15,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } protected override string Format() - => ScoreboardTimeUtils.FormatRelativeTime(Date, TimeSpan.FromHours(1)); + => Date.ToShortRelativeTime(TimeSpan.FromHours(1)); } } diff --git a/osu.Game/Utils/ScoreboardTimeUtils.cs b/osu.Game/Utils/ScoreboardTimeUtils.cs index 4e079ff39f..ec222de018 100644 --- a/osu.Game/Utils/ScoreboardTimeUtils.cs +++ b/osu.Game/Utils/ScoreboardTimeUtils.cs @@ -10,58 +10,6 @@ namespace osu.Game.Utils { public static class ScoreboardTimeUtils { - /// - /// Formats a provided date to a short relative string version for compact display. - /// - /// The time to be displayed. - /// A timespan denoting the time length beneath which "now" should be displayed. - /// A short relative string representing the input time. - public static string FormatRelativeTime(DateTimeOffset time, TimeSpan lowerCutoff) - { - // web uses momentjs's custom locales to format the date for the purposes of the scoreboard. - // this is intended to be a best-effort, more legible approximation of that. - // compare: - // * https://github.com/ppy/osu-web/blob/a8f5a68fb435cb19a4faa4c7c4bce08c4f096933/resources/assets/lib/scoreboard-time.tsx - // * https://momentjs.com/docs/#/customization/ (reference for the customisation format) - - // TODO: support localisation (probably via `CommonStrings.CountHours()` etc.) - // requires pluralisable string support framework-side - - var now = DateTime.Now; - var span = now - time; - - if (span < lowerCutoff) - return CommonStrings.TimeNow.ToString(); - - if (span.TotalMinutes < 1) - return formatQuantity("sec", (int)span.TotalSeconds); - if (span.TotalHours < 1) - return formatQuantity("min", (int)span.TotalMinutes); - if (span.TotalDays < 1) - return formatQuantity("hr", (int)span.TotalHours); - - // this is where this gets more complicated because of how the calendar works. - // since there's no `TotalMonths` / `TotalYears`, we have to iteratively add months/years - // and test against cutoff dates to determine how many months/years to show. - - if (time > now.AddMonths(-1)) - return formatQuantity("dy", (int)span.TotalDays); - - for (int months = 1; months <= 11; ++months) - { - if (time > now.AddMonths(-(months + 1))) - return formatQuantity("mo", months); - } - - int years = 1; - // Add upper bound to prevent a crash - while (years < 20 && time <= now.AddYears(-(years + 1))) - years += 1; - if (years < 20) - return formatQuantity("yr", years); - - return "never"; - } private static string formatQuantity(string template, int quantity) { From 3d0caa44c84f908a013eeac29bc126cfa20bacf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 13:43:30 +0900 Subject: [PATCH 153/306] Remove unused utils class --- osu.Game/Utils/ScoreboardTimeUtils.cs | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 osu.Game/Utils/ScoreboardTimeUtils.cs diff --git a/osu.Game/Utils/ScoreboardTimeUtils.cs b/osu.Game/Utils/ScoreboardTimeUtils.cs deleted file mode 100644 index ec222de018..0000000000 --- a/osu.Game/Utils/ScoreboardTimeUtils.cs +++ /dev/null @@ -1,22 +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.Game.Resources.Localisation.Web; - -#nullable enable - -namespace osu.Game.Utils -{ - public static class ScoreboardTimeUtils - { - - private static string formatQuantity(string template, int quantity) - { - if (quantity <= 1) - return $@"{quantity}{template}"; - - return $@"{quantity}{template}s"; - } - } -} From 656c58567dbe8068d7d4f7dfabcdf2b0818fbb87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 16:21:36 +0900 Subject: [PATCH 154/306] Add safeties to skip attempted import of the intro beatmap when osu! ruleset not present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In general running this import will not cause any critical failures, but the import itself *will* fail – and more loudly with the upcoming changes to `RulesetStore` (https://github.com/ppy/osu/pull/16890). Due to it being a loud failure, it will cause the notification overlay to display a parsing error, which can interrupt the flow of some tests. See test failure at https://github.com/ppy/osu/runs/5268848949?check_suite_focus=true as an example (video coverage at https://github.com/ppy/osu/pull/16890#issuecomment-1046542243). --- osu.Game/Screens/Menu/IntroScreen.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 98c4b15f7f..afe75c5ef7 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -19,6 +19,7 @@ using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osuTK; using osuTK.Graphics; @@ -71,6 +72,9 @@ namespace osu.Game.Screens.Menu [CanBeNull] private readonly Func createNextScreen; + [Resolved] + private RulesetStore rulesets { get; set; } + /// /// Whether the is provided by osu! resources, rather than a user beatmap. /// Only valid during or after . @@ -117,7 +121,11 @@ namespace osu.Game.Screens.Menu // we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available. if (initialBeatmap == null) { - if (!loadThemedIntro()) + // Intro beatmaps are generally made using the osu! ruleset. + // It might not be present in test projects for other rulesets. + bool osuRulesetPresent = rulesets.GetRuleset(0) != null; + + if (!loadThemedIntro() && osuRulesetPresent) { // if we detect that the theme track or beatmap is unavailable this is either first startup or things are in a bad state. // this could happen if a user has nuked their files store. for now, reimport to repair this. From 2f6e65a9a268bf3c4b29a4005556cae95d524a31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 16:35:39 +0900 Subject: [PATCH 155/306] Gracefully handle undefined `DateTimeOffset` values Only seems to happen in tests, but best to safeguard against this regardless. --- osu.Game/Extensions/TimeDisplayExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Extensions/TimeDisplayExtensions.cs b/osu.Game/Extensions/TimeDisplayExtensions.cs index ee6f458398..54af6a5942 100644 --- a/osu.Game/Extensions/TimeDisplayExtensions.cs +++ b/osu.Game/Extensions/TimeDisplayExtensions.cs @@ -58,6 +58,9 @@ namespace osu.Game.Extensions /// A short relative string representing the input time. public static string ToShortRelativeTime(this DateTimeOffset time, TimeSpan lowerCutoff) { + if (time == default) + return "-"; + var now = DateTime.Now; var difference = now - time; From c466d6df94e741a5def1a4395a5d6d03b29240c9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 21 Feb 2022 17:19:35 +0900 Subject: [PATCH 156/306] Ensure to not multiply by 0 --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 42ab30d4d0..d5a5aa4592 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -239,6 +239,10 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Classic: int totalHitObjects = statistics.Where(k => k.Key >= HitResult.Miss && k.Key <= HitResult.Perfect).Sum(k => k.Value); + // If there are no hitobjects then the beatmap can be composed of only ticks or spinners, so ensure we don't multiply by 0 at all times. + if (totalHitObjects == 0) + totalHitObjects = 1; + // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. double scaledStandardised = GetScore(ScoringMode.Standardised, accuracyRatio, comboRatio, statistics) / max_score; From 1737128334db2da3095f52e5fbcbb1c3fd169b8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 17:45:57 +0900 Subject: [PATCH 157/306] Allow room category to be copied even if `Spotlight` I remember that this conditional copy was added to support making copies of spotlight rooms without carrying across the `Spotlight` type, but in testing this is already handled web side to the point that it's not required. The rationale for allowing the copy is that this method is used for tests, where it was not being copied correctly from the input as expected (used at https://github.com/ppy/osu/blob/bdc3b76df0b58406796e2b08db13be7f2140fa7e/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs#L38). --- osu.Game/Online/Rooms/Room.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index bbe854f2dd..a328f8e8c0 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -168,8 +168,7 @@ namespace osu.Game.Online.Rooms RoomID.Value = other.RoomID.Value; Name.Value = other.Name.Value; - if (other.Category.Value != RoomCategory.Spotlight) - Category.Value = other.Category.Value; + Category.Value = other.Category.Value; if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) Host.Value = other.Host.Value; From ab8b502709f5da93709bcfc0f26a6e0053a9ccdc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 17:59:51 +0900 Subject: [PATCH 158/306] Add test coverage of spotlights being at the top of the listing --- .../Multiplayer/TestSceneLoungeRoomsContainer.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index c3d5f7ec23..9a66a2b05c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -44,15 +44,20 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBasicListChanges() { - AddStep("add rooms", () => RoomManager.AddRooms(3)); + AddStep("add rooms", () => RoomManager.AddRooms(5)); - AddAssert("has 3 rooms", () => container.Rooms.Count == 3); - AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.FirstOrDefault())); - AddAssert("has 2 rooms", () => container.Rooms.Count == 2); + AddAssert("has 5 rooms", () => container.Rooms.Count == 5); + + AddAssert("all spotlights at top", () => container.Rooms + .SkipWhile(r => r.Room.Category.Value == RoomCategory.Spotlight) + .All(r => r.Room.Category.Value == RoomCategory.Normal)); + + AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.FirstOrDefault(r => r.RoomID.Value == 0))); + AddAssert("has 4 rooms", () => container.Rooms.Count == 4); AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0)); AddStep("select first room", () => container.Rooms.First().TriggerClick()); - AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category.Value == RoomCategory.Spotlight))); } [Test] From 02a85005004ae66dc4477bfd38932badc16ec894 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 17:59:09 +0900 Subject: [PATCH 159/306] Ensure spotlights always show at the top of the lounge listing As proposed at https://github.com/ppy/osu/discussions/16936. Spotlights are intended to have focus, so let's make sure they are the first thing the user sees for the time being. --- .../Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 9f917c978c..3260427192 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -124,7 +124,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void updateSorting() { foreach (var room in roomFlow) - roomFlow.SetLayoutPosition(room, -(room.Room.RoomID.Value ?? 0)); + { + roomFlow.SetLayoutPosition(room, room.Room.Category.Value == RoomCategory.Spotlight + // Always show spotlight playlists at the top of the listing. + ? float.MinValue + : -(room.Room.RoomID.Value ?? 0)); + } } protected override bool OnClick(ClickEvent e) From 46b408be75b21c3fe872227c298d696851150d3e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 21 Feb 2022 17:59:50 +0900 Subject: [PATCH 160/306] Update tests to match new behaviour --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index f0d9ece06f..c6e7988543 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)] [TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)] [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)] - [TestCase(ScoringMode.Classic, HitResult.Meh, 41)] - [TestCase(ScoringMode.Classic, HitResult.Ok, 46)] - [TestCase(ScoringMode.Classic, HitResult.Great, 72)] + [TestCase(ScoringMode.Classic, HitResult.Meh, 20)] + [TestCase(ScoringMode.Classic, HitResult.Ok, 23)] + [TestCase(ScoringMode.Classic, HitResult.Great, 36)] public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore) { scoreProcessor.Mode.Value = scoringMode; @@ -86,17 +86,17 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points) [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points) [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] - [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 68)] - [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 81)] - [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 109)] - [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 149)] - [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 149)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 9)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 15)] + [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 86)] + [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 104)] + [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 140)] + [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 190)] + [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 190)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 18)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 31)] [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] - [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 149)] - [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 18)] - [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 18)] + [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 12)] + [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 36)] + [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)] public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) { var minResult = new TestJudgement(hitResult).MinResult; @@ -128,8 +128,8 @@ namespace osu.Game.Tests.Rulesets.Scoring /// [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000 - [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 69)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25) - [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 60)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25) + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 34)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 30)] public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore) { IEnumerable hitObjects = Enumerable From 7f4cc221d25c9d9733c5ec6189db61a3a0f0e2f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Feb 2022 18:33:27 +0900 Subject: [PATCH 161/306] Add API versioning --- osu.Game/Online/API/APIAccess.cs | 2 ++ osu.Game/Online/API/APIRequest.cs | 4 ++++ osu.Game/Online/API/DummyAPIAccess.cs | 2 ++ osu.Game/Online/API/IAPIProvider.cs | 5 +++++ 4 files changed, 13 insertions(+) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index c5302a393c..8c9741b98b 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -36,6 +36,8 @@ namespace osu.Game.Online.API public string WebsiteRootUrl { get; } + public int APIVersion => 20220217; // We may want to pull this from the game version eventually. + public Exception LastLoginError { get; private set; } public string ProvidedUsername { get; private set; } diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 91148c177f..776ff5fd8f 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.IO.Network; @@ -112,6 +113,9 @@ namespace osu.Game.Online.API WebRequest = CreateWebRequest(); WebRequest.Failed += Fail; WebRequest.AllowRetryOnTimeout = false; + + WebRequest.AddHeader("x-api-version", API.APIVersion.ToString(CultureInfo.InvariantCulture)); + if (!string.IsNullOrEmpty(API.AccessToken)) WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}"); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 7131c3a7d4..f292e95bd1 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -33,6 +33,8 @@ namespace osu.Game.Online.API public string WebsiteRootUrl => "http://localhost"; + public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd")); + public Exception LastLoginError { get; private set; } /// diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index a97eae77e3..470d46cd7f 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -57,6 +57,11 @@ namespace osu.Game.Online.API /// string WebsiteRootUrl { get; } + /// + /// The version of the API. + /// + int APIVersion { get; } + /// /// The last login error that occurred, if any. /// From 39d64e779cd576f5e0a7e62705c7063ceb49930a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Feb 2022 19:15:09 +0900 Subject: [PATCH 162/306] Handle API returned difficulty range for rooms --- osu.Game/Online/Rooms/Room.cs | 13 ++++++++++++ .../Components/StarRatingRangeDisplay.cs | 21 ++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index bbe854f2dd..6722dac51d 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -37,6 +37,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("channel_id")] public readonly Bindable ChannelId = new Bindable(); + [JsonProperty("difficulty_range")] + public readonly Bindable DifficultyRange = new Bindable(); + [Cached] [JsonIgnore] public readonly Bindable Category = new Bindable(); @@ -228,5 +231,15 @@ namespace osu.Game.Online.Rooms public bool ShouldSerializeEndDate() => false; #endregion + + [JsonObject(MemberSerialization.OptIn)] + public class RoomDifficultyRange + { + [JsonProperty("min")] + public double Min; + + [JsonProperty("max")] + public double Max; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index 95ecadd21a..f27f5d6741 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -12,6 +12,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Online.Rooms; using osuTK; namespace osu.Game.Screens.OnlinePlay.Components @@ -71,6 +72,9 @@ namespace osu.Game.Screens.OnlinePlay.Components }; } + [Resolved] + private Room room { get; set; } + protected override void LoadComplete() { base.LoadComplete(); @@ -80,10 +84,21 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateRange(object sender, NotifyCollectionChangedEventArgs e) { - var orderedDifficulties = Playlist.Select(p => p.Beatmap).OrderBy(b => b.StarRating).ToArray(); + StarDifficulty minDifficulty; + StarDifficulty maxDifficulty; - StarDifficulty minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0); - StarDifficulty maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarRating : 0, 0); + if (room.DifficultyRange != null) + { + minDifficulty = new StarDifficulty(room.DifficultyRange.Value.Min, 0); + maxDifficulty = new StarDifficulty(room.DifficultyRange.Value.Max, 0); + } + else + { + var orderedDifficulties = Playlist.Select(p => p.Beatmap).OrderBy(b => b.StarRating).ToArray(); + + minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0); + maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarRating : 0, 0); + } minDisplay.Current.Value = minDifficulty; maxDisplay.Current.Value = maxDifficulty; From b43008b9f6ffc796e5927bf218230d9a4e806dc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 19:02:21 +0900 Subject: [PATCH 163/306] Add cover and count handling from newer response version --- osu.Game/Online/Rooms/Room.cs | 21 +++++++++++++++++++ .../Components/OnlinePlayBackgroundSprite.cs | 3 ++- .../Lounge/Components/PlaylistCountPill.cs | 13 +++++++----- .../Screens/OnlinePlay/OnlinePlayComposite.cs | 6 ++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 6722dac51d..db24eeea41 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -37,6 +37,14 @@ namespace osu.Game.Online.Rooms [JsonProperty("channel_id")] public readonly Bindable ChannelId = new Bindable(); + [JsonProperty("current_playlist_item")] + [Cached] + public readonly Bindable CurrentPlaylistItem = new Bindable(); + + [JsonProperty("playlist_item_stats")] + [Cached] + public readonly Bindable PlaylistItemStats = new Bindable(); + [JsonProperty("difficulty_range")] public readonly Bindable DifficultyRange = new Bindable(); @@ -232,6 +240,19 @@ namespace osu.Game.Online.Rooms #endregion + [JsonObject(MemberSerialization.OptIn)] + public class RoomPlaylistItemStats + { + [JsonProperty("count_active")] + public int CountActive; + + [JsonProperty("count_total")] + public int CountTotal; + + [JsonProperty("ruleset_ids")] + public int[] RulesetIDs; + } + [JsonObject(MemberSerialization.OptIn)] public class RoomDifficultyRange { diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs index d46ff12279..2faa46e622 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs @@ -23,6 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { InternalChild = sprite = CreateBackgroundSprite(); + CurrentPlaylistItem.BindValueChanged(_ => updateBeatmap()); Playlist.CollectionChanged += (_, __) => updateBeatmap(); updateBeatmap(); @@ -30,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateBeatmap() { - sprite.Beatmap.Value = Playlist.GetCurrentItem()?.Beatmap; + sprite.Beatmap.Value = CurrentPlaylistItem.Value?.Beatmap ?? Playlist.GetCurrentItem()?.Beatmap; } protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs index ef2c2df4a6..f387adfeb0 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs @@ -1,13 +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.Collections.Specialized; using Humanizer; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { @@ -41,15 +42,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { base.LoadComplete(); - Playlist.BindCollectionChanged(updateCount, true); + PlaylistItemStats.BindValueChanged(updateCount, true); } - private void updateCount(object sender, NotifyCollectionChangedEventArgs e) + private void updateCount(ValueChangedEvent valueChangedEvent) { + int activeItems = valueChangedEvent.NewValue.CountActive; + count.Clear(); - count.AddText(Playlist.Count.ToLocalisableString(), s => s.Font = s.Font.With(weight: FontWeight.Bold)); + count.AddText(activeItems.ToLocalisableString(), s => s.Font = s.Font.With(weight: FontWeight.Bold)); count.AddText(" "); - count.AddText("Beatmap".ToQuantity(Playlist.Count, ShowQuantityAs.None)); + count.AddText("Beatmap".ToQuantity(activeItems, ShowQuantityAs.None)); } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index c833621fbc..3148b74c9c 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -32,6 +32,12 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable Type { get; private set; } + [Resolved(typeof(Room))] + protected Bindable CurrentPlaylistItem { get; private set; } + + [Resolved(typeof(Room))] + protected Bindable PlaylistItemStats { get; private set; } + [Resolved(typeof(Room))] protected BindableList Playlist { get; private set; } From b5348e04077910c65f497a88b5287d402dcf6427 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 19:05:32 +0900 Subject: [PATCH 164/306] Update ruleset filtering to use newly provided array return --- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 9f917c978c..a521306d7e 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.RulesetID == criteria.Ruleset.OnlineID); + matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats.Value.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID); if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); From ffa5291b74b76c3083a4694a0a5961b631fba3d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 19:37:36 +0900 Subject: [PATCH 165/306] Add fallback handling for item count to support different request types --- .../OnlinePlay/Lounge/Components/PlaylistCountPill.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs index f387adfeb0..1aef41efbd 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs @@ -3,12 +3,10 @@ using Humanizer; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { @@ -42,12 +40,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { base.LoadComplete(); - PlaylistItemStats.BindValueChanged(updateCount, true); + PlaylistItemStats.BindValueChanged(_ => updateCount(), true); + Playlist.BindCollectionChanged((_, __) => updateCount(), true); } - private void updateCount(ValueChangedEvent valueChangedEvent) + private void updateCount() { - int activeItems = valueChangedEvent.NewValue.CountActive; + int activeItems = PlaylistItemStats.Value?.CountActive ?? Playlist.Count; count.Clear(); count.AddText(activeItems.ToLocalisableString(), s => s.Font = s.Font.With(weight: FontWeight.Bold)); From c7e9cf904bbd777bdc878f893fd37624a321e39f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 19:38:26 +0900 Subject: [PATCH 166/306] Fix incorrect null check on now-bindable `DifficultyRange` --- .../Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index f27f5d6741..de56e6ff86 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Components StarDifficulty minDifficulty; StarDifficulty maxDifficulty; - if (room.DifficultyRange != null) + if (room.DifficultyRange.Value != null) { minDifficulty = new StarDifficulty(room.DifficultyRange.Value.Min, 0); maxDifficulty = new StarDifficulty(room.DifficultyRange.Value.Max, 0); From 655b23f408c483afb04916e16bfd4b15ebfbaf3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 19:46:31 +0900 Subject: [PATCH 167/306] Update playlist room display to a three column layout Similar to the changes made to multiplayer. --- .../Playlists/PlaylistsRoomSubScreen.cs | 227 ++++++++++-------- 1 file changed, 125 insertions(+), 102 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 542851cb0f..338a9c856f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -69,132 +69,155 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Room.MaxAttempts.BindValueChanged(attempts => progressSection.Alpha = Room.MaxAttempts.Value != null ? 1 : 0, true); } - protected override Drawable CreateMainContent() => new GridContainer + protected override Drawable CreateMainContent() => new Container { RelativeSizeAxes = Axes.Both, - Content = new[] + Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, + Child = new GridContainer { - new Drawable[] + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - new Container + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 5 }, - Child = new GridContainer + // Playlist items column + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 5 }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { new OverlinedPlaylistHeader(), }, + new Drawable[] + { + new DrawableRoomPlaylist + { + RelativeSizeAxes = Axes.Both, + Items = { BindTarget = Room.Playlist }, + SelectedItem = { BindTarget = SelectedItem }, + AllowSelection = true, + AllowShowingResults = true, + RequestResults = item => + { + Debug.Assert(RoomId.Value != null); + ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item, false)); + } + } + }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + } + }, + // Spacer + null, + // Middle column (mods and leaderboard) + new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] { new OverlinedPlaylistHeader(), }, + new[] + { + UserModsSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Margin = new MarginPadding { Bottom = 10 }, + Children = new Drawable[] + { + new OverlinedHeader("Extra mods"), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, + } + } + } + }, + }, new Drawable[] { - new DrawableRoomPlaylist + progressSection = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, - Items = { BindTarget = Room.Playlist }, - SelectedItem = { BindTarget = SelectedItem }, - AllowSelection = true, - AllowShowingResults = true, - RequestResults = item => + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Margin = new MarginPadding { Bottom = 10 }, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Debug.Assert(RoomId.Value != null); - ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item, false)); + new OverlinedHeader("Progress"), + new RoomLocalUserInfo(), } - } + }, }, + new Drawable[] + { + new OverlinedHeader("Leaderboard") + }, + new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + } + }, + // Spacer + null, + // Main right column + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { new OverlinedHeader("Chat") }, + new Drawable[] { new MatchChatDisplay(Room) { RelativeSizeAxes = Axes.Both } } }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(), } - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new[] - { - UserModsSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Margin = new MarginPadding { Bottom = 10 }, - Children = new Drawable[] - { - new OverlinedHeader("Extra mods"), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } - } - } - }, - }, - new Drawable[] - { - progressSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Margin = new MarginPadding { Bottom = 10 }, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new OverlinedHeader("Progress"), - new RoomLocalUserInfo(), - } - }, - }, - new Drawable[] - { - new OverlinedHeader("Leaderboard") - }, - new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, }, - new Drawable[] { new OverlinedHeader("Chat"), }, - new Drawable[] { new MatchChatDisplay(Room) { RelativeSizeAxes = Axes.Both } } }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 120), - } }, }, - }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400), - new Dimension(), - new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600), } }; From 98c008b95f49ac3c1676619aa9195cce4ad46c54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Feb 2022 19:48:39 +0900 Subject: [PATCH 168/306] Fix test failures due to order change --- .../Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs | 2 +- osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 9a66a2b05c..93cd281bc5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBasicListChanges() { - AddStep("add rooms", () => RoomManager.AddRooms(5)); + AddStep("add rooms", () => RoomManager.AddRooms(5, withSpotlightRooms: true)); AddAssert("has 5 rooms", () => container.Rooms.Count == 5); diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 8dfd969c51..6abcb2924c 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay base.JoinRoom(room, password, onSuccess, onError); } - public void AddRooms(int count, RulesetInfo ruleset = null, bool withPassword = false) + public void AddRooms(int count, RulesetInfo ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) { for (int i = 0; i < count; i++) { @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay Name = { Value = $@"Room {currentRoomId}" }, Host = { Value = new APIUser { Username = @"Host" } }, EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }, - Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal }, + Category = { Value = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal }, }; if (withPassword) From 2aa0364092819f821cd1be69c4d56e2150a1218d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 00:14:33 +0900 Subject: [PATCH 169/306] Fix null reference in tests during attempted ruleset filtering --- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index a521306d7e..f57bff13ca 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats.Value.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID); + matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats.Value?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false; if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); From 113153e6a34becbae3934939172511b2f8ee591a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 00:25:00 +0900 Subject: [PATCH 170/306] Fix remaining filter tests --- osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 8dfd969c51..7ade173c9c 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -43,6 +43,11 @@ namespace osu.Game.Tests.Visual.OnlinePlay if (ruleset != null) { + room.PlaylistItemStats.Value = new Room.RoomPlaylistItemStats + { + RulesetIDs = new[] { ruleset.OnlineID }, + }; + room.Playlist.Add(new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID, From 8d70b85e41c8bba1aa2c056554c66a59d0002f56 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Mon, 21 Feb 2022 20:20:24 +0100 Subject: [PATCH 171/306] Revert changes --- osu.Android/OsuGameAndroid.cs | 35 ------------------------------- osu.Game/Screens/Menu/MainMenu.cs | 12 ----------- 2 files changed, 47 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 914a6f7502..050bf2b787 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -5,11 +5,7 @@ using System; using Android.App; using Android.OS; using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Platform; -using osu.Framework.Screens; using osu.Game; -using osu.Game.Screens.Menu; using osu.Game.Updater; using osu.Game.Utils; using Xamarin.Essentials; @@ -21,8 +17,6 @@ namespace osu.Android [Cached] private readonly OsuGameActivity gameActivity; - private readonly BindableBool allowExiting = new BindableBool(); - public OsuGameAndroid(OsuGameActivity activity) : base(null) { @@ -73,45 +67,16 @@ namespace osu.Android } } - public override void SetHost(GameHost host) - { - base.SetHost(host); - host.AllowExitingAndroid.AddSource(allowExiting); - } - protected override void LoadComplete() { base.LoadComplete(); LoadComponentAsync(new GameplayScreenRotationLocker(), Add); } - protected override void ScreenChanged(IScreen current, IScreen newScreen) - { - base.ScreenChanged(current, newScreen); - - switch (newScreen) - { - case MainMenu _: - // allow the MainMenu to (dis)allow exiting based on its ButtonSystemState. - allowExiting.Value = true; - break; - - default: - allowExiting.Value = false; - break; - } - } - protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo(); - protected override void Dispose(bool isDisposing) - { - Host.AllowExitingAndroid.RemoveSource(allowExiting); - base.Dispose(isDisposing); - } - private class AndroidBatteryInfo : BatteryInfo { public override double ChargeLevel => Battery.ChargeLevel; diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 2391903861..8b1bab52b3 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -70,16 +70,12 @@ namespace osu.Game.Screens.Menu private ParallaxContainer buttonsContainer; private SongTicker songTicker; - private readonly BindableBool allowExitingAndroid = new BindableBool(true); - [BackgroundDependencyLoader(true)] private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) { holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); loginDisplayed = statics.GetBindable(Static.LoginOverlayDisplayed); - host.AllowExitingAndroid.AddSource(allowExitingAndroid); - if (host.CanExit) { AddInternal(exitConfirmOverlay = new ExitConfirmOverlay @@ -138,8 +134,6 @@ namespace osu.Game.Screens.Menu ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine)); break; } - - allowExitingAndroid.Value = state == ButtonSystemState.Initial; }; buttons.OnSettings = () => settings?.ToggleVisibility(); @@ -303,11 +297,5 @@ namespace osu.Game.Screens.Menu Schedule(loadSoloSongSelect); } - - protected override void Dispose(bool isDisposing) - { - host.AllowExitingAndroid.RemoveSource(allowExitingAndroid); - base.Dispose(isDisposing); - } } } From 3eee505aa26569059ca0fddf35c202518a484453 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Mon, 21 Feb 2022 20:24:17 +0100 Subject: [PATCH 172/306] Update "exit" flow when pressing back on Android --- osu.Game/Screens/Menu/ExitConfirmOverlay.cs | 23 ++++++++++++++++++ osu.Game/Screens/Menu/MainMenu.cs | 27 +++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs index a90b83c5fe..253ec17c28 100644 --- a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs +++ b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs @@ -3,6 +3,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -42,4 +43,26 @@ namespace osu.Game.Screens.Menu } } } + + /// + /// An that behaves as if the is always 0. + /// + /// This is useful for mobile devices using gesture navigation, where holding to confirm is not possible. + public class NoHoldExitConfirmOverlay : ExitConfirmOverlay, IKeyBindingHandler + { + public new bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) + return false; + + if (e.Action == GlobalAction.Back) + { + Progress.Value = 1; + Confirm(); + return true; + } + + return false; + } + } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 8b1bab52b3..7bc0cb48bf 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; @@ -89,6 +90,14 @@ namespace osu.Game.Screens.Menu } }); } + else if (host.CanSuspendToBackground) + { + AddInternal(exitConfirmOverlay = new NoHoldExitConfirmOverlay + { + // treat as if the UIHoldActivationDelay is always 0. see NoHoldExitConfirmOverlay xmldoc for more info. + Action = this.Exit + }); + } AddRangeInternal(new[] { @@ -148,6 +157,24 @@ namespace osu.Game.Screens.Menu private void confirmAndExit() { + if (host.CanSuspendToBackground) + { + // cancel the overlay as we're not actually exiting. + // this is the same action as 'onCancel' in `ConfirmExitDialog`. + exitConfirmOverlay.Abort(); + + // fade the track so the Bass.Pause() on suspend isn't as jarring. + const double fade_time = 500; + musicController.CurrentTrack + .VolumeTo(0, fade_time, Easing.Out).Then() + .VolumeTo(1, fade_time, Easing.In); + + host.SuspendToBackground(); + + // on hosts that can only suspend, we don't ever want to exit the game. + return; + } + if (exitConfirmed) return; exitConfirmed = true; From cd3641137bc920ef042396125c0c323fd7875fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Feb 2022 00:02:54 +0100 Subject: [PATCH 173/306] Add `OsuColour` method mapping colours from basic theme to mod types --- osu.Game/Graphics/OsuColour.cs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 886ba7ef92..afedf36cad 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Beatmaps; using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Utils; @@ -157,6 +158,36 @@ namespace osu.Game.Graphics } } + /// + /// Retrieves the main accent colour for a . + /// + public Color4 ForModType(ModType modType) + { + switch (modType) + { + case ModType.Automation: + return Blue1; + + case ModType.DifficultyIncrease: + return Red1; + + case ModType.DifficultyReduction: + return Lime1; + + case ModType.Conversion: + return Purple1; + + case ModType.Fun: + return Pink1; + + case ModType.System: + return Gray7; + + default: + throw new ArgumentOutOfRangeException(nameof(modType), modType, "Unknown mod type"); + } + } + /// /// Returns a foreground text colour that is supposed to contrast well with /// the supplied . From 5186693dad42b370b115da9a304b3fc7873470a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Feb 2022 00:03:07 +0100 Subject: [PATCH 174/306] Implement tiny mod switch --- .../UserInterface/TestSceneModSwitchTiny.cs | 85 +++++++++++++++++ osu.Game/Rulesets/UI/ModSwitchTiny.cs | 91 +++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs create mode 100644 osu.Game/Rulesets/UI/ModSwitchTiny.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs new file mode 100644 index 0000000000..dbde7ce425 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs @@ -0,0 +1,85 @@ +// 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 NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneModSwitchTiny : OsuTestScene + { + [Test] + public void TestOsu() => createSwitchTestFor(new OsuRuleset()); + + [Test] + public void TestTaiko() => createSwitchTestFor(new TaikoRuleset()); + + [Test] + public void TestCatch() => createSwitchTestFor(new CatchRuleset()); + + [Test] + public void TestMania() => createSwitchTestFor(new ManiaRuleset()); + + private void createSwitchTestFor(Ruleset ruleset) + { + AddStep("no colour scheme", () => Child = createContent(ruleset, null)); + + foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast()) + { + AddStep($"{scheme} colour scheme", () => Child = createContent(ruleset, scheme)); + } + + AddToggleStep("toggle active", active => this.ChildrenOfType().ForEach(s => s.Active.Value = active)); + } + + private static Drawable createContent(Ruleset ruleset, OverlayColourScheme? colourScheme) + { + var switchFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Padding = new MarginPadding(20), + ChildrenEnumerable = ruleset.CreateAllMods() + .GroupBy(mod => mod.Type) + .Select(group => new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Spacing = new Vector2(5), + ChildrenEnumerable = group.Select(mod => new ModSwitchTiny(mod)) + }) + }; + + if (colourScheme != null) + { + return new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(OverlayColourProvider), new OverlayColourProvider(colourScheme.Value)) + }, + Child = switchFlow + }; + } + + return switchFlow; + } + } +} diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs new file mode 100644 index 0000000000..093c2271b6 --- /dev/null +++ b/osu.Game/Rulesets/UI/ModSwitchTiny.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; +using osuTK; +using osuTK.Graphics; + +#nullable enable + +namespace osu.Game.Rulesets.UI +{ + public class ModSwitchTiny : CompositeDrawable + { + public BindableBool Active { get; } = new BindableBool(); + + private readonly IMod mod; + + private readonly Box background; + private readonly OsuSpriteText acronymText; + + private Color4 activeForegroundColour; + private Color4 inactiveForegroundColour; + + private Color4 activeBackgroundColour; + private Color4 inactiveBackgroundColour; + + public ModSwitchTiny(IMod mod) + { + this.mod = mod; + Size = new Vector2(73, 30); + + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + acronymText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = false, + Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black), + Text = mod.Acronym, + Margin = new MarginPadding + { + Top = 4 + } + } + } + }; + } + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, OverlayColourProvider? colourProvider) + { + inactiveBackgroundColour = colourProvider?.Background5 ?? colours.Gray3; + activeBackgroundColour = colours.ForModType(mod.Type); + + inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5; + activeForegroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Active.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + private void updateState() + { + acronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint); + background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint); + } + } +} From cfc41a0a366cc0c311cbee0cf1aa9ea37d87784a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Feb 2022 00:03:12 +0100 Subject: [PATCH 175/306] Implement small mod switch --- .../UserInterface/TestSceneModSwitchSmall.cs | 85 ++++++++++++++ osu.Game/Rulesets/UI/ModSwitchSmall.cs | 109 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs create mode 100644 osu.Game/Rulesets/UI/ModSwitchSmall.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs new file mode 100644 index 0000000000..447352b7a6 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchSmall.cs @@ -0,0 +1,85 @@ +// 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 NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneModSwitchSmall : OsuTestScene + { + [Test] + public void TestOsu() => createSwitchTestFor(new OsuRuleset()); + + [Test] + public void TestTaiko() => createSwitchTestFor(new TaikoRuleset()); + + [Test] + public void TestCatch() => createSwitchTestFor(new CatchRuleset()); + + [Test] + public void TestMania() => createSwitchTestFor(new ManiaRuleset()); + + private void createSwitchTestFor(Ruleset ruleset) + { + AddStep("no colour scheme", () => Child = createContent(ruleset, null)); + + foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast()) + { + AddStep($"{scheme} colour scheme", () => Child = createContent(ruleset, scheme)); + } + + AddToggleStep("toggle active", active => this.ChildrenOfType().ForEach(s => s.Active.Value = active)); + } + + private static Drawable createContent(Ruleset ruleset, OverlayColourScheme? colourScheme) + { + var switchFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Padding = new MarginPadding(20), + ChildrenEnumerable = ruleset.CreateAllMods() + .GroupBy(mod => mod.Type) + .Select(group => new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Spacing = new Vector2(5), + ChildrenEnumerable = group.Select(mod => new ModSwitchSmall(mod)) + }) + }; + + if (colourScheme != null) + { + return new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(OverlayColourProvider), new OverlayColourProvider(colourScheme.Value)) + }, + Child = switchFlow + }; + } + + return switchFlow; + } + } +} diff --git a/osu.Game/Rulesets/UI/ModSwitchSmall.cs b/osu.Game/Rulesets/UI/ModSwitchSmall.cs new file mode 100644 index 0000000000..5d9d9075bc --- /dev/null +++ b/osu.Game/Rulesets/UI/ModSwitchSmall.cs @@ -0,0 +1,109 @@ +// 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.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; +using osuTK; +using osuTK.Graphics; + +#nullable enable + +namespace osu.Game.Rulesets.UI +{ + public class ModSwitchSmall : CompositeDrawable + { + public BindableBool Active { get; } = new BindableBool(); + + private readonly IMod mod; + + private const float size = 60; + + private readonly SpriteIcon background; + private readonly SpriteIcon? modIcon; + + private Color4 activeForegroundColour; + private Color4 inactiveForegroundColour; + + private Color4 activeBackgroundColour; + private Color4 inactiveBackgroundColour; + + public ModSwitchSmall(IMod mod) + { + this.mod = mod; + + AutoSizeAxes = Axes.Both; + + FillFlowContainer contentFlow; + ModSwitchTiny tinySwitch; + + InternalChildren = new Drawable[] + { + background = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(size), + Icon = OsuIcon.ModBg + }, + contentFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(0, 4), + Direction = FillDirection.Vertical, + Child = tinySwitch = new ModSwitchTiny(mod) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Scale = new Vector2(0.6f), + Active = { BindTarget = Active } + } + } + }; + + if (mod.Icon != null) + { + contentFlow.Insert(-1, modIcon = new SpriteIcon + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(21), + Icon = mod.Icon.Value + }); + tinySwitch.Scale = new Vector2(0.3f); + } + } + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, OverlayColourProvider? colourProvider) + { + inactiveForegroundColour = colourProvider?.Background5 ?? colours.Gray3; + activeForegroundColour = colours.ForModType(mod.Type); + + inactiveBackgroundColour = colourProvider?.Background2 ?? colours.Gray5; + activeBackgroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Active.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + private void updateState() + { + modIcon?.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint); + background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint); + } + } +} From 0d56693b7aee6221d16ac2341d86d70b5a805a55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:11:22 +0900 Subject: [PATCH 176/306] Fix test not always checking the final bonus value Due to the previous logic not waiting until the spinner had completed, there could be false negatives as the check runs too early, with a potential additional bonus spin occurring afterwards. --- .../Mods/TestSceneOsuModSpunOut.cs | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index e71377a505..cb8eceb213 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -8,13 +8,15 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; -using osu.Game.Screens.Play; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -66,25 +68,45 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestSpinnerOnlyComplete() => CreateModTest(new ModTestData + public void TestSpinnerGetsNoBonusScore() { - Mod = new OsuModSpunOut(), - Autoplay = false, - Beatmap = singleSpinnerBeatmap, - PassCondition = () => + DrawableSpinner spinner = null; + List results = new List(); + + CreateModTest(new ModTestData { - var spinner = Player.ChildrenOfType().SingleOrDefault(); - var gameplayClockContainer = Player.ChildrenOfType().SingleOrDefault(); + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => + { + // Bind to the first spinner's results for further tracking. + if (spinner == null) + { + // We only care about the first spinner we encounter for this test. + var nextSpinner = Player.ChildrenOfType().SingleOrDefault(); - if (spinner == null || gameplayClockContainer == null) - return false; + if (nextSpinner == null) + return false; - if (!Precision.AlmostEquals(gameplayClockContainer.CurrentTime, spinner.HitObject.StartTime + spinner.HitObject.Duration, 200.0f)) - return false; + spinner = nextSpinner; + spinner.OnNewResult += (o, result) => results.Add(result); - return Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); - } - }); + results.Clear(); + } + + // we should only be checking the bonus/progress after the spinner has fully completed. + if (!results.OfType().Any(r => r.TimeCompleted != null)) + return false; + + return + results.Any(r => r.Type == HitResult.SmallTickHit) + && !results.Any(r => r.Type == HitResult.LargeTickHit) + && Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) + && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); + } + }); + } private Beatmap singleSpinnerBeatmap => new Beatmap { From 91acc9eec6a8859f665f64338420efc564faf33b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:36:08 +0900 Subject: [PATCH 177/306] Remove checks which are still going to occasionally fail due to pooling --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index cb8eceb213..93c4bd96de 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -101,9 +101,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods return results.Any(r => r.Type == HitResult.SmallTickHit) - && !results.Any(r => r.Type == HitResult.LargeTickHit) - && Precision.AlmostEquals(spinner.Progress, 1.0f, 0.05f) - && Precision.AlmostEquals(spinner.GainedBonus.Value, 0, 1); + && !results.Any(r => r.Type == HitResult.LargeTickHit); } }); } From 9e279c3ebc9cdc23ba5d4a1d192642b314019de8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:37:52 +0900 Subject: [PATCH 178/306] Fix completely incorrect judgement specification --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 93c4bd96de..e61720d8b9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -100,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods return false; return - results.Any(r => r.Type == HitResult.SmallTickHit) - && !results.Any(r => r.Type == HitResult.LargeTickHit); + results.Any(r => r.Type == HitResult.SmallBonus) + && !results.Any(r => r.Type == HitResult.LargeBonus); } }); } From d6032a2335e0de00dae43ce7f6fcef95ffbbe50e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:55:52 +0900 Subject: [PATCH 179/306] Fix beatmap overlay not re-fetching results after login --- .../BeatmapListing/BeatmapListingFilterControl.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 157753c09f..0f87f04270 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -67,6 +67,8 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private IAPIProvider api { get; set; } + private IBindable apiUser; + public BeatmapListingFilterControl() { RelativeSizeAxes = Axes.X; @@ -127,7 +129,7 @@ namespace osu.Game.Overlays.BeatmapListing } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, IAPIProvider api) { sortControlBackground.Colour = colourProvider.Background4; } @@ -161,6 +163,9 @@ namespace osu.Game.Overlays.BeatmapListing sortCriteria.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch()); + + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(_ => queueUpdateSearch()); } public void TakeFocus() => searchControl.TakeFocus(); @@ -190,6 +195,9 @@ namespace osu.Game.Overlays.BeatmapListing resetSearch(); + if (!api.IsLoggedIn) + return; + queryChangedDebounce = Scheduler.AddDelayed(() => { resetSearch(); From e744840d07aec8f0f6a75abb81ba1bc9dd9dbba8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 14:56:04 +0900 Subject: [PATCH 180/306] Ensure old results are cleared from beatmap overlay on logout --- osu.Game/Overlays/BeatmapListingOverlay.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index fbed234cc7..3476968ded 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Localisation; using osu.Framework.Graphics; @@ -19,6 +20,7 @@ using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Containers; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapListing; using osu.Game.Resources.Localisation.Web; @@ -32,6 +34,11 @@ namespace osu.Game.Overlays [Resolved] private PreviewTrackManager previewTrackManager { get; set; } + [Resolved] + private IAPIProvider api { get; set; } + + private IBindable apiUser; + private Drawable currentContent; private Container panelTarget; private FillFlowContainer foundContent; @@ -93,6 +100,13 @@ namespace osu.Game.Overlays { base.LoadComplete(); filterControl.CardSize.BindValueChanged(_ => onCardSizeChanged()); + + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(_ => + { + if (api.IsLoggedIn) + addContentToResultsArea(Drawable.Empty()); + }); } public void ShowWithSearch(string query) From cde3d9c08ba8e3b9af8e8e6192c9febdfa306baa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 15:15:57 +0900 Subject: [PATCH 181/306] Change precedence order to favour playlist as a source for beatmap count --- .../OnlinePlay/Lounge/Components/PlaylistCountPill.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs index 1aef41efbd..e21439e931 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.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 Humanizer; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; @@ -46,7 +47,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void updateCount() { - int activeItems = PlaylistItemStats.Value?.CountActive ?? Playlist.Count; + int activeItems = Playlist.Count > 0 || PlaylistItemStats.Value == null + // For now, use the playlist as the source of truth if it has any items. + // This allows the count to display correctly on the room screen (after joining a room). + ? Playlist.Count(i => !i.Expired) + : PlaylistItemStats.Value.CountActive; count.Clear(); count.AddText(activeItems.ToLocalisableString(), s => s.Font = s.Font.With(weight: FontWeight.Bold)); From f12044b03e97da4bbb883dd026c267f796345fe6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 15:31:08 +0900 Subject: [PATCH 182/306] Add mention of `PlaylistItem.Beatmap` being a placeholder in many cases --- osu.Game/Online/Rooms/PlaylistItem.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 33718f050b..f696362cbb 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -62,6 +62,10 @@ namespace osu.Game.Online.Rooms [JsonProperty("beatmap_id")] private int onlineBeatmapId => Beatmap.OnlineID; + /// + /// A beatmap representing this playlist item. + /// In many cases, this will *not* contain any usable information apart from OnlineID. + /// [JsonIgnore] public IBeatmapInfo Beatmap { get; set; } = null!; From 057fd6c352c354367ba5056dc7ed8c92976391de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 15:37:42 +0900 Subject: [PATCH 183/306] Add mention of `StarRatingRangeDisplay` fallback scenario being wrong for multiplayer --- .../Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index de56e6ff86..0b673006ef 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -94,6 +94,8 @@ namespace osu.Game.Screens.OnlinePlay.Components } else { + // In multiplayer rooms, the beatmaps of playlist items will not be populated to a point this can be correct. + // Either populating them via BeatmapLookupCache or polling the API for the room's DifficultyRange will be required. var orderedDifficulties = Playlist.Select(p => p.Beatmap).OrderBy(b => b.StarRating).ToArray(); minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0); From 347a2346b9d73352a482b882d81ad49cf8ed4176 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 15:40:30 +0900 Subject: [PATCH 184/306] Fix `TestSceneEditorSaving` not waiting for timeline load As seen at https://github.com/ppy/osu/runs/5276431764?check_suite_focus=true. --- osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index adaa24d542..d1c1558003 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -49,6 +49,8 @@ namespace osu.Game.Tests.Visual.Editing double originalTimelineZoom = 0; double changedTimelineZoom = 0; + AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + AddStep("Set beat divisor", () => Editor.Dependencies.Get().Value = 16); AddStep("Set timeline zoom", () => { From 61b3280de157b86d6b6ad61558902c491e617fc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 15:47:00 +0900 Subject: [PATCH 185/306] Add missing property copies in `Room.CopyFrom` implementation --- osu.Game/Online/Rooms/Room.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index d24cb7e74b..8ed37ad6b5 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -194,6 +194,8 @@ namespace osu.Game.Online.Rooms EndDate.Value = other.EndDate.Value; UserScore.Value = other.UserScore.Value; QueueMode.Value = other.QueueMode.Value; + DifficultyRange.Value = other.DifficultyRange.Value; + PlaylistItemStats.Value = other.PlaylistItemStats.Value; if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); From ca0a04115374368184ca8363ac7026384b9c9dcf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 16:45:04 +0900 Subject: [PATCH 186/306] Fix missing escaping causing test failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 4c477d58b6..39a51876c2 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -269,7 +269,7 @@ namespace osu.Game.Online.Chat handleAdvanced(advanced_link_regex, result, startIndex); // handle editor times - handleMatches(time_regex, "{0}", $"{OsuGameBase.OSU_PROTOCOL}edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp); + handleMatches(time_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); // handle channels handleMatches(channel_regex, "{0}", $"{OsuGameBase.OSU_PROTOCOL}chan/{0}", result, startIndex, LinkAction.OpenChannel); From ed008267d7a932c64dac5a3566af1bcdd145e415 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 16:45:18 +0900 Subject: [PATCH 187/306] Fix one more case of escaping not being present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 39a51876c2..333c554d44 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -272,7 +272,7 @@ namespace osu.Game.Online.Chat handleMatches(time_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); // handle channels - handleMatches(channel_regex, "{0}", $"{OsuGameBase.OSU_PROTOCOL}chan/{0}", result, startIndex, LinkAction.OpenChannel); + handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); string empty = ""; while (space-- > 0) From 5efffa208acc118d8acf16cae9a609318e85b7b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 17:08:09 +0900 Subject: [PATCH 188/306] Add test coverage of beatmap set overlay actually showing requested beatmap --- .../TestSceneStartupBeatmapDisplay.cs | 34 ++++++++++++++++++- .../TestSceneStartupBeatmapSetDisplay.cs | 32 ++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs index 1efa24435e..961b7dedc3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs @@ -5,18 +5,50 @@ using System.Linq; using NUnit.Framework; 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.Overlays; +using osu.Game.Overlays.BeatmapSet; namespace osu.Game.Tests.Visual.Navigation { public class TestSceneStartupBeatmapDisplay : OsuGameTestScene { - protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { "osu://b/75" }); + private const int requested_beatmap_id = 75; + private const int requested_beatmap_set_id = 1; + + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { $"osu://b/{requested_beatmap_id}" }); + + [SetUp] + public void Setup() => Schedule(() => + { + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapSetRequest gbr: + var apiBeatmapSet = CreateAPIBeatmapSet(); + apiBeatmapSet.OnlineID = requested_beatmap_set_id; + apiBeatmapSet.Beatmaps = apiBeatmapSet.Beatmaps.Append(new APIBeatmap + { + DifficultyName = "Target difficulty", + OnlineID = requested_beatmap_id, + }).ToArray(); + + gbr.TriggerSuccess(apiBeatmapSet); + return true; + } + + return false; + }; + }); [Test] public void TestBeatmapLink() { AddUntilStep("Beatmap overlay displayed", () => Game.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + AddUntilStep("Beatmap overlay showing content", () => Game.ChildrenOfType().FirstOrDefault()?.Beatmap.Value.OnlineID == requested_beatmap_id); } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs index 1339c514e4..1aa56896d3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapSetDisplay.cs @@ -5,18 +5,48 @@ using System.Linq; using NUnit.Framework; 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.Overlays; namespace osu.Game.Tests.Visual.Navigation { public class TestSceneStartupBeatmapSetDisplay : OsuGameTestScene { - protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { "osu://s/1" }); + private const int requested_beatmap_set_id = 1; + + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { $"osu://s/{requested_beatmap_set_id}" }); + + [SetUp] + public void Setup() => Schedule(() => + { + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapSetRequest gbr: + + var apiBeatmapSet = CreateAPIBeatmapSet(); + apiBeatmapSet.OnlineID = requested_beatmap_set_id; + apiBeatmapSet.Beatmaps = apiBeatmapSet.Beatmaps.Append(new APIBeatmap + { + DifficultyName = "Target difficulty", + OnlineID = 75, + }).ToArray(); + gbr.TriggerSuccess(apiBeatmapSet); + return true; + } + + return false; + }; + }); [Test] public void TestBeatmapSetLink() { AddUntilStep("Beatmap overlay displayed", () => Game.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + AddUntilStep("Beatmap overlay showing content", () => Game.ChildrenOfType().FirstOrDefault()?.Header.BeatmapSet.Value.OnlineID == requested_beatmap_set_id); } } } From 6de4e05e492aa7ff03e068000ffdf83cb2dd65d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 17:14:00 +0900 Subject: [PATCH 189/306] Fix current selection not being correctly maintained when `BeatmapPicker` updates its display --- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 59e8e8db3c..031442814d 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -183,7 +183,14 @@ namespace osu.Game.Overlays.BeatmapSet } starRatingContainer.FadeOut(100); - Beatmap.Value = Difficulties.FirstOrDefault()?.Beatmap; + + // If a selection is already made, try and maintain it. + if (Beatmap.Value != null) + Beatmap.Value = Difficulties.FirstOrDefault(b => b.Beatmap.OnlineID == Beatmap.Value.OnlineID)?.Beatmap; + + // Else just choose the first available difficulty for now. + Beatmap.Value ??= Difficulties.FirstOrDefault()?.Beatmap; + plays.Value = BeatmapSet?.PlayCount ?? 0; favourites.Value = BeatmapSet?.FavouriteCount ?? 0; From d1d6847d3205d5ea1226dc063d7315b95cd47491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Feb 2022 22:24:37 +0100 Subject: [PATCH 190/306] Add comment about split usage in osu:// protocol link handling --- osu.Game/OsuGame.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 390e96d768..77eec004c0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -921,6 +921,9 @@ namespace osu.Game if (!url.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) throw new ArgumentException("Invalid osu URL provided.", nameof(url)); + // note that `StringSplitOptions.RemoveEmptyEntries` is not explicitly specified here + // in order to ensure that the protocol URL is valid (i.e. it has two slashes in the `osu://` part, + // causing the link content to be stored to the 2nd index rather than the 1st). string[] pieces = url.Split('/'); switch (pieces[2]) From d8fa443ea0eb21f607253aa9d8de679d94a51800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Feb 2022 23:22:00 +0100 Subject: [PATCH 191/306] Extract default mod switch measurements to constants For use later when specific sizes/scales of the mod switches are desired. --- osu.Game/Rulesets/UI/ModSwitchSmall.cs | 6 +++--- osu.Game/Rulesets/UI/ModSwitchTiny.cs | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModSwitchSmall.cs b/osu.Game/Rulesets/UI/ModSwitchSmall.cs index 5d9d9075bc..676bbac95c 100644 --- a/osu.Game/Rulesets/UI/ModSwitchSmall.cs +++ b/osu.Game/Rulesets/UI/ModSwitchSmall.cs @@ -21,9 +21,9 @@ namespace osu.Game.Rulesets.UI { public BindableBool Active { get; } = new BindableBool(); - private readonly IMod mod; + public const float DEFAULT_SIZE = 60; - private const float size = 60; + private readonly IMod mod; private readonly SpriteIcon background; private readonly SpriteIcon? modIcon; @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.UI { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(size), + Size = new Vector2(DEFAULT_SIZE), Icon = OsuIcon.ModBg }, contentFlow = new FillFlowContainer diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs index 093c2271b6..b1d453f588 100644 --- a/osu.Game/Rulesets/UI/ModSwitchTiny.cs +++ b/osu.Game/Rulesets/UI/ModSwitchTiny.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.UI { public BindableBool Active { get; } = new BindableBool(); + public const float DEFAULT_HEIGHT = 30; + private readonly IMod mod; private readonly Box background; @@ -36,7 +38,7 @@ namespace osu.Game.Rulesets.UI public ModSwitchTiny(IMod mod) { this.mod = mod; - Size = new Vector2(73, 30); + Size = new Vector2(73, DEFAULT_HEIGHT); InternalChild = new CircularContainer { From 7bdcb5952ed67bc895da12833767bc3746326d85 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Wed, 23 Feb 2022 00:36:56 +0100 Subject: [PATCH 192/306] Fix handling badly-formatted osu:// urls --- osu.Game/Online/Chat/MessageFormatter.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index d7974004b1..3517777325 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -236,8 +236,7 @@ namespace osu.Game.Online.Chat break; default: - linkType = LinkAction.External; - break; + return new LinkDetails(LinkAction.External, url); } return new LinkDetails(linkType, args[2]); From 2bea485af8da9bddd5fe404dd71e967a3c9b631b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Feb 2022 13:37:45 +0900 Subject: [PATCH 193/306] Fix currently playing text not showing in lounge --- osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 3148b74c9c..7d1feb0316 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -91,9 +91,15 @@ namespace osu.Game.Screens.OnlinePlay Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true); } - protected virtual void UpdateSelectedItem() - => SelectedItem.Value = RoomID.Value == null || subScreenSelectedItem == null - ? Playlist.GetCurrentItem() - : subScreenSelectedItem.Value; + protected void UpdateSelectedItem() + { + if (RoomID.Value == null || subScreenSelectedItem == null) + { + SelectedItem.Value = CurrentPlaylistItem.Value ?? Playlist.GetCurrentItem(); + return; + } + + SelectedItem.Value = subScreenSelectedItem.Value; + } } } From 71a012bea6021ad559da8ccce2f92413bff80c21 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Feb 2022 13:42:47 +0900 Subject: [PATCH 194/306] Don't update count twice immediately MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs index e21439e931..a6bbcd548d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { base.LoadComplete(); - PlaylistItemStats.BindValueChanged(_ => updateCount(), true); + PlaylistItemStats.BindValueChanged(_ => updateCount()); Playlist.BindCollectionChanged((_, __) => updateCount(), true); } From 87da650dfb5651c608562865a37db35a0e04c32e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 13:51:25 +0900 Subject: [PATCH 195/306] Update framework --- osu.Android.props | 2 +- osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 24a0d20874..526ce959a6 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index cd675e467b..1107089a46 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { - var dllStore = new DllResourceStore(DynamicCompilationOriginal.GetType().Assembly); + var dllStore = new DllResourceStore(GetType().Assembly); metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), this, true); defaultSkin = new DefaultLegacySkin(this); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4c6f81defa..7dfd099df1 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 99b9de3fe2..9d0e1790f0 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From a6b6644c2e441513bfba493fbe56e307996bd4ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 16:22:13 +0900 Subject: [PATCH 196/306] Replace LINQ queries with recommendations --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index e61720d8b9..70b7d1f740 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -96,12 +96,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } // we should only be checking the bonus/progress after the spinner has fully completed. - if (!results.OfType().Any(r => r.TimeCompleted != null)) + if (results.OfType().All(r => r.TimeCompleted == null)) return false; return results.Any(r => r.Type == HitResult.SmallBonus) - && !results.Any(r => r.Type == HitResult.LargeBonus); + && results.All(r => r.Type != HitResult.LargeBonus); } }); } From 054ed546e3800e6aa0425b362b68dacf9e19fb75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 16:56:50 +0900 Subject: [PATCH 197/306] Fix intermittent failures in remaining test method --- .../Mods/TestSceneOsuModSpunOut.cs | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 70b7d1f740..a8953c1a6f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -26,13 +26,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods protected override bool AllowFail => true; [Test] - public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData + public void TestSpinnerAutoCompleted() { - Mod = new OsuModSpunOut(), - Autoplay = false, - Beatmap = singleSpinnerBeatmap, - PassCondition = () => Player.ChildrenOfType().SingleOrDefault()?.Progress >= 1 - }); + DrawableSpinner spinner = null; + JudgementResult lastResult = null; + + CreateModTest(new ModTestData + { + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => + { + // Bind to the first spinner's results for further tracking. + if (spinner == null) + { + // We only care about the first spinner we encounter for this test. + var nextSpinner = Player.ChildrenOfType().SingleOrDefault(); + + if (nextSpinner == null) + return false; + + lastResult = null; + + spinner = nextSpinner; + spinner.OnNewResult += (o, result) => lastResult = result; + } + + return lastResult?.Type == HitResult.Great; + } + }); + } [TestCase(null)] [TestCase(typeof(OsuModDoubleTime))] From 5d73691de4e13ed827d0a731ea909e7b33d115d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 17:02:39 +0900 Subject: [PATCH 198/306] Use existing `HandleLink` flow rather than reimplmenting --- osu.Game/OsuGame.cs | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 77eec004c0..fa5a336b7c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -906,7 +906,7 @@ namespace osu.Game if (firstPath.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) { - handleOsuProtocolUrl(firstPath); + HandleLink(firstPath); } else { @@ -916,30 +916,6 @@ namespace osu.Game } } - private void handleOsuProtocolUrl(string url) - { - if (!url.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal)) - throw new ArgumentException("Invalid osu URL provided.", nameof(url)); - - // note that `StringSplitOptions.RemoveEmptyEntries` is not explicitly specified here - // in order to ensure that the protocol URL is valid (i.e. it has two slashes in the `osu://` part, - // causing the link content to be stored to the 2nd index rather than the 1st). - string[] pieces = url.Split('/'); - - switch (pieces[2]) - { - case "s": - if (int.TryParse(pieces[3], out int beatmapSetId)) - ShowBeatmapSet(beatmapSetId); - break; - - case "b": - if (int.TryParse(pieces[3], out int beatmapId)) - ShowBeatmap(beatmapId); - break; - } - } - private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays) { otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); From 28c9c5ab6a0ac889223a7f46ba6b16504cc9a3ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 17:05:46 +0900 Subject: [PATCH 199/306] Remove unnecessary `ShouldSerialize` rules in `Room` --- osu.Game/Online/Rooms/Room.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 8ed37ad6b5..7512d4fcf9 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -230,15 +230,9 @@ namespace osu.Game.Online.Rooms // They rely on being named exactly the same as the corresponding fields (casing included) and as such should NOT be renamed // unless the fields are also renamed. - [UsedImplicitly] - public bool ShouldSerializeRoomID() => false; - [UsedImplicitly] public bool ShouldSerializeHost() => false; - [UsedImplicitly] - public bool ShouldSerializeEndDate() => false; - #endregion [JsonObject(MemberSerialization.OptIn)] From f14a9af801c53552356f798a74e68fb6a18b4a3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 17:06:40 +0900 Subject: [PATCH 200/306] Make `Room` opt-in rather than opt-out for json serialization --- osu.Game/Online/Rooms/Room.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 7512d4fcf9..5deedaaa6b 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -15,6 +15,7 @@ using osu.Game.Utils; namespace osu.Game.Online.Rooms { + [JsonObject(MemberSerialization.OptIn)] public class Room : IDeepCloneable { [Cached] @@ -49,7 +50,6 @@ namespace osu.Game.Online.Rooms public readonly Bindable DifficultyRange = new Bindable(); [Cached] - [JsonIgnore] public readonly Bindable Category = new Bindable(); // Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106) @@ -62,19 +62,15 @@ namespace osu.Game.Online.Rooms } [Cached] - [JsonIgnore] public readonly Bindable MaxAttempts = new Bindable(); [Cached] - [JsonIgnore] public readonly Bindable Status = new Bindable(new RoomStatusOpen()); [Cached] - [JsonIgnore] public readonly Bindable Availability = new Bindable(); [Cached] - [JsonIgnore] public readonly Bindable Type = new Bindable(); // Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106) @@ -87,7 +83,6 @@ namespace osu.Game.Online.Rooms } [Cached] - [JsonIgnore] public readonly Bindable QueueMode = new Bindable(); [JsonConverter(typeof(SnakeCaseStringEnumConverter))] @@ -99,7 +94,6 @@ namespace osu.Game.Online.Rooms } [Cached] - [JsonIgnore] public readonly Bindable MaxParticipants = new Bindable(); [Cached] @@ -124,7 +118,6 @@ namespace osu.Game.Online.Rooms public readonly Bindable Password = new Bindable(); [Cached] - [JsonIgnore] public readonly Bindable Duration = new Bindable(); [JsonProperty("duration")] From 43c83d2de1cd2cb861ba57f80dc903c837cb8181 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 17:07:29 +0900 Subject: [PATCH 201/306] Add note about why `RoomID` is nulled in `DeepClone` --- osu.Game/Online/Rooms/Room.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 5deedaaa6b..b301c81921 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -162,6 +162,8 @@ namespace osu.Game.Online.Rooms var copy = new Room(); copy.CopyFrom(this); + + // ID must be unset as we use this as a marker for whether this is a client-side (not-yet-created) room or not. copy.RoomID.Value = null; return copy; From 53bbd0067545fbfc99e131de0a1bb6918980ed4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Feb 2022 17:12:38 +0900 Subject: [PATCH 202/306] Also make `APIUser` opt-in and remove the remaining serialization exclusion rule --- osu.Game/Online/API/Requests/Responses/APIUser.cs | 1 + osu.Game/Online/Rooms/Room.cs | 12 ------------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 2b64e5de06..a53ac1cd9b 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -12,6 +12,7 @@ using osu.Game.Users; namespace osu.Game.Online.API.Requests.Responses { + [JsonObject(MemberSerialization.OptIn)] public class APIUser : IEquatable, IUser { [JsonProperty(@"id")] diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index b301c81921..c7f34905e2 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -219,17 +218,6 @@ namespace osu.Game.Online.Rooms Playlist.RemoveAll(i => i.Expired); } - #region Newtonsoft.Json implicit ShouldSerialize() methods - - // The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases. - // They rely on being named exactly the same as the corresponding fields (casing included) and as such should NOT be renamed - // unless the fields are also renamed. - - [UsedImplicitly] - public bool ShouldSerializeHost() => false; - - #endregion - [JsonObject(MemberSerialization.OptIn)] public class RoomPlaylistItemStats { From 5dd0d48df968594b86f4463fc1634dc9c93abbe0 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Wed, 23 Feb 2022 14:06:22 +0100 Subject: [PATCH 203/306] Move the key handling logic to MainMenu and simplify it Also makes use of the host.SuspendToBackground() return value. --- osu.Game/Screens/Menu/ExitConfirmOverlay.cs | 23 -------- osu.Game/Screens/Menu/MainMenu.cs | 59 +++++++++++---------- 2 files changed, 32 insertions(+), 50 deletions(-) diff --git a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs index 253ec17c28..a90b83c5fe 100644 --- a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs +++ b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs @@ -3,7 +3,6 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -43,26 +42,4 @@ namespace osu.Game.Screens.Menu } } } - - /// - /// An that behaves as if the is always 0. - /// - /// This is useful for mobile devices using gesture navigation, where holding to confirm is not possible. - public class NoHoldExitConfirmOverlay : ExitConfirmOverlay, IKeyBindingHandler - { - public new bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat) - return false; - - if (e.Action == GlobalAction.Back) - { - Progress.Value = 1; - Confirm(); - return true; - } - - return false; - } - } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 7bc0cb48bf..a2cb448d40 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -6,12 +6,15 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online.API; using osu.Game.Overlays; @@ -26,7 +29,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Menu { - public class MainMenu : OsuScreen, IHandlePresentBeatmap + public class MainMenu : OsuScreen, IHandlePresentBeatmap, IKeyBindingHandler { public const float FADE_IN_DURATION = 300; @@ -90,14 +93,6 @@ namespace osu.Game.Screens.Menu } }); } - else if (host.CanSuspendToBackground) - { - AddInternal(exitConfirmOverlay = new NoHoldExitConfirmOverlay - { - // treat as if the UIHoldActivationDelay is always 0. see NoHoldExitConfirmOverlay xmldoc for more info. - Action = this.Exit - }); - } AddRangeInternal(new[] { @@ -157,24 +152,6 @@ namespace osu.Game.Screens.Menu private void confirmAndExit() { - if (host.CanSuspendToBackground) - { - // cancel the overlay as we're not actually exiting. - // this is the same action as 'onCancel' in `ConfirmExitDialog`. - exitConfirmOverlay.Abort(); - - // fade the track so the Bass.Pause() on suspend isn't as jarring. - const double fade_time = 500; - musicController.CurrentTrack - .VolumeTo(0, fade_time, Easing.Out).Then() - .VolumeTo(1, fade_time, Easing.In); - - host.SuspendToBackground(); - - // on hosts that can only suspend, we don't ever want to exit the game. - return; - } - if (exitConfirmed) return; exitConfirmed = true; @@ -324,5 +301,33 @@ namespace osu.Game.Screens.Menu Schedule(loadSoloSongSelect); } + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat) + return false; + + if (e.Action == GlobalAction.Back && host.CanSuspendToBackground) + { + bool didSuspend = host.SuspendToBackground(); + + if (didSuspend) + { + // fade the track so the Bass.Pause() on suspend isn't as jarring. + const double fade_time = 500; + musicController.CurrentTrack + .VolumeTo(0, fade_time, Easing.Out).Then() + .VolumeTo(1, fade_time, Easing.In); + + return true; + } + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } } } From 5ff4d3f8e5fca93f662bf4d0d0d171b712a7c43f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 19:05:46 +0900 Subject: [PATCH 204/306] Add support to `SpectatorClient` to resend failed frame bundles --- osu.Game/Online/Spectator/SpectatorClient.cs | 48 ++++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index a54ea0d9ee..ba0a9a77a0 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -168,8 +168,6 @@ namespace osu.Game.Online.Spectator }); } - public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data); - public void EndPlaying(GameplayState state) { // This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue). @@ -180,7 +178,7 @@ namespace osu.Game.Online.Spectator return; if (pendingFrames.Count > 0) - purgePendingFrames(true); + purgePendingFrames(); IsPlaying = false; currentBeatmap = null; @@ -230,9 +228,11 @@ namespace osu.Game.Online.Spectator protected abstract Task StopWatchingUserInternal(int userId); + private readonly Queue pendingFrameBundles = new Queue(); + private readonly Queue pendingFrames = new Queue(); - private double lastSendTime; + private double lastPurgeTime; private Task? lastSend; @@ -242,7 +242,7 @@ namespace osu.Game.Online.Spectator { base.Update(); - if (pendingFrames.Count > 0 && Time.Current - lastSendTime > TIME_BETWEEN_SENDS) + if (pendingFrames.Count > 0 && Time.Current - lastPurgeTime > TIME_BETWEEN_SENDS) purgePendingFrames(); } @@ -260,23 +260,41 @@ namespace osu.Game.Online.Spectator purgePendingFrames(); } - private void purgePendingFrames(bool force = false) + private void purgePendingFrames() { - if (lastSend?.IsCompleted == false && !force) - return; - if (pendingFrames.Count == 0) return; - var frames = pendingFrames.ToArray(); - - pendingFrames.Clear(); - Debug.Assert(currentScore != null); - SendFrames(new FrameDataBundle(currentScore.ScoreInfo, frames)); + var frames = pendingFrames.ToArray(); + var bundle = new FrameDataBundle(currentScore.ScoreInfo, frames); - lastSendTime = Time.Current; + pendingFrames.Clear(); + lastPurgeTime = Time.Current; + + pendingFrameBundles.Enqueue(bundle); + + sendNextBundleIfRequired(); + } + + private void sendNextBundleIfRequired() + { + if (lastSend?.IsCompleted == false) + return; + + if (!pendingFrameBundles.TryPeek(out var bundle)) + return; + + lastSend = SendFramesInternal(bundle); + lastSend.ContinueWith(t => Schedule(() => + { + // If the last bundle send wasn't successful, try again without dequeuing. + if (t.IsCompletedSuccessfully) + pendingFrameBundles.Dequeue(); + + sendNextBundleIfRequired(); + })); } } } From cff6f854724c8ce4f8ba8935734664a5e5f7c048 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 19:08:32 +0900 Subject: [PATCH 205/306] Add note about reconnection being insufficient currently --- osu.Game/Online/Spectator/SpectatorClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index ba0a9a77a0..7afceb41a4 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -94,6 +94,7 @@ namespace osu.Game.Online.Spectator // re-send state in case it wasn't received if (IsPlaying) + // TODO: this is likely sent out of order after a reconnect scenario. needs further consideration. BeginPlayingInternal(currentState); } else From 14c8ce50a0849fb97882db3f9dd2a9553ac118bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Feb 2022 17:56:07 +0900 Subject: [PATCH 206/306] Prefix all test send methods in `TestSpectatorClient` with `Send` --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 10 +++++----- .../TestSceneMultiSpectatorLeaderboard.cs | 2 +- .../Multiplayer/TestSceneMultiSpectatorScreen.cs | 12 ++++++------ .../TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- .../TestSceneMultiplayerGameplayLeaderboardTeams.cs | 2 +- .../Online/TestSceneCurrentlyPlayingDisplay.cs | 4 ++-- .../Tests/Visual/Spectator/TestSpectatorClient.cs | 7 ++----- 7 files changed, 18 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index d47ebf9f0d..d6f8c7addf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Passed)); + AddStep("send passed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Passed)); AddUntilStep("state is passed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Passed); start(); @@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id)); + AddStep("send quit", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit); start(); @@ -293,7 +293,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Failed)); + AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); start(); @@ -312,9 +312,9 @@ namespace osu.Game.Tests.Visual.Gameplay private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); - private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); + private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.SendStartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); - private void finish(SpectatedUserState state = SpectatedUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); + private void finish(SpectatedUserState state = SpectatedUserState.Quit) => AddStep("end play", () => spectatorClient.SendEndPlay(streamingUser.Id, state)); private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 6b3573b3cb..488ecdb8af 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach ((int userId, var _) in clocks) { - SpectatorClient.StartPlay(userId, 0); + SpectatorClient.SendStartPlay(userId, 0); OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId }); } }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 7ce0c6a94d..a785301f62 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -70,11 +70,11 @@ namespace osu.Game.Tests.Visual.Multiplayer loadSpectateScreen(false); AddWaitStep("wait a bit", 10); - AddStep("load player first_player_id", () => SpectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId)); + AddStep("load player first_player_id", () => SpectatorClient.SendStartPlay(PLAYER_1_ID, importedBeatmapId)); AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType().Count() == 1); AddWaitStep("wait a bit", 10); - AddStep("load player second_player_id", () => SpectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId)); + AddStep("load player second_player_id", () => SpectatorClient.SendStartPlay(PLAYER_2_ID, importedBeatmapId)); AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType().Count() == 2); } @@ -133,8 +133,8 @@ namespace osu.Game.Tests.Visual.Multiplayer TeamID = 1, }; - SpectatorClient.StartPlay(player1.UserID, importedBeatmapId); - SpectatorClient.StartPlay(player2.UserID, importedBeatmapId); + SpectatorClient.SendStartPlay(player1.UserID, importedBeatmapId); + SpectatorClient.SendStartPlay(player2.UserID, importedBeatmapId); playingUsers.Add(player1); playingUsers.Add(player2); @@ -397,7 +397,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }; OnlinePlayDependencies.MultiplayerClient.AddUser(user.User, true); - SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); + SpectatorClient.SendStartPlay(id, beatmapId ?? importedBeatmapId); playingUsers.Add(user); } @@ -411,7 +411,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var user = playingUsers.Single(u => u.UserID == userId); OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull()); - SpectatorClient.EndPlay(userId); + SpectatorClient.SendEndPlay(userId); playingUsers.Remove(user); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 6605f82d5c..cfac5da4ff 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int user in users) { - SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); + SpectatorClient.SendStartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); multiplayerUsers.Add(OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true)); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index dabc1c1e5a..f751b162d1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int user in users) { - SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); + SpectatorClient.SendStartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); var roomUser = OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true); roomUser.MatchState = new TeamVersusUserState diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index b5a03b558d..35a4f8cf2d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -56,9 +56,9 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestBasicDisplay() { - AddStep("Add playing user", () => spectatorClient.StartPlay(streamingUser.Id, 0)); + AddStep("Add playing user", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType()?.FirstOrDefault()?.User.Id == 2); - AddStep("Remove playing user", () => spectatorClient.EndPlay(streamingUser.Id)); + AddStep("Remove playing user", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType().Any()); } diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 1322a99ea7..741b489d95 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -3,7 +3,6 @@ #nullable enable -using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -47,7 +46,7 @@ namespace osu.Game.Tests.Visual.Spectator /// /// The user to start play for. /// The playing beatmap id. - public void StartPlay(int userId, int beatmapId) + public void SendStartPlay(int userId, int beatmapId) { userBeatmapDictionary[userId] = beatmapId; userNextFrameDictionary[userId] = 0; @@ -59,7 +58,7 @@ namespace osu.Game.Tests.Visual.Spectator /// /// The user to end play for. /// The spectator state to end play with. - public void EndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) + public void SendEndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) { if (!userBeatmapDictionary.ContainsKey(userId)) return; @@ -74,8 +73,6 @@ namespace osu.Game.Tests.Visual.Spectator userBeatmapDictionary.Remove(userId); } - public new void Schedule(Action action) => base.Schedule(action); - /// /// Sends frames for an arbitrary user, in bundles containing 10 frames each. /// From c94e7e2abe98799d9372b2d0ba4f5fc91fc668a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 02:18:35 +0900 Subject: [PATCH 207/306] Add ability to simulate network failures to `TestSpectatorClient` --- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../TestSceneMultiSpectatorLeaderboard.cs | 4 ++-- .../TestSceneMultiSpectatorScreen.cs | 4 ++-- .../Visual/Spectator/TestSpectatorClient.cs | 20 ++++++++++++++++--- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index d6f8c7addf..d614815316 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -321,7 +321,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void sendFrames(int count = 10) { - AddStep("send frames", () => spectatorClient.SendFrames(streamingUser.Id, count)); + AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count)); } private void loadSpectatingScreen() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 488ecdb8af..42bb99de24 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -68,10 +68,10 @@ namespace osu.Game.Tests.Visual.Multiplayer // For player 2, send frames in sets of 10. for (int i = 0; i < 100; i++) { - SpectatorClient.SendFrames(PLAYER_1_ID, 1); + SpectatorClient.SendFramesFromUser(PLAYER_1_ID, 1); if (i % 10 == 0) - SpectatorClient.SendFrames(PLAYER_2_ID, 10); + SpectatorClient.SendFramesFromUser(PLAYER_2_ID, 10); } }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index a785301f62..4b89efe858 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -360,7 +360,7 @@ namespace osu.Game.Tests.Visual.Multiplayer // to ensure negative gameplay start time does not affect spectator, send frames exactly after StartGameplay(). // (similar to real spectating sessions in which the first frames get sent between StartGameplay() and player load complete) - AddStep("send frames at gameplay start", () => getInstance(PLAYER_1_ID).OnGameplayStarted += () => SpectatorClient.SendFrames(PLAYER_1_ID, 100)); + AddStep("send frames at gameplay start", () => getInstance(PLAYER_1_ID).OnGameplayStarted += () => SpectatorClient.SendFramesFromUser(PLAYER_1_ID, 100)); AddUntilStep("wait for player load", () => spectatorScreen.AllPlayersLoaded); @@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("send frames", () => { foreach (int id in userIds) - SpectatorClient.SendFrames(id, count); + SpectatorClient.SendFramesFromUser(id, count); }); } diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 741b489d95..ae5d20f4f4 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -20,10 +21,15 @@ namespace osu.Game.Tests.Visual.Spectator public class TestSpectatorClient : SpectatorClient { /// - /// Maximum number of frames sent per bundle via . + /// Maximum number of frames sent per bundle via . /// public const int FRAME_BUNDLE_SIZE = 10; + /// + /// Whether to force send operations to fail (simulating a network issue). + /// + public bool ShouldFailSendingFrames { get; set; } + public override IBindable IsConnected { get; } = new Bindable(true); public IReadOnlyDictionary LastReceivedUserFrames => lastReceivedUserFrames; @@ -75,10 +81,12 @@ namespace osu.Game.Tests.Visual.Spectator /// /// Sends frames for an arbitrary user, in bundles containing 10 frames each. + /// This bypasses the standard queueing mechanism completely and should only be used to test cases where multiple users need to be sending data. + /// Importantly, will have no effect. /// /// The user to send frames for. /// The total number of frames to send. - public void SendFrames(int userId, int count) + public void SendFramesFromUser(int userId, int count) { var frames = new List(); @@ -120,7 +128,13 @@ namespace osu.Game.Tests.Visual.Spectator return ((ISpectatorClient)this).UserBeganPlaying(api.LocalUser.Value.Id, state); } - protected override Task SendFramesInternal(FrameDataBundle data) => ((ISpectatorClient)this).UserSentFrames(api.LocalUser.Value.Id, data); + protected override Task SendFramesInternal(FrameDataBundle bundle) + { + if (ShouldFailSendingFrames) + return Task.FromException(new InvalidOperationException()); + + return ((ISpectatorClient)this).UserSentFrames(api.LocalUser.Value.Id, bundle); + } protected override Task EndPlayingInternal(SpectatorState state) => ((ISpectatorClient)this).UserFinishedPlaying(api.LocalUser.Value.Id, state); From 47b84295a6c32c4b9ac4ae4a8cd6d3d26d5206be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 02:19:12 +0900 Subject: [PATCH 208/306] Fix bundle send logic not correctly waiting for task completion (due to nested schedule) --- osu.Game/Online/Spectator/SpectatorClient.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 7afceb41a4..428a4cdbdb 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -221,8 +221,7 @@ namespace osu.Game.Online.Spectator protected abstract Task BeginPlayingInternal(SpectatorState state); - protected abstract Task SendFramesInternal(FrameDataBundle data); - + protected abstract Task SendFramesInternal(FrameDataBundle bundle); protected abstract Task EndPlayingInternal(SpectatorState state); protected abstract Task WatchUserInternal(int userId); @@ -281,19 +280,27 @@ namespace osu.Game.Online.Spectator private void sendNextBundleIfRequired() { + Debug.Assert(ThreadSafety.IsUpdateThread); + if (lastSend?.IsCompleted == false) return; if (!pendingFrameBundles.TryPeek(out var bundle)) return; - lastSend = SendFramesInternal(bundle); - lastSend.ContinueWith(t => Schedule(() => + TaskCompletionSource tcs = new TaskCompletionSource(); + + lastSend = tcs.Task; + + SendFramesInternal(bundle).ContinueWith(t => Schedule(() => { + bool wasSuccessful = t.Exception == null; + // If the last bundle send wasn't successful, try again without dequeuing. - if (t.IsCompletedSuccessfully) + if (wasSuccessful) pendingFrameBundles.Dequeue(); + tcs.SetResult(wasSuccessful); sendNextBundleIfRequired(); })); } From 260cf793fefe2e3f862373e2f35c7c1bf18de8ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 02:18:42 +0900 Subject: [PATCH 209/306] Add test coverage of more advanced frame delivery scenarios to `TestSceneSpectatorPlayback` --- .../Gameplay/TestSceneSpectatorPlayback.cs | 57 +++++++++++++++---- .../Visual/Spectator/TestSpectatorClient.cs | 4 ++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index a4d8460846..4ec46036f6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -41,8 +41,12 @@ namespace osu.Game.Tests.Visual.Gameplay private Replay replay; + private TestSpectatorClient spectatorClient; + private ManualClock manualClock; + private TestReplayRecorder recorder; + private OsuSpriteText latencyDisplay; private TestFramedReplayInputHandler replayHandler; @@ -54,7 +58,6 @@ namespace osu.Game.Tests.Visual.Gameplay { replay = new Replay(); manualClock = new ManualClock(); - SpectatorClient spectatorClient; Child = new DependencyProvidingContainer { @@ -76,7 +79,7 @@ namespace osu.Game.Tests.Visual.Gameplay { recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Recorder = new TestReplayRecorder + Recorder = recorder = new TestReplayRecorder { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), }, @@ -143,22 +146,52 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + [Test] + public void TestBasic() + { + AddUntilStep("received frames", () => replay.Frames.Count > 50); + AddStep("stop sending frames", () => recorder.Expire()); + AddUntilStep("wait for all frames received", () => replay.Frames.Count == recorder.SentFrames.Count); + } + + [Test] + public void TestWithSendFailure() + { + AddUntilStep("received frames", () => replay.Frames.Count > 50); + + int framesReceivedSoFar = 0; + int frameSendAttemptsSoFar = 0; + + AddStep("start failing sends", () => + { + spectatorClient.ShouldFailSendingFrames = true; + framesReceivedSoFar = replay.Frames.Count; + frameSendAttemptsSoFar = spectatorClient.FrameSendAttempts; + }); + + AddUntilStep("wait for send attempts", () => spectatorClient.FrameSendAttempts > frameSendAttemptsSoFar + 5); + AddAssert("frames did not increase", () => framesReceivedSoFar == replay.Frames.Count); + + AddStep("stop failing sends", () => spectatorClient.ShouldFailSendingFrames = false); + + AddUntilStep("wait for next frames", () => framesReceivedSoFar < replay.Frames.Count); + + AddStep("stop sending frames", () => recorder.Expire()); + + AddUntilStep("wait for all frames received", () => replay.Frames.Count == recorder.SentFrames.Count); + AddAssert("ensure frames were received in the correct sequence", () => replay.Frames.Select(f => f.Time).SequenceEqual(recorder.SentFrames.Select(f => f.Time))); + } + private void onNewFrames(int userId, FrameDataBundle frames) { - Logger.Log($"Received {frames.Frames.Count} new frames ({string.Join(',', frames.Frames.Select(f => ((int)f.Time).ToString()))})"); - foreach (var legacyFrame in frames.Frames) { var frame = new TestReplayFrame(); frame.FromLegacy(legacyFrame, null); replay.Frames.Add(frame); } - } - [Test] - public void TestBasic() - { - AddStep("Wait for user input", () => { }); + Logger.Log($"Received {frames.Frames.Count} new frames (total {replay.Frames.Count} of {recorder.SentFrames.Count})"); } private double latency = SpectatorClient.TIME_BETWEEN_SENDS; @@ -318,6 +351,8 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { + public List SentFrames = new List(); + public TestReplayRecorder() : base(new Score { @@ -332,7 +367,9 @@ namespace osu.Game.Tests.Visual.Gameplay protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) { - return new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); + var testReplayFrame = new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); + SentFrames.Add(testReplayFrame); + return testReplayFrame; } } } diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index ae5d20f4f4..f5da95bd7b 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -30,6 +30,8 @@ namespace osu.Game.Tests.Visual.Spectator /// public bool ShouldFailSendingFrames { get; set; } + public int FrameSendAttempts { get; private set; } + public override IBindable IsConnected { get; } = new Bindable(true); public IReadOnlyDictionary LastReceivedUserFrames => lastReceivedUserFrames; @@ -130,6 +132,8 @@ namespace osu.Game.Tests.Visual.Spectator protected override Task SendFramesInternal(FrameDataBundle bundle) { + FrameSendAttempts++; + if (ShouldFailSendingFrames) return Task.FromException(new InvalidOperationException()); From 694c6ad872c87aeabdc1d430fbadf082f24463bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 00:59:25 +0900 Subject: [PATCH 210/306] Fix frames potentially getting lost due to non-matching `Schedule` usage --- osu.Game/Online/Spectator/SpectatorClient.cs | 38 ++++++++++++-------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 428a4cdbdb..0620643bc3 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Replays.Legacy; @@ -156,6 +157,8 @@ namespace osu.Game.Online.Spectator IsPlaying = true; + totalBundledFrames = 0; + // transfer state at point of beginning play currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; @@ -169,6 +172,21 @@ namespace osu.Game.Online.Spectator }); } + public void HandleFrame(ReplayFrame frame) => Schedule(() => + { + if (!IsPlaying) + { + Logger.Log($"Frames arrived at {nameof(SpectatorClient)} outside of gameplay scope and will be ignored."); + return; + } + + if (frame is IConvertibleReplayFrame convertible) + pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap)); + + if (pendingFrames.Count > max_pending_frames) + purgePendingFrames(); + }); + public void EndPlaying(GameplayState state) { // This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue). @@ -236,6 +254,8 @@ namespace osu.Game.Online.Spectator private Task? lastSend; + private int totalBundledFrames; + private const int max_pending_frames = 30; protected override void Update() @@ -246,20 +266,6 @@ namespace osu.Game.Online.Spectator purgePendingFrames(); } - public void HandleFrame(ReplayFrame frame) - { - Debug.Assert(ThreadSafety.IsUpdateThread); - - if (!IsPlaying) - return; - - if (frame is IConvertibleReplayFrame convertible) - pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap)); - - if (pendingFrames.Count > max_pending_frames) - purgePendingFrames(); - } - private void purgePendingFrames() { if (pendingFrames.Count == 0) @@ -270,6 +276,10 @@ namespace osu.Game.Online.Spectator var frames = pendingFrames.ToArray(); var bundle = new FrameDataBundle(currentScore.ScoreInfo, frames); + totalBundledFrames += frames.Length; + + Console.WriteLine($"Purging {pendingFrames.Count} frames (total {totalBundledFrames})"); + pendingFrames.Clear(); lastPurgeTime = Time.Current; From 5ffdd57895bd61652855683e2064713006ae7fda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 02:19:37 +0900 Subject: [PATCH 211/306] Rename weirdly named parameter --- osu.Game/Online/Spectator/OnlineSpectatorClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index ddde69c627..4d6ca0b311 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -57,14 +57,14 @@ namespace osu.Game.Online.Spectator return connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), state); } - protected override Task SendFramesInternal(FrameDataBundle data) + protected override Task SendFramesInternal(FrameDataBundle bundle) { if (!IsConnected.Value) return Task.CompletedTask; Debug.Assert(connection != null); - return connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); + return connection.SendAsync(nameof(ISpectatorServer.SendFrameData), bundle); } protected override Task EndPlayingInternal(SpectatorState state) From 3f2ef030e4a1d82978643b8e31a080629c8654a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 02:29:49 +0900 Subject: [PATCH 212/306] Group `SpectatorClient` private fields together --- osu.Game/Online/Spectator/SpectatorClient.cs | 49 ++++++++++---------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 0620643bc3..d0b89e981d 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -46,18 +46,6 @@ namespace osu.Game.Online.Spectator /// public IBindableList PlayingUsers => playingUsers; - /// - /// All users currently being watched. - /// - private readonly List watchedUsers = new List(); - - private readonly BindableDictionary watchedUserStates = new BindableDictionary(); - private readonly BindableList playingUsers = new BindableList(); - private readonly SpectatorState currentState = new SpectatorState(); - - private IBeatmap? currentBeatmap; - private Score? currentScore; - /// /// Whether the local user is playing. /// @@ -78,6 +66,30 @@ namespace osu.Game.Online.Spectator /// public event Action? OnUserFinishedPlaying; + /// + /// All users currently being watched. + /// + private readonly List watchedUsers = new List(); + + private readonly BindableDictionary watchedUserStates = new BindableDictionary(); + private readonly BindableList playingUsers = new BindableList(); + private readonly SpectatorState currentState = new SpectatorState(); + + private IBeatmap? currentBeatmap; + private Score? currentScore; + + private readonly Queue pendingFrameBundles = new Queue(); + + private readonly Queue pendingFrames = new Queue(); + + private double lastPurgeTime; + + private Task? lastSend; + + private int totalBundledFrames; + + private const int max_pending_frames = 30; + [BackgroundDependencyLoader] private void load() { @@ -240,24 +252,13 @@ namespace osu.Game.Online.Spectator protected abstract Task BeginPlayingInternal(SpectatorState state); protected abstract Task SendFramesInternal(FrameDataBundle bundle); + protected abstract Task EndPlayingInternal(SpectatorState state); protected abstract Task WatchUserInternal(int userId); protected abstract Task StopWatchingUserInternal(int userId); - private readonly Queue pendingFrameBundles = new Queue(); - - private readonly Queue pendingFrames = new Queue(); - - private double lastPurgeTime; - - private Task? lastSend; - - private int totalBundledFrames; - - private const int max_pending_frames = 30; - protected override void Update() { base.Update(); From 8a0aba6c596bcadf1c0e15ef661e8846503841d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Feb 2022 23:11:12 +0100 Subject: [PATCH 213/306] Implement mod panel for new mod select screen --- .../Visual/UserInterface/TestSceneModPanel.cs | 42 +++ osu.Game/Overlays/Mods/ModPanel.cs | 272 ++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs create mode 100644 osu.Game/Overlays/Mods/ModPanel.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs new file mode 100644 index 0000000000..bdf3f98863 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs @@ -0,0 +1,42 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneModPanel : OsuManualInputManagerTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + [Test] + public void TestVariousPanels() + { + AddStep("create content", () => Child = new FillFlowContainer + { + Width = 300, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(0, 5), + Children = new[] + { + new ModPanel(new OsuModHalfTime()), + new ModPanel(new OsuModFlashlight()), + new ModPanel(new OsuModAutoplay()), + new ModPanel(new OsuModAlternate()), + new ModPanel(new OsuModApproachDifferent()) + } + }); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs new file mode 100644 index 0000000000..4d08b22017 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -0,0 +1,272 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK; +using osuTK.Input; + +#nullable enable + +namespace osu.Game.Overlays.Mods +{ + public class ModPanel : OsuClickableContainer + { + public Mod Mod { get; } + public BindableBool Active { get; } = new BindableBool(); + + protected readonly Box Background; + protected readonly Container SwitchContainer; + protected readonly Container MainContentContainer; + protected readonly Box TextBackground; + protected readonly FillFlowContainer TextFlow; + + [Resolved] + protected OverlayColourProvider ColourProvider { get; private set; } = null!; + + protected const double TRANSITION_DURATION = 150; + protected const float SHEAR_X = 0.2f; + + protected const float HEIGHT = 42; + protected const float CORNER_RADIUS = 7; + protected const float IDLE_SWITCH_WIDTH = 54; + protected const float EXPANDED_SWITCH_WIDTH = 70; + + private Colour4 activeColour; + private Colour4 activeHoverColour; + + private Sample? sampleOff; + private Sample? sampleOn; + + public ModPanel(Mod mod) + { + Mod = mod; + + RelativeSizeAxes = Axes.X; + Height = 42; + + // all below properties are applied to `Content` rather than the `ModPanel` in its entirety + // to allow external components to set these properties on the panel without affecting + // its "internal" appearance. + Content.Masking = true; + Content.CornerRadius = CORNER_RADIUS; + Content.BorderThickness = 2; + Content.Shear = new Vector2(SHEAR_X, 0); + + Children = new Drawable[] + { + Background = new Box + { + RelativeSizeAxes = Axes.Both + }, + SwitchContainer = new Container + { + RelativeSizeAxes = Axes.Y, + Child = new ModSwitchSmall(mod) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Active = { BindTarget = Active }, + Shear = new Vector2(-SHEAR_X, 0), + Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) + } + }, + MainContentContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = CORNER_RADIUS, + Children = new Drawable[] + { + TextBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + TextFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 17.5f, + Vertical = 4 + }, + Direction = FillDirection.Vertical, + Children = new[] + { + new OsuSpriteText + { + Text = mod.Name, + Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), + Shear = new Vector2(-SHEAR_X, 0), + Margin = new MarginPadding + { + Left = -18 * SHEAR_X + } + }, + new OsuSpriteText + { + Text = mod.Description, + Font = OsuFont.Default.With(size: 12), + RelativeSizeAxes = Axes.X, + Truncate = true, + Shear = new Vector2(-SHEAR_X, 0) + } + } + } + } + } + } + }; + + Action = Active.Toggle; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + sampleOn = audio.Samples.Get(@"UI/check-on"); + sampleOff = audio.Samples.Get(@"UI/check-off"); + + activeColour = colours.ForModType(Mod.Type); + activeHoverColour = activeColour.Lighten(0.3f); + } + + protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); + + protected override void LoadComplete() + { + base.LoadComplete(); + Active.BindValueChanged(_ => + { + playStateChangeSamples(); + UpdateState(); + }); + + UpdateState(); + FinishTransforms(true); + } + + private void playStateChangeSamples() + { + if (Active.Value) + sampleOn?.Play(); + else + sampleOff?.Play(); + } + + protected override bool OnHover(HoverEvent e) + { + UpdateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + UpdateState(); + base.OnHoverLost(e); + } + + private double? mouseDownTime; + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + mouseDownTime = Time.Current; + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + mouseDownTime = null; + base.OnMouseUp(e); + } + + protected override bool OnDragStart(DragStartEvent e) + { + mouseDownTime = null; + return true; + } + + protected override void Update() + { + base.Update(); + + if (mouseDownTime != null) + { + double startTime = mouseDownTime.Value; + double endTime = startTime + 600; + + float startValue = Active.Value ? EXPANDED_SWITCH_WIDTH : IDLE_SWITCH_WIDTH; + float endValue = IDLE_SWITCH_WIDTH + (EXPANDED_SWITCH_WIDTH - IDLE_SWITCH_WIDTH) * (Active.Value ? 0.2f : 0.8f); + + float targetWidth = Interpolation.ValueAt(Math.Clamp(Time.Current, startTime, endTime), startValue, endValue, startTime, endTime, Easing.OutQuint); + + SwitchContainer.Width = targetWidth; + MainContentContainer.Padding = new MarginPadding + { + Left = targetWidth, + Right = CORNER_RADIUS + }; + } + } + + protected virtual void UpdateState() + { + if (Active.Value) + { + Colour4 backgroundTextColour = IsHovered ? activeHoverColour : activeColour; + Colour4 backgroundColour = Interpolation.ValueAt(0.7f, Colour4.Black, backgroundTextColour, 0, 1); + + Content.TransformTo(nameof(BorderColour), (ColourInfo)backgroundColour, TRANSITION_DURATION, Easing.OutQuint); + Background.FadeColour(backgroundColour, TRANSITION_DURATION, Easing.OutQuint); + SwitchContainer.ResizeWidthTo(EXPANDED_SWITCH_WIDTH, TRANSITION_DURATION, Easing.OutQuint); + MainContentContainer.TransformTo(nameof(Padding), new MarginPadding + { + Left = EXPANDED_SWITCH_WIDTH, + Right = CORNER_RADIUS + }, TRANSITION_DURATION, Easing.OutQuint); + TextBackground.FadeColour(backgroundTextColour, TRANSITION_DURATION, Easing.OutQuint); + TextFlow.FadeColour(ColourProvider.Background6, TRANSITION_DURATION, Easing.OutQuint); + } + else + { + Colour4 backgroundColour = ColourProvider.Background3; + if (IsHovered) + backgroundColour = Interpolation.ValueAt(0.25f, backgroundColour, activeColour, 0, 1); + + Colour4 textBackgroundColour = ColourProvider.Background2; + if (IsHovered) + textBackgroundColour = Interpolation.ValueAt(0.25f, textBackgroundColour, activeColour, 0, 1); + + Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, textBackgroundColour), TRANSITION_DURATION, Easing.OutQuint); + Background.FadeColour(backgroundColour, TRANSITION_DURATION, Easing.OutQuint); + SwitchContainer.ResizeWidthTo(IDLE_SWITCH_WIDTH, TRANSITION_DURATION, Easing.OutQuint); + MainContentContainer.TransformTo(nameof(Padding), new MarginPadding + { + Left = IDLE_SWITCH_WIDTH, + Right = CORNER_RADIUS + }, TRANSITION_DURATION, Easing.OutQuint); + TextBackground.FadeColour(textBackgroundColour, TRANSITION_DURATION, Easing.OutQuint); + TextFlow.FadeColour(Colour4.White, TRANSITION_DURATION, Easing.OutQuint); + } + } + } +} From bbe2dfa458c5c0a0dfa56d68717c8c625c4fd01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Feb 2022 23:11:26 +0100 Subject: [PATCH 214/306] Move out incompatibility displaying tooltip to own class --- .../IncompatibilityDisplayingModButton.cs | 51 --------------- .../Mods/IncompatibilityDisplayingTooltip.cs | 64 +++++++++++++++++++ 2 files changed, 64 insertions(+), 51 deletions(-) create mode 100644 osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs index 0f51439252..6e2cb40596 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs @@ -8,11 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play.HUD; using osu.Game.Utils; using osuTK; @@ -66,52 +62,5 @@ namespace osu.Game.Overlays.Mods } public override ITooltip GetCustomTooltip() => new IncompatibilityDisplayingTooltip(); - - private class IncompatibilityDisplayingTooltip : ModButtonTooltip - { - private readonly OsuSpriteText incompatibleText; - - private readonly Bindable> incompatibleMods = new Bindable>(); - - [Resolved] - private Bindable ruleset { get; set; } - - public IncompatibilityDisplayingTooltip() - { - AddRange(new Drawable[] - { - incompatibleText = new OsuSpriteText - { - Margin = new MarginPadding { Top = 5 }, - Font = OsuFont.GetFont(weight: FontWeight.Regular), - Text = "Incompatible with:" - }, - new ModDisplay - { - Current = incompatibleMods, - ExpansionMode = ExpansionMode.AlwaysExpanded, - Scale = new Vector2(0.7f) - } - }); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - incompatibleText.Colour = colours.BlueLight; - } - - protected override void UpdateDisplay(Mod mod) - { - base.UpdateDisplay(mod); - - var incompatibleTypes = mod.IncompatibleMods; - - var allMods = ruleset.Value.CreateInstance().AllMods; - - incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).Select(m => m.CreateInstance()).ToList(); - incompatibleText.Text = incompatibleMods.Value.Any() ? "Incompatible with:" : "Compatible with all mods"; - } - } } } diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.cs new file mode 100644 index 0000000000..d8117c8f00 --- /dev/null +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingTooltip.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 System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + internal class IncompatibilityDisplayingTooltip : ModButtonTooltip + { + private readonly OsuSpriteText incompatibleText; + + private readonly Bindable> incompatibleMods = new Bindable>(); + + [Resolved] + private Bindable ruleset { get; set; } + + public IncompatibilityDisplayingTooltip() + { + AddRange(new Drawable[] + { + incompatibleText = new OsuSpriteText + { + Margin = new MarginPadding { Top = 5 }, + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = "Incompatible with:" + }, + new ModDisplay + { + Current = incompatibleMods, + ExpansionMode = ExpansionMode.AlwaysExpanded, + Scale = new Vector2(0.7f) + } + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + incompatibleText.Colour = colours.BlueLight; + } + + protected override void UpdateDisplay(Mod mod) + { + base.UpdateDisplay(mod); + + var incompatibleTypes = mod.IncompatibleMods; + + var allMods = ruleset.Value.CreateInstance().AllMods; + + incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).Select(m => m.CreateInstance()).ToList(); + incompatibleText.Text = incompatibleMods.Value.Any() ? "Incompatible with:" : "Compatible with all mods"; + } + } +} From 713f89a59c7d0c964d5c4f1c0760dd45fe596bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Feb 2022 23:11:38 +0100 Subject: [PATCH 215/306] Implement incompatibility-displaying variant of mod panel --- .../Visual/UserInterface/TestSceneModPanel.cs | 39 ++++++++ .../Mods/IncompatibilityDisplayingModPanel.cs | 88 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs index bdf3f98863..95323e5dfa 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs @@ -1,14 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { @@ -38,5 +41,41 @@ namespace osu.Game.Tests.Visual.UserInterface } }); } + + [Test] + public void TestIncompatibilityDisplay() + { + IncompatibilityDisplayingModPanel panel = null; + + AddStep("create panel with DT", () => Child = panel = new IncompatibilityDisplayingModPanel(new OsuModDoubleTime()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.None, + Width = 300 + }); + + clickPanel(); + AddAssert("panel active", () => panel.Active.Value); + + clickPanel(); + AddAssert("panel not active", () => !panel.Active.Value); + + AddStep("set incompatible mod", () => SelectedMods.Value = new[] { new OsuModHalfTime() }); + + clickPanel(); + AddAssert("panel not active", () => !panel.Active.Value); + + AddStep("reset mods", () => SelectedMods.Value = Array.Empty()); + + clickPanel(); + AddAssert("panel active", () => panel.Active.Value); + + void clickPanel() => AddStep("click panel", () => + { + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Left); + }); + } } } diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs new file mode 100644 index 0000000000..4b6759c209 --- /dev/null +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs @@ -0,0 +1,88 @@ +// 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; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; + +namespace osu.Game.Overlays.Mods +{ + public class IncompatibilityDisplayingModPanel : ModPanel, IHasCustomTooltip + { + private readonly BindableBool incompatible = new BindableBool(); + + [Resolved] + private Bindable> selectedMods { get; set; } + + public IncompatibilityDisplayingModPanel(Mod mod) + : base(mod) + { + } + + protected override void LoadComplete() + { + selectedMods.BindValueChanged(_ => updateIncompatibility(), true); + incompatible.BindValueChanged(_ => Scheduler.AddOnce(UpdateState)); + // base call will run `UpdateState()` first time and finish transforms. + base.LoadComplete(); + } + + private void updateIncompatibility() + { + incompatible.Value = selectedMods.Value.Count > 0 && !selectedMods.Value.Contains(Mod) && !ModUtils.CheckCompatibleSet(selectedMods.Value.Append(Mod)); + } + + protected override void UpdateState() + { + Action = incompatible.Value ? () => { } : (Action)Active.Toggle; + + if (incompatible.Value) + { + Colour4 backgroundColour = ColourProvider.Background5; + Colour4 textBackgroundColour = ColourProvider.Background4; + + Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, textBackgroundColour), TRANSITION_DURATION, Easing.OutQuint); + Background.FadeColour(backgroundColour, TRANSITION_DURATION, Easing.OutQuint); + + SwitchContainer.ResizeWidthTo(IDLE_SWITCH_WIDTH, TRANSITION_DURATION, Easing.OutQuint); + SwitchContainer.FadeColour(Colour4.Gray, TRANSITION_DURATION, Easing.OutQuint); + MainContentContainer.TransformTo(nameof(Padding), new MarginPadding + { + Left = IDLE_SWITCH_WIDTH, + Right = CORNER_RADIUS + }, TRANSITION_DURATION, Easing.OutQuint); + + TextBackground.FadeColour(textBackgroundColour, TRANSITION_DURATION, Easing.OutQuint); + TextFlow.FadeColour(Colour4.White.Opacity(0.5f), TRANSITION_DURATION, Easing.OutQuint); + return; + } + + SwitchContainer.FadeColour(Colour4.White, TRANSITION_DURATION, Easing.OutQuint); + base.UpdateState(); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (incompatible.Value) + return true; // bypasses base call purposely in order to not play out the intermediate state animation. + + return base.OnMouseDown(e); + } + + #region IHasCustomTooltip + + public ITooltip GetCustomTooltip() => new IncompatibilityDisplayingTooltip(); + + public Mod TooltipContent => Mod; + + #endregion + } +} From 435bdd0b4a68d7abf66b2f7eb0c5d70f16246580 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 14:55:35 +0900 Subject: [PATCH 216/306] Combine and simplify state management logic This makes a few changes to bring things into a better shape during mouse interactions with the mod panels: - Dragging away from the panel now works in line with other buttons (ie. `OsuButton`) - Hovering now uses a lightened version of the current state, rather than always using the active colour. I think this feels better. - Mouse down now uses a transform point of 0.5. This is to give the button a feeling of one of those latching light switches which resists until reaching a point of overcoming the spring and switching state. I think 0.4 (non-active) and 0.6 (from active) may work better, but left at 0.5 for simplicity of implementation and I think it's good enough? - Border always uses the gradiented version. I did this for simplicity of implementation, but also think it looks better. - Adjusted transform durations to feel better to me. --- osu.Game/Overlays/Mods/ModPanel.cs | 102 ++++++++++------------------- 1 file changed, 35 insertions(+), 67 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 4d08b22017..13e2b5bb0b 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -40,6 +40,7 @@ namespace osu.Game.Overlays.Mods protected OverlayColourProvider ColourProvider { get; private set; } = null!; protected const double TRANSITION_DURATION = 150; + protected const float SHEAR_X = 0.2f; protected const float HEIGHT = 42; @@ -48,7 +49,6 @@ namespace osu.Game.Overlays.Mods protected const float EXPANDED_SWITCH_WIDTH = 70; private Colour4 activeColour; - private Colour4 activeHoverColour; private Sample? sampleOff; private Sample? sampleOn; @@ -146,7 +146,6 @@ namespace osu.Game.Overlays.Mods sampleOff = audio.Samples.Get(@"UI/check-off"); activeColour = colours.ForModType(Mod.Type); - activeHoverColour = activeColour.Lighten(0.3f); } protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); @@ -184,89 +183,58 @@ namespace osu.Game.Overlays.Mods base.OnHoverLost(e); } - private double? mouseDownTime; + private bool mouseDown; protected override bool OnMouseDown(MouseDownEvent e) { if (e.Button == MouseButton.Left) - mouseDownTime = Time.Current; + mouseDown = true; + + UpdateState(); return true; } protected override void OnMouseUp(MouseUpEvent e) { - mouseDownTime = null; + mouseDown = false; + + UpdateState(); base.OnMouseUp(e); } - protected override bool OnDragStart(DragStartEvent e) - { - mouseDownTime = null; - return true; - } - - protected override void Update() - { - base.Update(); - - if (mouseDownTime != null) - { - double startTime = mouseDownTime.Value; - double endTime = startTime + 600; - - float startValue = Active.Value ? EXPANDED_SWITCH_WIDTH : IDLE_SWITCH_WIDTH; - float endValue = IDLE_SWITCH_WIDTH + (EXPANDED_SWITCH_WIDTH - IDLE_SWITCH_WIDTH) * (Active.Value ? 0.2f : 0.8f); - - float targetWidth = Interpolation.ValueAt(Math.Clamp(Time.Current, startTime, endTime), startValue, endValue, startTime, endTime, Easing.OutQuint); - - SwitchContainer.Width = targetWidth; - MainContentContainer.Padding = new MarginPadding - { - Left = targetWidth, - Right = CORNER_RADIUS - }; - } - } - protected virtual void UpdateState() { - if (Active.Value) + float targetWidth = Active.Value ? EXPANDED_SWITCH_WIDTH : IDLE_SWITCH_WIDTH; + double transitionDuration = TRANSITION_DURATION; + + Colour4 textBackgroundColour = Active.Value ? activeColour : (Colour4)ColourProvider.Background2; + Colour4 mainBackgroundColour = Active.Value ? activeColour.Darken(0.3f) : (Colour4)ColourProvider.Background3; + Colour4 textColour = Active.Value ? (Colour4)ColourProvider.Background6 : Colour4.White; + + // Hover affects colour of button background + if (IsHovered) { - Colour4 backgroundTextColour = IsHovered ? activeHoverColour : activeColour; - Colour4 backgroundColour = Interpolation.ValueAt(0.7f, Colour4.Black, backgroundTextColour, 0, 1); - - Content.TransformTo(nameof(BorderColour), (ColourInfo)backgroundColour, TRANSITION_DURATION, Easing.OutQuint); - Background.FadeColour(backgroundColour, TRANSITION_DURATION, Easing.OutQuint); - SwitchContainer.ResizeWidthTo(EXPANDED_SWITCH_WIDTH, TRANSITION_DURATION, Easing.OutQuint); - MainContentContainer.TransformTo(nameof(Padding), new MarginPadding - { - Left = EXPANDED_SWITCH_WIDTH, - Right = CORNER_RADIUS - }, TRANSITION_DURATION, Easing.OutQuint); - TextBackground.FadeColour(backgroundTextColour, TRANSITION_DURATION, Easing.OutQuint); - TextFlow.FadeColour(ColourProvider.Background6, TRANSITION_DURATION, Easing.OutQuint); + textBackgroundColour = textBackgroundColour.Lighten(0.1f); + mainBackgroundColour = mainBackgroundColour.Lighten(0.1f); } - else + + // Mouse down adds a halfway tween of the movement + if (mouseDown) { - Colour4 backgroundColour = ColourProvider.Background3; - if (IsHovered) - backgroundColour = Interpolation.ValueAt(0.25f, backgroundColour, activeColour, 0, 1); - - Colour4 textBackgroundColour = ColourProvider.Background2; - if (IsHovered) - textBackgroundColour = Interpolation.ValueAt(0.25f, textBackgroundColour, activeColour, 0, 1); - - Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, textBackgroundColour), TRANSITION_DURATION, Easing.OutQuint); - Background.FadeColour(backgroundColour, TRANSITION_DURATION, Easing.OutQuint); - SwitchContainer.ResizeWidthTo(IDLE_SWITCH_WIDTH, TRANSITION_DURATION, Easing.OutQuint); - MainContentContainer.TransformTo(nameof(Padding), new MarginPadding - { - Left = IDLE_SWITCH_WIDTH, - Right = CORNER_RADIUS - }, TRANSITION_DURATION, Easing.OutQuint); - TextBackground.FadeColour(textBackgroundColour, TRANSITION_DURATION, Easing.OutQuint); - TextFlow.FadeColour(Colour4.White, TRANSITION_DURATION, Easing.OutQuint); + targetWidth = (float)Interpolation.Lerp(IDLE_SWITCH_WIDTH, EXPANDED_SWITCH_WIDTH, 0.5f); + transitionDuration *= 4; } + + Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(mainBackgroundColour, textBackgroundColour), transitionDuration, Easing.OutQuint); + Background.FadeColour(mainBackgroundColour, transitionDuration, Easing.OutQuint); + SwitchContainer.ResizeWidthTo(targetWidth, transitionDuration, Easing.OutQuint); + MainContentContainer.TransformTo(nameof(Padding), new MarginPadding + { + Left = targetWidth, + Right = CORNER_RADIUS + }, transitionDuration, Easing.OutQuint); + TextBackground.FadeColour(textBackgroundColour, transitionDuration, Easing.OutQuint); + TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint); } } } From 3f6bdc5585de6653642d8c6ec119e6917dcb67d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 15:40:06 +0900 Subject: [PATCH 217/306] Don't expose "mark as read" errors to the user via notifications This can happen if the user leaves the channel before the request is fired. You can't mark a channel as read when you're not in the channel. Addresses https://github.com/ppy/osu/discussions/16973. --- osu.Game/Online/Chat/ChannelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 77b52c34d9..38e655db71 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -618,7 +618,7 @@ namespace osu.Game.Online.Chat var req = new MarkChannelAsReadRequest(channel, message); req.Success += () => channel.LastReadId = message.Id; - req.Failure += e => Logger.Error(e, $"Failed to mark channel {channel} up to '{message}' as read"); + req.Failure += e => Logger.Log($"Failed to mark channel {channel} up to '{message}' as read ({e.Message})", LoggingTarget.Network, LogLevel.Verbose); api.Queue(req); } From 401cf2a955eb82d45afa03105090df7ecf6931eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 15:54:45 +0900 Subject: [PATCH 218/306] Allow pausing game-wide audio via hotkey as long as `LocalUserPlaying` is not set `Player` seems to handle this correctly locally already, which is to say if the user attempts to toggle the pause state incorrectly, it will still recover. The logic stoppic this operation was only in the key binding handler, which meant it was already possible from the now playing overlay this whole time, so I *think* this should be quite safe. --- osu.Game/Overlays/Music/MusicKeyBindingHandler.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index baee17fb00..6b33c9200e 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -30,17 +30,20 @@ namespace osu.Game.Overlays.Music [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } + [Resolved] + private OsuGame game { get; set; } + public bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) return false; - if (beatmap.Disabled) - return false; - switch (e.Action) { case GlobalAction.MusicPlay: + if (game.LocalUserPlaying.Value) + return false; + // use previous state as TogglePause may not update the track's state immediately (state update is run on the audio thread see https://github.com/ppy/osu/issues/9880#issuecomment-674668842) bool wasPlaying = musicController.IsPlaying; @@ -49,11 +52,17 @@ namespace osu.Game.Overlays.Music return true; case GlobalAction.MusicNext: + if (beatmap.Disabled) + return false; + musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast(GlobalActionKeyBindingStrings.MusicNext, e.Action))); return true; case GlobalAction.MusicPrev: + if (beatmap.Disabled) + return false; + musicController.PreviousTrack(res => { switch (res) From b4a54b38e71a960bcb6382aadc32f1cbe2ade358 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 16:02:16 +0900 Subject: [PATCH 219/306] Remove redundant parameter specification --- osu.Game/Online/Chat/ChannelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 38e655db71..47e45e67d1 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -618,7 +618,7 @@ namespace osu.Game.Online.Chat var req = new MarkChannelAsReadRequest(channel, message); req.Success += () => channel.LastReadId = message.Id; - req.Failure += e => Logger.Log($"Failed to mark channel {channel} up to '{message}' as read ({e.Message})", LoggingTarget.Network, LogLevel.Verbose); + req.Failure += e => Logger.Log($"Failed to mark channel {channel} up to '{message}' as read ({e.Message})", LoggingTarget.Network); api.Queue(req); } From c6d78b93257b172d044cc8932bd857555a82571d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 16:12:15 +0900 Subject: [PATCH 220/306] Fix several oversights in data linking causing drawable rooms not updating as expected --- osu.Game/Online/Rooms/Room.cs | 2 ++ .../Components/StarRatingRangeDisplay.cs | 16 ++++++---------- .../Screens/OnlinePlay/OnlinePlayComposite.cs | 3 +++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index c7f34905e2..a33150fe08 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -46,6 +46,7 @@ namespace osu.Game.Online.Rooms public readonly Bindable PlaylistItemStats = new Bindable(); [JsonProperty("difficulty_range")] + [Cached] public readonly Bindable DifficultyRange = new Bindable(); [Cached] @@ -190,6 +191,7 @@ namespace osu.Game.Online.Rooms QueueMode.Value = other.QueueMode.Value; DifficultyRange.Value = other.DifficultyRange.Value; PlaylistItemStats.Value = other.PlaylistItemStats.Value; + CurrentPlaylistItem.Value = other.CurrentPlaylistItem.Value; if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index 0b673006ef..7425e46bd3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -12,7 +11,6 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; -using osu.Game.Online.Rooms; using osuTK; namespace osu.Game.Screens.OnlinePlay.Components @@ -72,25 +70,23 @@ namespace osu.Game.Screens.OnlinePlay.Components }; } - [Resolved] - private Room room { get; set; } - protected override void LoadComplete() { base.LoadComplete(); - Playlist.BindCollectionChanged(updateRange, true); + DifficultyRange.BindValueChanged(_ => updateRange()); + Playlist.BindCollectionChanged((_, __) => updateRange(), true); } - private void updateRange(object sender, NotifyCollectionChangedEventArgs e) + private void updateRange() { StarDifficulty minDifficulty; StarDifficulty maxDifficulty; - if (room.DifficultyRange.Value != null) + if (DifficultyRange.Value != null) { - minDifficulty = new StarDifficulty(room.DifficultyRange.Value.Min, 0); - maxDifficulty = new StarDifficulty(room.DifficultyRange.Value.Max, 0); + minDifficulty = new StarDifficulty(DifficultyRange.Value.Min, 0); + maxDifficulty = new StarDifficulty(DifficultyRange.Value.Max, 0); } else { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 7d1feb0316..984880dc3c 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -41,6 +41,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected BindableList Playlist { get; private set; } + [Resolved(typeof(Room))] + protected Bindable DifficultyRange { get; private set; } + [Resolved(typeof(Room))] protected Bindable Category { get; private set; } From bb1aa032bdd70c62abb6d6fe3833dacde8b32a71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Feb 2022 16:20:40 +0900 Subject: [PATCH 221/306] Combine `SelectedItem` and `CurrentPlaylistItem` into same storage --- .../Lounge/Components/DrawableRoom.cs | 2 +- .../Match/MultiplayerMatchSettingsOverlay.cs | 4 ++-- .../Match/MultiplayerReadyButton.cs | 4 ++-- .../Match/Playlist/MultiplayerPlaylist.cs | 4 ++-- .../Screens/OnlinePlay/OnlinePlayComposite.cs | 22 ++++++++----------- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index a1a82c907a..5adce862a0 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -388,7 +388,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected override void LoadComplete() { base.LoadComplete(); - SelectedItem.BindValueChanged(onSelectedItemChanged, true); + CurrentPlaylistItem.BindValueChanged(onSelectedItemChanged, true); } private CancellationTokenSource beatmapLookupCancellation; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 7f1db733b3..be98a9d4e9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -343,7 +343,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match base.LoadComplete(); drawablePlaylist.Items.BindTo(Playlist); - drawablePlaylist.SelectedItem.BindTo(SelectedItem); + drawablePlaylist.SelectedItem.BindTo(CurrentPlaylistItem); } protected override void Update() @@ -419,7 +419,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (text.StartsWith(not_found_prefix, StringComparison.Ordinal)) { ErrorText.Text = "The selected beatmap is not available online."; - SelectedItem.Value.MarkInvalid(); + CurrentPlaylistItem.Value.MarkInvalid(); } else { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 06959d942f..023af85f3b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => updateState()); + CurrentPlaylistItem.BindValueChanged(_ => updateState()); } protected override void OnRoomUpdated() @@ -111,7 +111,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match bool enableButton = Room?.State == MultiplayerRoomState.Open - && SelectedItem.Value?.ID == Room.Settings.PlaylistItemId + && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired && !operationInProgress.Value; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 7b90532cce..eeafebfec0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -52,14 +52,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist queueList = new MultiplayerQueueList { RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem }, + SelectedItem = { BindTarget = CurrentPlaylistItem }, RequestEdit = item => RequestEdit?.Invoke(item) }, historyList = new MultiplayerHistoryList { RelativeSizeAxes = Axes.Both, Alpha = 0, - SelectedItem = { BindTarget = SelectedItem } + SelectedItem = { BindTarget = CurrentPlaylistItem } } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 984880dc3c..95d9b2af15 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -32,6 +32,10 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable Type { get; private set; } + /// + /// The currently selected item in the , or the current item from + /// if this is not within a . + /// [Resolved(typeof(Room))] protected Bindable CurrentPlaylistItem { get; private set; } @@ -80,12 +84,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] private IBindable subScreenSelectedItem { get; set; } - /// - /// The currently selected item in the , or the current item from - /// if this is not within a . - /// - protected readonly Bindable SelectedItem = new Bindable(); - protected override void LoadComplete() { base.LoadComplete(); @@ -96,13 +94,11 @@ namespace osu.Game.Screens.OnlinePlay protected void UpdateSelectedItem() { - if (RoomID.Value == null || subScreenSelectedItem == null) - { - SelectedItem.Value = CurrentPlaylistItem.Value ?? Playlist.GetCurrentItem(); - return; - } - - SelectedItem.Value = subScreenSelectedItem.Value; + // null room ID means this is a room in the process of being created. + if (RoomID.Value == null) + CurrentPlaylistItem.Value = Playlist.GetCurrentItem(); + else if (subScreenSelectedItem != null) + CurrentPlaylistItem.Value = subScreenSelectedItem.Value; } } } From 328166f0d56043199ce8e1de9e6af080dc22de35 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Feb 2022 17:01:04 +0900 Subject: [PATCH 222/306] Add failing test --- osu.Game.Tests/Online/TestAPIModJsonSerialization.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index 1b7a7656b5..d5ea3e492c 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -121,6 +121,17 @@ namespace osu.Game.Tests.Online Assert.That((deserialised?.Mods[0])?.Settings["speed_change"], Is.EqualTo(2)); } + [Test] + public void TestAPIModDetachedFromSource() + { + var mod = new OsuModDoubleTime { SpeedChange = { Value = 1.01 } }; + var apiMod = new APIMod(mod); + + mod.SpeedChange.Value = 1.5; + + Assert.That(apiMod.Settings["speed_change"], Is.EqualTo(1.01d)); + } + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => new Mod[] From 2acaffd5e72b80bbb90ae0a0b2e250527db7365c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Feb 2022 17:01:11 +0900 Subject: [PATCH 223/306] Fix APIMod storing bindables instead of value --- osu.Game/Online/API/APIMod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 62f9976c0f..67041cae07 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online.API var bindable = (IBindable)property.GetValue(mod); if (!bindable.IsDefault) - Settings.Add(property.Name.Underscore(), bindable); + Settings.Add(property.Name.Underscore(), ModUtils.GetSettingUnderlyingValue(bindable)); } } From 7193bc8554a0fb3db8cc2fd08fb31f3082fc91fa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Feb 2022 17:01:43 +0900 Subject: [PATCH 224/306] Fix playlists comparing mod equality via APIMod --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 8403e1e0fe..7efeae8129 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Extensions; -using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -40,8 +39,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (ruleset.Value.OnlineID != PlaylistItem.RulesetID) throw new InvalidOperationException("Current Ruleset does not match PlaylistItem's Ruleset"); - var localMods = Mods.Value.Select(m => new APIMod(m)).ToArray(); - if (!PlaylistItem.RequiredMods.All(m => localMods.Any(m.Equals))) + var requiredLocalMods = PlaylistItem.RequiredMods.Select(m => m.ToMod(GameplayState.Ruleset)); + if (!requiredLocalMods.All(m => Mods.Value.Any(m.Equals))) throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods"); } From 255b3b067b3774b39a4f0cbf6f07555b5d3fbd2b Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Thu, 24 Feb 2022 10:13:27 +0100 Subject: [PATCH 225/306] Remove track fade --- osu.Game/Screens/Menu/MainMenu.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index a2cb448d40..81e03b94ae 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -309,18 +309,8 @@ namespace osu.Game.Screens.Menu if (e.Action == GlobalAction.Back && host.CanSuspendToBackground) { - bool didSuspend = host.SuspendToBackground(); - - if (didSuspend) - { - // fade the track so the Bass.Pause() on suspend isn't as jarring. - const double fade_time = 500; - musicController.CurrentTrack - .VolumeTo(0, fade_time, Easing.Out).Then() - .VolumeTo(1, fade_time, Easing.In); - + if (host.SuspendToBackground()) return true; - } } return false; From 6f29cbccd140cd222d3c72e8c2b17c94080ae43c Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Thu, 24 Feb 2022 10:36:10 +0100 Subject: [PATCH 226/306] Remove unused using --- osu.Game/Screens/Menu/MainMenu.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 81e03b94ae..0357332cf6 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; From c5b1e5cbf84d9c7f2ea52caa8b5e83b67c54b7d7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Feb 2022 20:11:58 +0900 Subject: [PATCH 227/306] Fix union resolver failing on multiple derived types --- osu.Game/Online/SignalRUnionWorkaroundResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/SignalRUnionWorkaroundResolver.cs b/osu.Game/Online/SignalRUnionWorkaroundResolver.cs index e64f9ed91c..c96f93df78 100644 --- a/osu.Game/Online/SignalRUnionWorkaroundResolver.cs +++ b/osu.Game/Online/SignalRUnionWorkaroundResolver.cs @@ -27,7 +27,7 @@ namespace osu.Game.Online // This should not be required. The fallback should work. But something is weird with the way caching is done. // For future adventurers, I would not advise looking into this further. It's likely not worth the effort. - baseMap = baseMap.Concat(baseMap.Select(t => (t.baseType, t.baseType))); + baseMap = baseMap.Concat(baseMap.Select(t => (t.baseType, t.baseType)).Distinct()); return new Dictionary(baseMap.Select(t => { From cd8190b2e744f635e86fe197cd6b360c9460963d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Feb 2022 20:45:15 +0900 Subject: [PATCH 228/306] Remove Microsoft.CodeAnalysis.NetAnalyzers package --- Directory.Build.props | 1 - 1 file changed, 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index c1682638c2..5bdf12218c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,6 @@ - $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset From c189cc5d002c0a14ab15b83cdc6c462c2d03f78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Feb 2022 21:01:37 +0100 Subject: [PATCH 229/306] Remove unused using directive --- osu.Game/Overlays/Mods/ModPanel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 13e2b5bb0b..af8ad3eb18 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.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.Audio; using osu.Framework.Audio.Sample; From 33a87976a8ac17f8ac0d559b254c6c4a0862c35e Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Thu, 24 Feb 2022 21:11:49 +0100 Subject: [PATCH 230/306] Rewrite to read better Co-authored-by: Dean Herbert --- osu.Game/Screens/Menu/MainMenu.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 0357332cf6..b0208a0ae8 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -306,10 +307,13 @@ namespace osu.Game.Screens.Menu if (e.Repeat) return false; - if (e.Action == GlobalAction.Back && host.CanSuspendToBackground) + switch (e.Action) { - if (host.SuspendToBackground()) - return true; + case GlobalAction.Back: + // In the case of a host being able to exit, the back action is handled by ExitConfirmOverlay. + Debug.Assert(!host.CanExit); + + return host.SuspendToBackground(); } return false; From f9d9ad388b565a517bc2ad918368265a6a05fe56 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 25 Feb 2022 16:03:28 +0900 Subject: [PATCH 231/306] Add chat display to multiplayer spectator screen --- .../TestSceneMultiSpectatorScreen.cs | 7 ++--- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- .../Spectate/MultiSpectatorScreen.cs | 27 +++++++++++++------ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 7ce0c6a94d..77a06e5746 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -15,6 +15,7 @@ using osu.Game.Configuration; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.UI; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; @@ -377,7 +378,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap); Ruleset.Value = importedBeatmap.Ruleset; - LoadScreen(spectatorScreen = new TestMultiSpectatorScreen(playingUsers.ToArray(), gameplayStartTime)); + LoadScreen(spectatorScreen = new TestMultiSpectatorScreen(SelectedRoom.Value, playingUsers.ToArray(), gameplayStartTime)); }); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); @@ -465,8 +466,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { private readonly double? gameplayStartTime; - public TestMultiSpectatorScreen(MultiplayerRoomUser[] users, double? gameplayStartTime = null) - : base(users) + public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? gameplayStartTime = null) + : base(room, users) { this.gameplayStartTime = gameplayStartTime; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 429b0ad89b..c78dcb7cb6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -449,7 +449,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer switch (client.LocalUser.State) { case MultiplayerUserState.Spectating: - return new MultiSpectatorScreen(users.Take(PlayerGrid.MAX_PLAYERS).ToArray()); + return new MultiSpectatorScreen(Room, users.Take(PlayerGrid.MAX_PLAYERS).ToArray()); default: return new MultiplayerPlayerLoader(() => new MultiplayerPlayer(Room, SelectedItem.Value, users)); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index e5eeeb3448..1cc4497675 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -11,10 +11,12 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Online.Spectator; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Spectate; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { @@ -48,15 +50,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private PlayerArea currentAudioSource; private bool canStartMasterClock; + private readonly Room room; private readonly MultiplayerRoomUser[] users; /// /// Creates a new . /// + /// The room. /// The players to spectate. - public MultiSpectatorScreen(MultiplayerRoomUser[] users) + public MultiSpectatorScreen(Room room, MultiplayerRoomUser[] users) : base(users.Select(u => u.UserID).ToArray()) { + this.room = room; this.users = users; instances = new PlayerArea[Users.Count]; @@ -65,7 +70,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [BackgroundDependencyLoader] private void load() { - Container leaderboardContainer; + FillFlowContainer leaderboardFlow; Container scoreDisplayContainer; masterClockContainer = CreateMasterGameplayClockContainer(Beatmap.Value); @@ -97,10 +102,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { new Drawable[] { - leaderboardContainer = new Container + leaderboardFlow = new FillFlowContainer { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5) }, grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } } @@ -125,14 +133,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, users) { Expanded = { Value = true }, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, }, l => { foreach (var instance in instances) leaderboard.AddClock(instance.UserId, instance.GameplayClock); - leaderboardContainer.Add(leaderboard); + leaderboardFlow.Insert(0, leaderboard); if (leaderboard.TeamScores.Count == 2) { @@ -143,6 +149,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }, scoreDisplayContainer.Add); } }); + + LoadComponentAsync(new GameplayChatDisplay(room) + { + Expanded = { Value = true }, + }, chat => leaderboardFlow.Insert(1, chat)); } protected override void LoadComplete() From 48ed9c61442ca05bb7fda4eaf8d7c64f8a37cdad Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 25 Feb 2022 16:03:46 +0900 Subject: [PATCH 232/306] Enable high chat polling rate --- osu.Game/OsuGame.cs | 3 ++- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 3 +++ osu.Game/Users/UserActivity.cs | 10 ++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fa5a336b7c..fb81e4fd14 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -838,7 +838,8 @@ namespace osu.Game channelManager.HighPollRate.Value = chatOverlay.State.Value == Visibility.Visible || API.Activity.Value is UserActivity.InLobby - || API.Activity.Value is UserActivity.InMultiplayerGame; + || API.Activity.Value is UserActivity.InMultiplayerGame + || API.Activity.Value is UserActivity.SpectatingMultiplayerGame; } Add(difficultyRecommender); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 1cc4497675..3bb76c4a76 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -16,6 +16,7 @@ using osu.Game.Online.Spectator; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Spectate; +using osu.Game.Users; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate @@ -36,6 +37,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true); + protected override UserActivity InitialActivity => new UserActivity.SpectatingMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); + [Resolved] private OsuColour colours { get; set; } diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 516aa80652..2f945d6e1c 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -50,6 +50,16 @@ namespace osu.Game.Users public override string Status => $@"{base.Status} with others"; } + public class SpectatingMultiplayerGame : InGame + { + public SpectatingMultiplayerGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) + : base(beatmapInfo, ruleset) + { + } + + public override string Status => $"Watching others {base.Status.ToLowerInvariant()}"; + } + public class InPlaylistGame : InGame { public InPlaylistGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) From 387ae59bc4108372afaa75f3050b10eb04ee26dc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 25 Feb 2022 16:12:25 +0900 Subject: [PATCH 233/306] Fix nullref in tests --- .../OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index 53fd111c27..d08a63e21f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.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.Bindables; using osu.Framework.Graphics; @@ -15,10 +16,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class GameplayChatDisplay : MatchChatDisplay, IKeyBindingHandler { - [Resolved] + [Resolved(CanBeNull = true)] + [CanBeNull] private ILocalUserPlayInfo localUserInfo { get; set; } - private IBindable localUserPlaying = new Bindable(); + private readonly IBindable localUserPlaying = new Bindable(); public override bool PropagatePositionalInputSubTree => !localUserPlaying.Value; @@ -46,7 +48,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); + if (localUserInfo != null) + localUserPlaying.BindTo(localUserInfo.IsPlaying); + localUserPlaying.BindValueChanged(playing => { // for now let's never hold focus. this avoid misdirected gameplay keys entering chat. From 8eef1774d5b2b6a58f67a17d1ca3804ce2dbbf76 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 25 Feb 2022 20:18:22 +0900 Subject: [PATCH 234/306] Use Logger.Log instead of console --- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index d0b89e981d..70baf69037 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -279,7 +279,7 @@ namespace osu.Game.Online.Spectator totalBundledFrames += frames.Length; - Console.WriteLine($"Purging {pendingFrames.Count} frames (total {totalBundledFrames})"); + Logger.Log($"Purging {pendingFrames.Count} frames (total {totalBundledFrames})"); pendingFrames.Clear(); lastPurgeTime = Time.Current; From 67082b8c5de9d97d353ea973f34bc6773af4029f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Feb 2022 22:25:36 +0900 Subject: [PATCH 235/306] Remove verbose logging from `SpectatorClient` for now --- osu.Game/Online/Spectator/SpectatorClient.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 70baf69037..8f22078010 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -86,8 +86,6 @@ namespace osu.Game.Online.Spectator private Task? lastSend; - private int totalBundledFrames; - private const int max_pending_frames = 30; [BackgroundDependencyLoader] @@ -169,8 +167,6 @@ namespace osu.Game.Online.Spectator IsPlaying = true; - totalBundledFrames = 0; - // transfer state at point of beginning play currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; @@ -277,10 +273,6 @@ namespace osu.Game.Online.Spectator var frames = pendingFrames.ToArray(); var bundle = new FrameDataBundle(currentScore.ScoreInfo, frames); - totalBundledFrames += frames.Length; - - Logger.Log($"Purging {pendingFrames.Count} frames (total {totalBundledFrames})"); - pendingFrames.Clear(); lastPurgeTime = Time.Current; From 926108af7da97c9f0114702ba94adc0f3643c4c7 Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 25 Feb 2022 14:15:19 +0100 Subject: [PATCH 236/306] Change launch args from dotnet 5 to dotnet 6 --- .vscode/launch.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1b590008cd..d93fddf42d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Desktop/bin/Debug/net5.0/osu!.dll" + "${workspaceRoot}/osu.Desktop/bin/Debug/net6.0/osu!.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Debug)", @@ -19,7 +19,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Desktop/bin/Release/net5.0/osu!.dll" + "${workspaceRoot}/osu.Desktop/bin/Release/net6.0/osu!.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Release)", @@ -31,7 +31,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Game.Tests/bin/Debug/net5.0/osu.Game.Tests.dll" + "${workspaceRoot}/osu.Game.Tests/bin/Debug/net6.0/osu.Game.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tests (Debug)", @@ -43,7 +43,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Game.Tests/bin/Release/net5.0/osu.Game.Tests.dll" + "${workspaceRoot}/osu.Game.Tests/bin/Release/net6.0/osu.Game.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tests (Release)", @@ -55,7 +55,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Desktop/bin/Debug/net5.0/osu!.dll", + "${workspaceRoot}/osu.Desktop/bin/Debug/net6.0/osu!.dll", "--tournament" ], "cwd": "${workspaceRoot}", @@ -68,7 +68,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Desktop/bin/Release/net5.0/osu!.dll", + "${workspaceRoot}/osu.Desktop/bin/Release/net6.0/osu!.dll", "--tournament" ], "cwd": "${workspaceRoot}", @@ -81,7 +81,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net5.0/osu.Game.Tournament.Tests.dll", + "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net6.0/osu.Game.Tournament.Tests.dll", "--tournament" ], "cwd": "${workspaceRoot}", @@ -94,7 +94,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net5.0/osu.Game.Tournament.Tests.dll", + "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/net6.0/osu.Game.Tournament.Tests.dll", "--tournament" ], "cwd": "${workspaceRoot}", @@ -105,7 +105,7 @@ "name": "Benchmark", "type": "coreclr", "request": "launch", - "program": "${workspaceRoot}/osu.Game.Benchmarks/bin/Release/net5.0/osu.Game.Benchmarks.dll", + "program": "${workspaceRoot}/osu.Game.Benchmarks/bin/Release/net6.0/osu.Game.Benchmarks.dll", "args": [ "--filter", "*" From 1a7a160f0a5a10345fd1537bf106022589038b18 Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 25 Feb 2022 14:26:44 +0100 Subject: [PATCH 237/306] Update vscode launch.json files for all other projects --- .../osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json | 4 ++-- .../osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json | 4 ++-- .../.vscode/launch.json | 4 ++-- .../osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json | 4 ++-- osu.Game.Rulesets.Catch.Tests/.vscode/launch.json | 4 ++-- osu.Game.Rulesets.Mania.Tests/.vscode/launch.json | 4 ++-- osu.Game.Rulesets.Osu.Tests/.vscode/launch.json | 4 ++-- osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json | 4 ++-- osu.Game.Tournament.Tests/.vscode/launch.json | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json index fd03878699..b433819346 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll" + "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll" + "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.EmptyFreeformRuleset.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json index bd9db14259..d60bc2571d 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll" + "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll" + "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json index 24e4873ed6..f1f37f6363 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll" + "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll" + "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.EmptyScrolling.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json index bd9db14259..d60bc2571d 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll" + "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Pippidon.Tests.dll" + "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Pippidon.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json index 9aaaf418c2..201343a036 100644 --- a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Catch.Tests.dll" + "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Catch.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Catch.Tests.dll" + "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Catch.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json index e3d7956e85..f6a067a831 100644 --- a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Mania.Tests.dll" + "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Mania.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Mania.Tests.dll" + "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Mania.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json index 01a5985464..61be25b845 100644 --- a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Osu.Tests.dll" + "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Osu.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Osu.Tests.dll" + "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Osu.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json index 779ebba9ae..56ec7d8d9c 100644 --- a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Rulesets.Taiko.Tests.dll" + "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Rulesets.Taiko.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/net5.0/osu.Game.Rulesets.Taiko.Tests.dll" + "${workspaceRoot}/bin/Release/net6.0/osu.Game.Rulesets.Taiko.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/osu.Game.Tournament.Tests/.vscode/launch.json b/osu.Game.Tournament.Tests/.vscode/launch.json index 28532d3ed3..51aa541811 100644 --- a/osu.Game.Tournament.Tests/.vscode/launch.json +++ b/osu.Game.Tournament.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/net5.0/osu.Game.Tournament.Tests.dll" + "${workspaceRoot}/bin/Debug/net6.0/osu.Game.Tournament.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/net5.0/osu.Game.Tournament.Tests.dll" + "${workspaceRoot}/bin/Release/net6.0/osu.Game.Tournament.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", From c7f2759eb92385acb9e80f1cfd5addc79ad28ab7 Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 25 Feb 2022 14:27:54 +0100 Subject: [PATCH 238/306] Update build configurations for Rider to net6.0 paths --- .../.idea/runConfigurations/Benchmarks.xml | 6 +++--- .../.idea/runConfigurations/CatchRuleset__Tests_.xml | 6 +++--- .../.idea/runConfigurations/ManiaRuleset__Tests_.xml | 6 +++--- .../.idea/runConfigurations/OsuRuleset__Tests_.xml | 6 +++--- .../.idea/runConfigurations/TaikoRuleset__Tests_.xml | 6 +++--- .../.idea/runConfigurations/Tournament.xml | 6 +++--- .../.idea/runConfigurations/Tournament__Tests_.xml | 6 +++--- .idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml | 6 +++--- .../.idea/runConfigurations/osu___Tests_.xml | 6 +++--- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml index 498a710df9..d500c595c0 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml @@ -1,8 +1,8 @@ - Custom, - - First = Common, - Last = Custom } } From 7de5dad4f0ae616c7b3803f3a4b1d2d260b33bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Feb 2022 19:23:02 +0100 Subject: [PATCH 247/306] Add test coverage for divisor behaviour --- .../Editing/TestSceneBeatDivisorControl.cs | 115 ++++++++++++++++++ .../Compose/Components/BeatDivisorControl.cs | 6 +- 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index e37019e5d3..4503b1a5f6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -81,5 +82,119 @@ namespace osu.Game.Tests.Visual.Editing sliderDrawQuad.Centre.Y ); } + + [Test] + public void TestBeatChevronNavigation() + { + switchBeatSnap(1); + assertBeatSnap(1); + + switchBeatSnap(3); + assertBeatSnap(8); + + switchBeatSnap(-1); + assertBeatSnap(4); + + switchBeatSnap(-3); + assertBeatSnap(16); + } + + [Test] + public void TestBeatPresetNavigation() + { + assertPreset(BeatDivisorType.Common); + + switchPresets(1); + assertPreset(BeatDivisorType.Triplets); + + switchPresets(1); + assertPreset(BeatDivisorType.Common); + + switchPresets(-1); + assertPreset(BeatDivisorType.Triplets); + + switchPresets(-1); + assertPreset(BeatDivisorType.Common); + + setDivisorViaInput(3); + assertPreset(BeatDivisorType.Triplets); + + setDivisorViaInput(8); + assertPreset(BeatDivisorType.Common); + + setDivisorViaInput(15); + assertPreset(BeatDivisorType.Custom, 15); + + switchBeatSnap(-1); + assertBeatSnap(5); + + switchBeatSnap(-1); + assertBeatSnap(3); + + setDivisorViaInput(5); + assertPreset(BeatDivisorType.Custom, 15); + + switchPresets(1); + assertPreset(BeatDivisorType.Common); + + switchPresets(-1); + assertPreset(BeatDivisorType.Triplets); + } + + private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () => + { + int chevronIndex = direction > 0 ? 1 : 0; + var chevronButton = beatDivisorControl.ChildrenOfType().ElementAt(chevronIndex); + InputManager.MoveMouseTo(chevronButton); + InputManager.Click(MouseButton.Left); + }, Math.Abs(direction)); + + private void assertBeatSnap(int expected) => AddAssert($"beat snap is {expected}", + () => bindableBeatDivisor.Value == expected); + + private void switchPresets(int direction) => AddRepeatStep($"move presets {(direction > 0 ? "forward" : "backward")}", () => + { + int chevronIndex = direction > 0 ? 3 : 2; + var chevronButton = beatDivisorControl.ChildrenOfType().ElementAt(chevronIndex); + InputManager.MoveMouseTo(chevronButton); + InputManager.Click(MouseButton.Left); + }, Math.Abs(direction)); + + private void assertPreset(BeatDivisorType type, int? maxDivisor = null) + { + AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type == type); + + if (type == BeatDivisorType.Custom) + { + Debug.Assert(maxDivisor != null); + AddAssert($"max divisor is {maxDivisor}", () => bindableBeatDivisor.ValidDivisors.Value.Presets.Max() == maxDivisor.Value); + } + } + + private void setDivisorViaInput(int divisor) + { + AddStep("open divisor input popover", () => + { + var button = beatDivisorControl.ChildrenOfType().Single(); + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + BeatDivisorControl.CustomDivisorPopover popover = null; + AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().SingleOrDefault()) != null && popover.IsLoaded); + AddStep($"set divisor to {divisor}", () => + { + var textBox = popover.ChildrenOfType().Single(); + InputManager.MoveMouseTo(textBox); + InputManager.Click(MouseButton.Left); + textBox.Text = divisor.ToString(); + InputManager.Key(Key.Enter); + }); + AddStep("dismiss popover", () => + { + InputManager.Key(Key.Escape); + }); + AddUntilStep("wait for dismiss", () => !this.ChildrenOfType().Any()); + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 04e9ac842c..d5b0ab19aa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -214,7 +214,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private class DivisorDisplay : OsuAnimatedButton, IHasPopover + internal class DivisorDisplay : OsuAnimatedButton, IHasPopover { public BindableBeatDivisor BeatDivisor { get; } = new BindableBeatDivisor(); @@ -264,7 +264,7 @@ namespace osu.Game.Screens.Edit.Compose.Components }; } - private class CustomDivisorPopover : OsuPopover + internal class CustomDivisorPopover : OsuPopover { public BindableBeatDivisor BeatDivisor { get; } = new BindableBeatDivisor(); @@ -347,7 +347,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private class ChevronButton : IconButton + internal class ChevronButton : IconButton { public ChevronButton() { From a5600516f0200a8e596fa2011ba488106259bc1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Feb 2022 20:13:44 +0100 Subject: [PATCH 248/306] Fix test failures --- .../Visual/Editing/TestSceneBeatDivisorControl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index 4503b1a5f6..94c0822235 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -43,10 +43,10 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestBindableBeatDivisor() { - AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 4); + AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 2); AddAssert("divisor is 4", () => bindableBeatDivisor.Value == 4); - AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 3); - AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 12); + AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 1); + AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 8); } [Test] From 2e04a83554b982d3468e3692dbd70bef36e6c46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Feb 2022 17:52:16 +0100 Subject: [PATCH 249/306] Implement column display for new mod design --- .../UserInterface/TestSceneModColumn.cs | 48 +++++ osu.Game/Overlays/Mods/ModColumn.cs | 196 ++++++++++++++++++ osu.Game/Overlays/Mods/ModPanel.cs | 4 +- 3 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs create mode 100644 osu.Game/Overlays/Mods/ModColumn.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs new file mode 100644 index 0000000000..59641ae000 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneModColumn : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + [TestCase(ModType.DifficultyReduction)] + [TestCase(ModType.DifficultyIncrease)] + [TestCase(ModType.Conversion)] + [TestCase(ModType.Automation)] + [TestCase(ModType.Fun)] + public void TestBasic(ModType modType) + { + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = new ModColumn(modType) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + + AddStep("change ruleset to osu!", () => Ruleset.Value = new OsuRuleset().RulesetInfo); + AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); + AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo); + AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs new file mode 100644 index 0000000000..03ef3b889a --- /dev/null +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -0,0 +1,196 @@ +// 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 System.Threading; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; +using osuTK; + +#nullable enable + +namespace osu.Game.Overlays.Mods +{ + public class ModColumn : CompositeDrawable + { + private readonly ModType modType; + + private readonly Bindable>> availableMods = new Bindable>>(); + + private readonly TextFlowContainer headerText; + private readonly Box headerBackground; + private readonly Container contentContainer; + private readonly Box contentBackground; + private readonly FillFlowContainer panelFlow; + + private Colour4 accentColour; + + private const float header_height = 60; + + public ModColumn(ModType modType) + { + this.modType = modType; + + Width = 450; + RelativeSizeAxes = Axes.Y; + Shear = new Vector2(ModPanel.SHEAR_X, 0); + CornerRadius = ModPanel.CORNER_RADIUS; + Masking = true; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Height = header_height + ModPanel.CORNER_RADIUS, + Children = new Drawable[] + { + headerBackground = new Box + { + RelativeSizeAxes = Axes.X, + Height = header_height + ModPanel.CORNER_RADIUS + }, + headerText = new OsuTextFlowContainer(t => + { + t.Font = OsuFont.TorusAlternate.With(size: 24); + t.Shadow = false; + t.Colour = Colour4.Black; + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Shear = new Vector2(-ModPanel.SHEAR_X, 0), + Padding = new MarginPadding + { + Horizontal = 15, + Bottom = ModPanel.CORNER_RADIUS + } + } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = header_height }, + Child = contentContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = ModPanel.CORNER_RADIUS, + BorderThickness = 4, + Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10) + }, + Content = new[] + { + new[] + { + Empty() + }, + new Drawable[] + { + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarOverlapsContent = false, + Child = panelFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 10), + Padding = new MarginPadding + { + Horizontal = 10 + }, + } + } + }, + new[] { Empty() } + } + } + } + } + } + }; + + createHeaderText(); + } + + private void createHeaderText() + { + IEnumerable headerTextWords = modType.Humanize(LetterCasing.Title).Split(' '); + + if (headerTextWords.Count() > 1) + { + headerText.AddText($"{headerTextWords.First()} ", t => t.Font = t.Font.With(weight: FontWeight.SemiBold)); + headerTextWords = headerTextWords.Skip(1); + } + + headerText.AddText(string.Join(' ', headerTextWords)); + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours) + { + availableMods.BindTo(game.AvailableMods); + + headerBackground.Colour = accentColour = colours.ForModType(modType); + + contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3); + contentBackground.Colour = colourProvider.Background4; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods), true); + } + + private CancellationTokenSource? cancellationTokenSource; + + private void updateMods() + { + var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(modType) ?? Array.Empty()).ToList(); + + if (newMods.SequenceEqual(panelFlow.Children.Select(p => p.Mod))) + return; + + cancellationTokenSource?.Cancel(); + + var panels = newMods.Select(mod => new ModPanel(mod) + { + Shear = new Vector2(-ModPanel.SHEAR_X, 0) + }); + + LoadComponentsAsync(panels, loaded => + { + panelFlow.ChildrenEnumerable = loaded; + }, (cancellationTokenSource = new CancellationTokenSource()).Token); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index af8ad3eb18..446ebe12c5 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -40,10 +40,10 @@ namespace osu.Game.Overlays.Mods protected const double TRANSITION_DURATION = 150; - protected const float SHEAR_X = 0.2f; + public const float SHEAR_X = 0.2f; + public const float CORNER_RADIUS = 7; protected const float HEIGHT = 42; - protected const float CORNER_RADIUS = 7; protected const float IDLE_SWITCH_WIDTH = 54; protected const float EXPANDED_SWITCH_WIDTH = 70; From f40bd394871ddff25ef2a50827500663c747df8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Feb 2022 18:33:13 +0100 Subject: [PATCH 250/306] Add toggle all checkbox to column display --- osu.Game/Overlays/Mods/ModColumn.cs | 85 ++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 03ef3b889a..a42b2bcb96 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -8,15 +8,19 @@ using System.Threading; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osu.Game.Utils; using osuTK; +using osuTK.Graphics; #nullable enable @@ -33,12 +37,13 @@ namespace osu.Game.Overlays.Mods private readonly Container contentContainer; private readonly Box contentBackground; private readonly FillFlowContainer panelFlow; + private readonly ToggleAllCheckbox? toggleAllCheckbox; private Colour4 accentColour; private const float header_height = 60; - public ModColumn(ModType modType) + public ModColumn(ModType modType, bool allowBulkSelection) { this.modType = modType; @@ -48,6 +53,7 @@ namespace osu.Game.Overlays.Mods CornerRadius = ModPanel.CORNER_RADIUS; Masking = true; + Container controlContainer; InternalChildren = new Drawable[] { new Container @@ -108,9 +114,13 @@ namespace osu.Game.Overlays.Mods }, Content = new[] { - new[] + new Drawable[] { - Empty() + controlContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 20 } + } }, new Drawable[] { @@ -139,6 +149,18 @@ namespace osu.Game.Overlays.Mods }; createHeaderText(); + + if (allowBulkSelection) + { + controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + LabelText = "Enable All", + Shear = new Vector2(-ModPanel.SHEAR_X, 0) + }); + } } private void createHeaderText() @@ -161,6 +183,12 @@ namespace osu.Game.Overlays.Mods headerBackground.Colour = accentColour = colours.ForModType(modType); + if (toggleAllCheckbox != null) + { + toggleAllCheckbox.AccentColour = accentColour; + toggleAllCheckbox.AccentHoverColour = accentColour.Lighten(0.3f); + } + contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3); contentBackground.Colour = colourProvider.Background4; } @@ -192,5 +220,56 @@ namespace osu.Game.Overlays.Mods panelFlow.ChildrenEnumerable = loaded; }, (cancellationTokenSource = new CancellationTokenSource()).Token); } + + private class ToggleAllCheckbox : OsuCheckbox + { + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + accentColour = value; + updateState(); + } + } + + private Color4 accentHoverColour; + + public Color4 AccentHoverColour + { + get => accentHoverColour; + set + { + accentHoverColour = value; + updateState(); + } + } + + public ToggleAllCheckbox() + : base(false) + { + } + + protected override void ApplyLabelParameters(SpriteText text) + { + base.ApplyLabelParameters(text); + text.Font = text.Font.With(weight: FontWeight.SemiBold); + } + + [BackgroundDependencyLoader] + private void load() + { + updateState(); + } + + private void updateState() + { + Nub.AccentColour = AccentColour; + Nub.GlowingAccentColour = AccentHoverColour; + Nub.GlowColour = AccentHoverColour.Opacity(0.2f); + } + } } } From 53e8072632ae3302ec6ee56c0f45e2fddecf7946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Feb 2022 18:45:04 +0100 Subject: [PATCH 251/306] Port multiselection from previous design --- .../UserInterface/TestSceneModColumn.cs | 17 +++- osu.Game/Overlays/Mods/ModColumn.cs | 81 ++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 59641ae000..e58649d989 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.UserInterface { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(30), - Child = new ModColumn(modType) + Child = new ModColumn(modType, false) { Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -44,5 +44,20 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo); AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); } + + [Test] + public void TestMultiSelection() + { + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = new ModColumn(ModType.DifficultyIncrease, true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + } } } diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index a42b2bcb96..8ca968368e 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -152,7 +152,8 @@ namespace osu.Game.Overlays.Mods if (allowBulkSelection) { - controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox + controlContainer.Height = 50; + controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -218,9 +219,72 @@ namespace osu.Game.Overlays.Mods LoadComponentsAsync(panels, loaded => { panelFlow.ChildrenEnumerable = loaded; + foreach (var panel in panelFlow) + panel.Active.BindValueChanged(_ => updateToggleState()); + updateToggleState(); }, (cancellationTokenSource = new CancellationTokenSource()).Token); } + #region Bulk select / deselect + + private const double initial_multiple_selection_delay = 120; + + private double selectionDelay = initial_multiple_selection_delay; + private double lastSelection; + + private readonly Queue pendingSelectionOperations = new Queue(); + + protected override void Update() + { + base.Update(); + + if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay) + { + if (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) + { + dequeuedAction(); + + // each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements). + selectionDelay = Math.Max(30, selectionDelay * 0.8f); + lastSelection = Time.Current; + } + else + { + // reset the selection delay after all animations have been completed. + // this will cause the next action to be immediately performed. + selectionDelay = initial_multiple_selection_delay; + } + } + } + + private void updateToggleState() + { + if (toggleAllCheckbox != null && pendingSelectionOperations.Count == 0) + toggleAllCheckbox.Current.Value = panelFlow.All(panel => panel.Active.Value); + } + + /// + /// Selects all mods. + /// + public void SelectAll() + { + pendingSelectionOperations.Clear(); + + foreach (var button in panelFlow.Where(b => !b.Active.Value)) + pendingSelectionOperations.Enqueue(() => button.Active.Value = true); + } + + /// + /// Deselects all mods. + /// + public void DeselectAll() + { + pendingSelectionOperations.Clear(); + + foreach (var button in panelFlow.Where(b => b.Active.Value)) + pendingSelectionOperations.Enqueue(() => button.Active.Value = false); + } + private class ToggleAllCheckbox : OsuCheckbox { private Color4 accentColour; @@ -247,9 +311,12 @@ namespace osu.Game.Overlays.Mods } } - public ToggleAllCheckbox() + private readonly ModColumn column; + + public ToggleAllCheckbox(ModColumn column) : base(false) { + this.column = column; } protected override void ApplyLabelParameters(SpriteText text) @@ -270,6 +337,16 @@ namespace osu.Game.Overlays.Mods Nub.GlowingAccentColour = AccentHoverColour; Nub.GlowColour = AccentHoverColour.Opacity(0.2f); } + + protected override void OnUserChange(bool value) + { + if (value) + column.SelectAll(); + else + column.DeselectAll(); + } } + + #endregion } } From a80b4334ff841c5e09662159589b9354c39f40ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 12:18:59 +0100 Subject: [PATCH 252/306] Tweak layout of column display for better spacing --- osu.Game/Overlays/Mods/ModColumn.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 8ca968368e..15b2874edb 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -108,9 +108,8 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.Both, RowDimensions = new[] { - new Dimension(GridSizeMode.Absolute, 50), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10) + new Dimension(GridSizeMode.AutoSize), + new Dimension() }, Content = new[] { @@ -118,7 +117,7 @@ namespace osu.Game.Overlays.Mods { controlContainer = new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, Padding = new MarginPadding { Horizontal = 20 } } }, @@ -133,14 +132,10 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0, 10), - Padding = new MarginPadding - { - Horizontal = 10 - }, + Padding = new MarginPadding(10) } } - }, - new[] { Empty() } + } } } } @@ -161,6 +156,12 @@ namespace osu.Game.Overlays.Mods LabelText = "Enable All", Shear = new Vector2(-ModPanel.SHEAR_X, 0) }); + panelFlow.Padding = new MarginPadding + { + Top = 0, + Bottom = 10, + Horizontal = 10 + }; } } From fe4e4bf9c5295d04bfcbac3e38e9127b3855b0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 13:09:06 +0100 Subject: [PATCH 253/306] Add test coverage of multiselection behaviour --- .../UserInterface/TestSceneModColumn.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index e58649d989..0401d516ed 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.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.Linq; 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.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Catch; @@ -12,11 +16,12 @@ using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneModColumn : OsuTestScene + public class TestSceneModColumn : OsuManualInputManagerTestScene { [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); @@ -58,6 +63,25 @@ namespace osu.Game.Tests.Visual.UserInterface Origin = Anchor.Centre } }); + + clickToggle(); + AddUntilStep("all panels selected", () => this.ChildrenOfType().All(panel => panel.Active.Value)); + + clickToggle(); + AddUntilStep("all panels deselected", () => this.ChildrenOfType().All(panel => !panel.Active.Value)); + + AddStep("manually activate all panels", () => this.ChildrenOfType().ForEach(panel => panel.Active.Value = true)); + AddUntilStep("checkbox selected", () => this.ChildrenOfType().Single().Current.Value); + + AddStep("deselect first panel", () => this.ChildrenOfType().First().Active.Value = false); + AddUntilStep("checkbox selected", () => !this.ChildrenOfType().Single().Current.Value); + + void clickToggle() => AddStep("click toggle", () => + { + var checkbox = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(checkbox); + InputManager.Click(MouseButton.Left); + }); } } } From a83f96b026a9d5657d60eda44b594ef090014d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 13:40:52 +0100 Subject: [PATCH 254/306] Add filtering support to mod column --- .../UserInterface/TestSceneModColumn.cs | 50 +++++++++++++++++++ osu.Game/Overlays/Mods/ModColumn.cs | 37 ++++++++++++-- osu.Game/Overlays/Mods/ModPanel.cs | 17 +++++++ 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 0401d516ed..71a974da7d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.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.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -83,5 +84,54 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); } + + [Test] + public void TestFiltering() + { + TestModColumn column = null; + + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = column = new TestModColumn(ModType.Fun, true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + + AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); + + clickToggle(); + AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); + AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value)); + + AddStep("unset filter", () => column.Filter = null); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddAssert("checkbox not selected", () => !column.ChildrenOfType().Single().Current.Value); + + AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); + AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value); + + void clickToggle() => AddStep("click toggle", () => + { + var checkbox = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(checkbox); + InputManager.Click(MouseButton.Left); + }); + } + + private class TestModColumn : ModColumn + { + public new bool SelectionAnimationRunning => base.SelectionAnimationRunning; + + public TestModColumn(ModType modType, bool allowBulkSelection) + : base(modType, allowBulkSelection) + { + } + } } } diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 15b2874edb..eae57f3574 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -28,6 +28,18 @@ namespace osu.Game.Overlays.Mods { public class ModColumn : CompositeDrawable { + private Func? filter; + + public Func? Filter + { + get => filter; + set + { + filter = value; + updateFilter(); + } + } + private readonly ModType modType; private readonly Bindable>> availableMods = new Bindable>>(); @@ -220,9 +232,12 @@ namespace osu.Game.Overlays.Mods LoadComponentsAsync(panels, loaded => { panelFlow.ChildrenEnumerable = loaded; + foreach (var panel in panelFlow) panel.Active.BindValueChanged(_ => updateToggleState()); updateToggleState(); + + updateFilter(); }, (cancellationTokenSource = new CancellationTokenSource()).Token); } @@ -235,6 +250,8 @@ namespace osu.Game.Overlays.Mods private readonly Queue pendingSelectionOperations = new Queue(); + protected bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0; + protected override void Update() { base.Update(); @@ -260,8 +277,8 @@ namespace osu.Game.Overlays.Mods private void updateToggleState() { - if (toggleAllCheckbox != null && pendingSelectionOperations.Count == 0) - toggleAllCheckbox.Current.Value = panelFlow.All(panel => panel.Active.Value); + if (toggleAllCheckbox != null && !SelectionAnimationRunning) + toggleAllCheckbox.Current.Value = panelFlow.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); } /// @@ -271,7 +288,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in panelFlow.Where(b => !b.Active.Value)) + foreach (var button in panelFlow.Where(b => !b.Active.Value && !b.Filtered.Value)) pendingSelectionOperations.Enqueue(() => button.Active.Value = true); } @@ -282,7 +299,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in panelFlow.Where(b => b.Active.Value)) + foreach (var button in panelFlow.Where(b => b.Active.Value && !b.Filtered.Value)) pendingSelectionOperations.Enqueue(() => button.Active.Value = false); } @@ -349,5 +366,17 @@ namespace osu.Game.Overlays.Mods } #endregion + + #region Filtering support + + private void updateFilter() + { + foreach (var modPanel in panelFlow) + modPanel.ApplyFilter(Filter); + + updateToggleState(); + } + + #endregion } } diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 446ebe12c5..7e4d19850d 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.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.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -28,6 +29,7 @@ namespace osu.Game.Overlays.Mods { public Mod Mod { get; } public BindableBool Active { get; } = new BindableBool(); + public BindableBool Filtered { get; } = new BindableBool(); protected readonly Box Background; protected readonly Container SwitchContainer; @@ -157,6 +159,7 @@ namespace osu.Game.Overlays.Mods playStateChangeSamples(); UpdateState(); }); + Filtered.BindValueChanged(_ => updateFilterState()); UpdateState(); FinishTransforms(true); @@ -235,5 +238,19 @@ namespace osu.Game.Overlays.Mods TextBackground.FadeColour(textBackgroundColour, transitionDuration, Easing.OutQuint); TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint); } + + #region Filtering support + + public void ApplyFilter(Func? filter) + { + Filtered.Value = filter != null && !filter.Invoke(Mod); + } + + private void updateFilterState() + { + this.FadeTo(Filtered.Value ? 0 : 1); + } + + #endregion } } From b690df05de195561d8d691d476982323087aee93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 14:14:19 +0100 Subject: [PATCH 255/306] Hide multiselection checkbox if everything is filtered --- osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs | 8 ++++++++ osu.Game/Overlays/Mods/ModColumn.cs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 71a974da7d..534e9e0144 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -116,6 +116,14 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value); + AddStep("filter out everything", () => column.Filter = _ => false); + AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => panel.Filtered.Value)); + AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent); + + AddStep("inset filter", () => column.Filter = null); + AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddUntilStep("checkbox hidden", () => column.ChildrenOfType().Single().IsPresent); + void clickToggle() => AddStep("click toggle", () => { var checkbox = this.ChildrenOfType().Single(); diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index eae57f3574..649f54a96a 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -278,7 +278,10 @@ namespace osu.Game.Overlays.Mods private void updateToggleState() { if (toggleAllCheckbox != null && !SelectionAnimationRunning) + { + toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0; toggleAllCheckbox.Current.Value = panelFlow.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); + } } /// From 16c6b9b3b315096d0a8be6638aaac445a475f124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 14:10:35 +0100 Subject: [PATCH 256/306] Add keyboard selection support to mod column --- .../UserInterface/TestSceneModColumn.cs | 37 +++++++++++++++++++ osu.Game/Overlays/Mods/ModColumn.cs | 25 ++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 534e9e0144..01ef4b4b60 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -132,6 +132,43 @@ namespace osu.Game.Tests.Visual.UserInterface }); } + [Test] + public void TestKeyboardSelection() + { + ModColumn column = null; + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = column = new ModColumn(ModType.DifficultyReduction, true, new Key[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + + AddStep("press W", () => InputManager.Key(Key.W)); + AddAssert("NF panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("press W again", () => InputManager.Key(Key.W)); + AddAssert("NF panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("set filter to NF", () => column.Filter = mod => mod.Acronym == "NF"); + + AddStep("press W", () => InputManager.Key(Key.W)); + AddAssert("NF panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("press W again", () => InputManager.Key(Key.W)); + AddAssert("NF panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("filter out everything", () => column.Filter = _ => false); + + AddStep("press W", () => InputManager.Key(Key.W)); + AddAssert("NF panel not selected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("clear filter", () => column.Filter = null); + } + private class TestModColumn : ModColumn { public new bool SelectionAnimationRunning => base.SelectionAnimationRunning; diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 649f54a96a..dd3b7274fa 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -21,6 +22,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Utils; using osuTK; using osuTK.Graphics; +using osuTK.Input; #nullable enable @@ -41,6 +43,7 @@ namespace osu.Game.Overlays.Mods } private readonly ModType modType; + private readonly Key[]? toggleKeys; private readonly Bindable>> availableMods = new Bindable>>(); @@ -55,9 +58,10 @@ namespace osu.Game.Overlays.Mods private const float header_height = 60; - public ModColumn(ModType modType, bool allowBulkSelection) + public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null) { this.modType = modType; + this.toggleKeys = toggleKeys; Width = 450; RelativeSizeAxes = Axes.Y; @@ -381,5 +385,24 @@ namespace osu.Game.Overlays.Mods } #endregion + + #region Keyboard selection support + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.ControlPressed || e.AltPressed) return false; + if (toggleKeys == null) return false; + + int index = Array.IndexOf(toggleKeys, e.Key); + if (index < 0) return false; + + var panel = panelFlow.ElementAtOrDefault(index); + if (panel == null || panel.Filtered.Value) return false; + + panel.Active.Toggle(); + return true; + } + + #endregion } } From 774952addaf2c2f65a59b470a8c3475cd0024f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Feb 2022 23:08:31 +0100 Subject: [PATCH 257/306] Rescale components from figma to real dimensions --- osu.Game/Overlays/Mods/ModColumn.cs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index dd3b7274fa..7f3cc8249f 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -56,14 +56,14 @@ namespace osu.Game.Overlays.Mods private Colour4 accentColour; - private const float header_height = 60; + private const float header_height = 42; public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null) { this.modType = modType; this.toggleKeys = toggleKeys; - Width = 450; + Width = 320; RelativeSizeAxes = Axes.Y; Shear = new Vector2(ModPanel.SHEAR_X, 0); CornerRadius = ModPanel.CORNER_RADIUS; @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Mods }, headerText = new OsuTextFlowContainer(t => { - t.Font = OsuFont.TorusAlternate.With(size: 24); + t.Font = OsuFont.TorusAlternate.With(size: 17); t.Shadow = false; t.Colour = Colour4.Black; }) @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Mods Shear = new Vector2(-ModPanel.SHEAR_X, 0), Padding = new MarginPadding { - Horizontal = 15, + Horizontal = 17, Bottom = ModPanel.CORNER_RADIUS } } @@ -112,7 +112,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = ModPanel.CORNER_RADIUS, - BorderThickness = 4, + BorderThickness = 3, Children = new Drawable[] { contentBackground = new Box @@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Mods controlContainer = new Container { RelativeSizeAxes = Axes.X, - Padding = new MarginPadding { Horizontal = 20 } + Padding = new MarginPadding { Horizontal = 14 } } }, new Drawable[] @@ -147,8 +147,8 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0, 10), - Padding = new MarginPadding(10) + Spacing = new Vector2(0, 7), + Padding = new MarginPadding(7) } } } @@ -163,11 +163,12 @@ namespace osu.Game.Overlays.Mods if (allowBulkSelection) { - controlContainer.Height = 50; + controlContainer.Height = 35; controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Scale = new Vector2(0.8f), RelativeSizeAxes = Axes.X, LabelText = "Enable All", Shear = new Vector2(-ModPanel.SHEAR_X, 0) @@ -175,8 +176,8 @@ namespace osu.Game.Overlays.Mods panelFlow.Padding = new MarginPadding { Top = 0, - Bottom = 10, - Horizontal = 10 + Bottom = 7, + Horizontal = 7 }; } } From a1786f62d71999fc9b8991d3c43161932a0eeeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Feb 2022 23:10:22 +0100 Subject: [PATCH 258/306] Fix test failure due to attempting to set non-present divisor With the latest changes permitting fully custom beat snapping, the 1/3 snap divisor isn't immediately available in editor, requiring a switch to "triplets" mode first. --- .../Editor/TestSceneSliderStreamConversion.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs index 559d612037..70a9c03e65 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs @@ -7,6 +7,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; using osuTK; using osuTK.Input; @@ -72,7 +73,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorClock.Seek(slider.StartTime); EditorBeatmap.SelectedHitObjects.Add(slider); }); - AddStep("change beat divisor", () => beatDivisor.Value = 3); + AddStep("change beat divisor", () => + { + beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS; + beatDivisor.Value = 3; + }); convertToStream(); From fe63a09a0f2bb4a640ad92fad039c1f6a1fc323e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 28 Feb 2022 11:59:10 +0900 Subject: [PATCH 259/306] Fix missing dependency in test --- osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs index 868946d80f..b6004c651b 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs @@ -2,16 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps.Legacy; using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Tests.Components { [TestFixture] public class TestSceneSongBar : OsuTestScene { + [Cached] + private readonly LadderInfo ladder = new LadderInfo(); + [Test] public void TestSongBar() { From 4a555d067da4804d307b74c7bfd90fb7fbb6635c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 14:32:50 +0900 Subject: [PATCH 260/306] Change `ModPanel` to not handle `OnMouseDown` to allow drag scrolling in `ModColumn` --- osu.Game/Overlays/Mods/ModPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 7e4d19850d..312171cf74 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -193,7 +193,7 @@ namespace osu.Game.Overlays.Mods mouseDown = true; UpdateState(); - return true; + return false; } protected override void OnMouseUp(MouseUpEvent e) From 3634e12e66ff499831a441d5ff27f7ceab1e8627 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 15:21:01 +0900 Subject: [PATCH 261/306] Automatically focus divisor textbox and hide popover after successful change --- osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs | 4 ---- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index 94c0822235..6a0950c6dd 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -190,10 +190,6 @@ namespace osu.Game.Tests.Visual.Editing textBox.Text = divisor.ToString(); InputManager.Key(Key.Enter); }); - AddStep("dismiss popover", () => - { - InputManager.Key(Key.Escape); - }); AddUntilStep("wait for dismiss", () => !this.ChildrenOfType().Any()); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index d5b0ab19aa..c65423ef9f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -299,6 +300,8 @@ namespace osu.Game.Screens.Edit.Compose.Components base.LoadComplete(); BeatDivisor.BindValueChanged(_ => updateState(), true); divisorTextBox.OnCommit += (_, __) => setPresets(); + + Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox)); } private void setPresets() @@ -320,6 +323,8 @@ namespace osu.Game.Screens.Edit.Compose.Components } BeatDivisor.Value = divisor; + + this.HidePopover(); } private void updateState() From 368eadd8d1f02ac7263b093835cb4d98d69ec27b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 15:24:02 +0900 Subject: [PATCH 262/306] Remove unused using statement --- osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index c65423ef9f..bea72b7447 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; From 2be40f36f77c81501d5530ff136908111ce3ddb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 15:26:09 +0900 Subject: [PATCH 263/306] Reword popup text to read better (or more vaguely) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed some words but also don't mention "smaller" because it's... musically incorrect and also functionally incorrect – entering 1/[8] will result in 1/16 also being populated for instance. --- osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index bea72b7447..370c9016c7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -288,7 +288,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = "All other applicable smaller divisors will be automatically added to the list of presets." + Text = "Related divisors will be added to the list of presets." } } }; From 2e96f74c94a008e62a523e9ca15cf0b21ba05a71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 16:37:14 +0900 Subject: [PATCH 264/306] Allow `LegacyScoreEncoder` to be used without a beatmap if frames are already legacy frames --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 9460ec680c..14a6495282 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -5,9 +5,11 @@ using System; using System.IO; using System.Linq; using System.Text; +using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; +using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -29,12 +31,21 @@ namespace osu.Game.Scoring.Legacy private readonly Score score; private readonly IBeatmap beatmap; - public LegacyScoreEncoder(Score score, IBeatmap beatmap) + /// + /// Create a new score encoder for a specific score. + /// + /// The score to be encoded. + /// The beatmap used to convert frames for the score. May be null if the frames are already s. + /// + public LegacyScoreEncoder(Score score, [CanBeNull] IBeatmap beatmap) { this.score = score; this.beatmap = beatmap; - if (score.ScoreInfo.BeatmapInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.BeatmapInfo.Ruleset.OnlineID > 3) + if (beatmap == null && !score.Replay.Frames.All(f => f is LegacyReplayFrame)) + throw new ArgumentException("Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap)); + + if (score.ScoreInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.Ruleset.OnlineID > 3) throw new ArgumentException("Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } From 723e96309ae8b75b3a89a940ddebeef8e0ea45cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 16:40:00 +0900 Subject: [PATCH 265/306] Only convert non-legacy frames (and throw on conversion failure) --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 14a6495282..3355efc830 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -112,11 +113,16 @@ namespace osu.Game.Scoring.Legacy { int lastTime = 0; - foreach (var f in score.Replay.Frames.OfType().Select(f => f.ToLegacy(beatmap))) + foreach (var f in score.Replay.Frames) { + var legacyFrame = getLegacyFrame(f); + + if (legacyFrame == null) + throw new ArgumentException("One or more frames could not be converted to legacy frames"); + // Rounding because stable could only parse integral values - int time = (int)Math.Round(f.Time); - replayData.Append(FormattableString.Invariant($"{time - lastTime}|{f.MouseX ?? 0}|{f.MouseY ?? 0}|{(int)f.ButtonState},")); + int time = (int)Math.Round(legacyFrame.Time); + replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); lastTime = time; } } @@ -128,6 +134,14 @@ namespace osu.Game.Scoring.Legacy } } + private LegacyReplayFrame getLegacyFrame(ReplayFrame replayFrame) + { + if (replayFrame is LegacyReplayFrame legacyFrame) + return legacyFrame; + + return (replayFrame as IConvertibleReplayFrame)?.ToLegacy(beatmap); + } + private string getHpGraphFormatted() { // todo: implement, maybe? From 52e50db6b94ca11aa8df4ca2b14dbbb39c68ac47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 16:41:39 +0900 Subject: [PATCH 266/306] Enable `nullable` for `LegacyScoreEncoder` --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 3355efc830..1e39b66ead 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -5,7 +5,6 @@ using System; using System.IO; using System.Linq; using System.Text; -using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; @@ -14,6 +13,8 @@ using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; +#nullable enable + namespace osu.Game.Scoring.Legacy { public class LegacyScoreEncoder @@ -30,7 +31,7 @@ namespace osu.Game.Scoring.Legacy public const int FIRST_LAZER_VERSION = 30000000; private readonly Score score; - private readonly IBeatmap beatmap; + private readonly IBeatmap? beatmap; /// /// Create a new score encoder for a specific score. @@ -38,16 +39,16 @@ namespace osu.Game.Scoring.Legacy /// The score to be encoded. /// The beatmap used to convert frames for the score. May be null if the frames are already s. /// - public LegacyScoreEncoder(Score score, [CanBeNull] IBeatmap beatmap) + public LegacyScoreEncoder(Score score, IBeatmap? beatmap) { this.score = score; this.beatmap = beatmap; if (beatmap == null && !score.Replay.Frames.All(f => f is LegacyReplayFrame)) - throw new ArgumentException("Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap)); + throw new ArgumentException(@"Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap)); if (score.ScoreInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.Ruleset.OnlineID > 3) - throw new ArgumentException("Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); + throw new ArgumentException(@"Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } public void Encode(Stream stream) @@ -117,9 +118,6 @@ namespace osu.Game.Scoring.Legacy { var legacyFrame = getLegacyFrame(f); - if (legacyFrame == null) - throw new ArgumentException("One or more frames could not be converted to legacy frames"); - // Rounding because stable could only parse integral values int time = (int)Math.Round(legacyFrame.Time); replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); @@ -136,10 +134,17 @@ namespace osu.Game.Scoring.Legacy private LegacyReplayFrame getLegacyFrame(ReplayFrame replayFrame) { - if (replayFrame is LegacyReplayFrame legacyFrame) - return legacyFrame; + switch (replayFrame) + { + case LegacyReplayFrame legacyFrame: + return legacyFrame; - return (replayFrame as IConvertibleReplayFrame)?.ToLegacy(beatmap); + case IConvertibleReplayFrame convertibleFrame: + return convertibleFrame.ToLegacy(beatmap); + + default: + throw new ArgumentException(@"Frame could not be converted to legacy frames", nameof(replayFrame)); + } } private string getHpGraphFormatted() From a41e1c80f18b2b37f46963096bbc9c3082336bd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 19:11:06 +0900 Subject: [PATCH 267/306] Show hit error on results screen Leading up to implementation of "local offset", this feels like a good thing to have visible first and foremost. --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 1 + osu.Game.Rulesets.Osu/OsuRuleset.cs | 1 + osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 1 + .../Rulesets/Scoring/HitEventExtensions.cs | 3 +++ .../Ranking/Statistics/AverageHitError.cs | 27 +++++++++++++++++++ 5 files changed, 33 insertions(+) create mode 100644 osu.Game/Screens/Ranking/Statistics/AverageHitError.cs diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 180b9ef71b..859b6cfe76 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -394,6 +394,7 @@ namespace osu.Game.Rulesets.Mania { new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { + new AverageHitError(score.HitEvents), new UnstableRate(score.HitEvents) }), true) } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index ad00a025a1..5ade164566 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -314,6 +314,7 @@ namespace osu.Game.Rulesets.Osu { new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { + new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) }), true) } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index e56aabaf9d..de0ef8d95b 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -237,6 +237,7 @@ namespace osu.Game.Rulesets.Taiko { new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { + new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) }), true) } diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index f645b12483..5c0b4ecac3 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -22,6 +22,9 @@ namespace osu.Game.Rulesets.Scoring return 10 * standardDeviation(timeOffsets); } + public static double? CalculateAverageHitError(this IEnumerable hitEvents) => + hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).Average(); + private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit(); private static double? standardDeviation(double[] timeOffsets) diff --git a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs new file mode 100644 index 0000000000..d0e70251e7 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/AverageHitError.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 System; +using System.Collections.Generic; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Displays the unstable rate statistic for a given play. + /// + public class AverageHitError : SimpleStatisticItem + { + /// + /// Creates and computes an statistic. + /// + /// Sequence of s to calculate the unstable rate based on. + public AverageHitError(IEnumerable hitEvents) + : base("Average Hit Error") + { + Value = hitEvents.CalculateAverageHitError(); + } + + protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}"; + } +} From 159db38f8a5ff2ffac1b75c9aa67e6c782356d0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Feb 2022 19:14:43 +0900 Subject: [PATCH 268/306] Add missing xmldoc --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 5c0b4ecac3..637d0a872a 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -22,6 +22,13 @@ namespace osu.Game.Rulesets.Scoring return 10 * standardDeviation(timeOffsets); } + /// + /// Calculates the average hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. + /// + /// + /// A non-null value if unstable rate could be calculated, + /// and if unstable rate cannot be calculated due to being empty. + /// public static double? CalculateAverageHitError(this IEnumerable hitEvents) => hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).Average(); From f95b7b992b618d621157b92b6a31e59b798a1cba Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 28 Feb 2022 17:16:02 +0300 Subject: [PATCH 269/306] Disable running `mono-cil-strip` during iOS builds --- osu.iOS.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.iOS.props b/osu.iOS.props index 9d0e1790f0..80600655aa 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -15,7 +15,8 @@ --nosymbolstrip=BASS_FX_BPM_BeatCallbackReset --nosymbolstrip=BASS_FX_BPM_BeatCallbackSet --nosymbolstrip=BASS_FX_BPM_BeatDecodeGet --nosymbolstrip=BASS_FX_BPM_BeatFree --nosymbolstrip=BASS_FX_BPM_BeatGetParameters --nosymbolstrip=BASS_FX_BPM_BeatSetParameters --nosymbolstrip=BASS_FX_BPM_CallbackReset --nosymbolstrip=BASS_FX_BPM_CallbackSet --nosymbolstrip=BASS_FX_BPM_DecodeGet --nosymbolstrip=BASS_FX_BPM_Free --nosymbolstrip=BASS_FX_BPM_Translate --nosymbolstrip=BASS_FX_GetVersion --nosymbolstrip=BASS_FX_ReverseCreate --nosymbolstrip=BASS_FX_ReverseGetSource --nosymbolstrip=BASS_FX_TempoCreate --nosymbolstrip=BASS_FX_TempoGetRateRatio --nosymbolstrip=BASS_FX_TempoGetSource --nosymbolstrip=BASS_Mixer_ChannelFlags --nosymbolstrip=BASS_Mixer_ChannelGetData --nosymbolstrip=BASS_Mixer_ChannelGetEnvelopePos --nosymbolstrip=BASS_Mixer_ChannelGetLevel --nosymbolstrip=BASS_Mixer_ChannelGetLevelEx --nosymbolstrip=BASS_Mixer_ChannelGetMatrix --nosymbolstrip=BASS_Mixer_ChannelGetMixer --nosymbolstrip=BASS_Mixer_ChannelGetPosition --nosymbolstrip=BASS_Mixer_ChannelGetPositionEx --nosymbolstrip=BASS_Mixer_ChannelIsActive --nosymbolstrip=BASS_Mixer_ChannelRemove --nosymbolstrip=BASS_Mixer_ChannelRemoveSync --nosymbolstrip=BASS_Mixer_ChannelSetEnvelope --nosymbolstrip=BASS_Mixer_ChannelSetEnvelopePos --nosymbolstrip=BASS_Mixer_ChannelSetMatrix --nosymbolstrip=BASS_Mixer_ChannelSetMatrixEx --nosymbolstrip=BASS_Mixer_ChannelSetPosition --nosymbolstrip=BASS_Mixer_ChannelSetSync --nosymbolstrip=BASS_Mixer_GetVersion --nosymbolstrip=BASS_Mixer_StreamAddChannel --nosymbolstrip=BASS_Mixer_StreamAddChannelEx --nosymbolstrip=BASS_Mixer_StreamCreate --nosymbolstrip=BASS_Mixer_StreamGetChannels --nosymbolstrip=BASS_Split_StreamCreate --nosymbolstrip=BASS_Split_StreamGetAvailable --nosymbolstrip=BASS_Split_StreamGetSource --nosymbolstrip=BASS_Split_StreamGetSplits --nosymbolstrip=BASS_Split_StreamReset --nosymbolstrip=BASS_Split_StreamResetEx - --nolinkaway $(GeneratedMtouchSymbolStripFlags) + + --nolinkaway --nostrip $(GeneratedMtouchSymbolStripFlags) true From 42b27e305081884b3442b01e1b6de3e7ecbaee74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Feb 2022 20:44:13 +0100 Subject: [PATCH 270/306] Clean up test step names --- osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 01ef4b4b60..01afde8f7e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("checkbox selected", () => this.ChildrenOfType().Single().Current.Value); AddStep("deselect first panel", () => this.ChildrenOfType().First().Active.Value = false); - AddUntilStep("checkbox selected", () => !this.ChildrenOfType().Single().Current.Value); + AddUntilStep("checkbox not selected", () => !this.ChildrenOfType().Single().Current.Value); void clickToggle() => AddStep("click toggle", () => { @@ -121,8 +121,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent); AddStep("inset filter", () => column.Filter = null); - AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); - AddUntilStep("checkbox hidden", () => column.ChildrenOfType().Single().IsPresent); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddUntilStep("checkbox visible", () => column.ChildrenOfType().Single().IsPresent); void clickToggle() => AddStep("click toggle", () => { From 6cc972aa6afc457ca0815ed11194d9d5ce07a79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Feb 2022 21:36:13 +0100 Subject: [PATCH 271/306] Fix test failures by waiting for panel load --- .../Visual/UserInterface/TestSceneModColumn.cs | 9 +++++++-- osu.Game/Overlays/Mods/ModColumn.cs | 13 ++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 01afde8f7e..e47ae860c6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -54,17 +54,20 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestMultiSelection() { + ModColumn column = null; AddStep("create content", () => Child = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(30), - Child = new ModColumn(ModType.DifficultyIncrease, true) + Child = column = new ModColumn(ModType.DifficultyIncrease, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre } }); + AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded); + clickToggle(); AddUntilStep("all panels selected", () => this.ChildrenOfType().All(panel => panel.Active.Value)); @@ -140,13 +143,15 @@ namespace osu.Game.Tests.Visual.UserInterface { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(30), - Child = column = new ModColumn(ModType.DifficultyReduction, true, new Key[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }) + Child = column = new ModColumn(ModType.DifficultyReduction, true, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }) { Anchor = Anchor.Centre, Origin = Anchor.Centre } }); + AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded); + AddStep("press W", () => InputManager.Key(Key.W)); AddAssert("NF panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 7f3cc8249f..b615b7bd32 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -56,6 +57,9 @@ namespace osu.Game.Overlays.Mods private Colour4 accentColour; + private Task? latestLoadTask; + internal bool ItemsLoaded => latestLoadTask == null; + private const float header_height = 42; public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null) @@ -234,7 +238,9 @@ namespace osu.Game.Overlays.Mods Shear = new Vector2(-ModPanel.SHEAR_X, 0) }); - LoadComponentsAsync(panels, loaded => + Task? loadTask; + + latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded => { panelFlow.ChildrenEnumerable = loaded; @@ -244,6 +250,11 @@ namespace osu.Game.Overlays.Mods updateFilter(); }, (cancellationTokenSource = new CancellationTokenSource()).Token); + loadTask.ContinueWith(_ => + { + if (loadTask == latestLoadTask) + latestLoadTask = null; + }); } #region Bulk select / deselect From e8701f46f115348b502686916cc9f27fbbd5df01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Feb 2022 21:39:21 +0100 Subject: [PATCH 272/306] Add xmldoc to `Filter` to explain usage --- osu.Game/Overlays/Mods/ModColumn.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index b615b7bd32..3654f5246d 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -33,6 +33,11 @@ namespace osu.Game.Overlays.Mods { private Func? filter; + /// + /// Function determining whether each mod in the column should be displayed. + /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. + /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden. + /// public Func? Filter { get => filter; From 899b95e61bfb7d0a21a58cd2a9ede511fe554408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Feb 2022 21:46:58 +0100 Subject: [PATCH 273/306] Do not delay inital mod update by a frame --- osu.Game/Overlays/Mods/ModColumn.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 3654f5246d..736a0205e2 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -224,7 +224,8 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { base.LoadComplete(); - availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods), true); + availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); + updateMods(); } private CancellationTokenSource? cancellationTokenSource; From eb75a29b2024b1001001fe4ef7b523624e33ef3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 12:07:03 +0900 Subject: [PATCH 274/306] Use constant for maximum legacy ruleset id --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 1e39b66ead..1326395695 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -47,7 +48,7 @@ namespace osu.Game.Scoring.Legacy if (beatmap == null && !score.Replay.Frames.All(f => f is LegacyReplayFrame)) throw new ArgumentException(@"Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap)); - if (score.ScoreInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.Ruleset.OnlineID > 3) + if (score.ScoreInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.Ruleset.OnlineID > ILegacyRuleset.MAX_LEGACY_RULESET_ID) throw new ArgumentException(@"Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } From 7fa58427832e5dfaa6f9743650be7df15645a414 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 18:30:55 +0900 Subject: [PATCH 275/306] Add global statistics output for all realm reads/writes --- osu.Game/Database/RealmAccess.cs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 9bdbebfe89..f63e858b6f 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -14,14 +14,14 @@ using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; -using osu.Game.Configuration; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Models; -using osu.Game.Skinning; -using osu.Game.Stores; using osu.Game.Rulesets; using osu.Game.Scoring; +using osu.Game.Skinning; +using osu.Game.Stores; using Realms; using Realms.Exceptions; @@ -85,6 +85,14 @@ namespace osu.Game.Database private static readonly GlobalStatistic total_subscriptions = GlobalStatistics.Get(@"Realm", @"Subscriptions"); + private static readonly GlobalStatistic total_reads_update = GlobalStatistics.Get(@"Realm", @"Reads (Update)"); + + private static readonly GlobalStatistic total_reads_async = GlobalStatistics.Get(@"Realm", @"Reads (Async)"); + + private static readonly GlobalStatistic total_writes_update = GlobalStatistics.Get(@"Realm", @"Writes (Update)"); + + private static readonly GlobalStatistic total_writes_async = GlobalStatistics.Get(@"Realm", @"Writes (Async)"); + private readonly object realmLock = new object(); private Realm? updateRealm; @@ -213,8 +221,12 @@ namespace osu.Game.Database public T Run(Func action) { if (ThreadSafety.IsUpdateThread) + { + total_reads_update.Value++; return action(Realm); + } + total_reads_async.Value++; using (var realm = getRealmInstance()) return action(realm); } @@ -226,9 +238,13 @@ namespace osu.Game.Database public void Run(Action action) { if (ThreadSafety.IsUpdateThread) + { + total_reads_update.Value++; action(Realm); + } else { + total_reads_async.Value++; using (var realm = getRealmInstance()) action(realm); } @@ -241,9 +257,14 @@ namespace osu.Game.Database public void Write(Action action) { if (ThreadSafety.IsUpdateThread) + { + total_writes_update.Value++; Realm.Write(action); + } else { + total_writes_async.Value++; + using (var realm = getRealmInstance()) realm.Write(action); } From 9a117467b5ea36389da063102c051ce7eef86ed2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 18:31:33 +0900 Subject: [PATCH 276/306] Add `RealmAccess.WriteAsync` method --- .../RealmSubscriptionRegistrationTests.cs | 29 +++++++++++++++++++ osu.Game/Database/RealmAccess.cs | 12 ++++++++ 2 files changed, 41 insertions(+) diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index d62ce3b585..02d617d0e0 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Tests.Resources; @@ -18,6 +20,33 @@ namespace osu.Game.Tests.Database [TestFixture] public class RealmSubscriptionRegistrationTests : RealmTest { + [Test] + public void TestSubscriptionWithAsyncWrite() + { + ChangeSet? lastChanges = null; + + RunTestWithRealm((realm, _) => + { + var registration = realm.RegisterForNotifications(r => r.All(), onChanged); + + realm.Run(r => r.Refresh()); + + // Without forcing the write onto its own thread, realm will internally run the operation synchronously, which can cause a deadlock with `WaitSafely`. + Task.Run(async () => + { + await realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + }).WaitSafely(); + + realm.Run(r => r.Refresh()); + + Assert.That(lastChanges?.InsertedIndices, Has.One.Items); + + registration.Dispose(); + }); + + void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => lastChanges = changes; + } + [Test] public void TestSubscriptionWithContextLoss() { diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f63e858b6f..bf2b48ea52 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Input.Bindings; @@ -270,6 +271,17 @@ namespace osu.Game.Database } } + /// + /// Write changes to realm asynchronously, guaranteeing order of execution. + /// + /// The work to run. + public async Task WriteAsync(Action action) + { + total_writes_async.Value++; + using (var realm = getRealmInstance()) + await realm.WriteAsync(action); + } + /// /// Subscribe to a realm collection and begin watching for asynchronous changes. /// From 4117a6adf757afa89277246396d2c5e6f59b7cfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 14:13:27 +0900 Subject: [PATCH 277/306] Move player loader audio settings to new group --- osu.Game/Screens/Play/PlayerLoader.cs | 1 + .../Play/PlayerSettings/AudioSettings.cs | 29 +++++++++++++++++++ .../Play/PlayerSettings/VisualSettings.cs | 3 -- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 6009c85583..20c41958c9 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -167,6 +167,7 @@ namespace osu.Game.Screens.Play Children = new PlayerSettingsGroup[] { VisualSettings = new VisualSettings(), + new AudioSettings(), new InputSettings() } }, diff --git a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs new file mode 100644 index 0000000000..93457980f3 --- /dev/null +++ b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs @@ -0,0 +1,29 @@ +// 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.Screens.Play.PlayerSettings +{ + public class AudioSettings : PlayerSettingsGroup + { + private readonly PlayerCheckbox beatmapHitsoundsToggle; + + public AudioSettings() + : base("Audio Settings") + { + Children = new Drawable[] + { + beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); + } + } +} diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index a97078c461..81950efa9e 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -15,7 +15,6 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerCheckbox showStoryboardToggle; private readonly PlayerCheckbox beatmapSkinsToggle; private readonly PlayerCheckbox beatmapColorsToggle; - private readonly PlayerCheckbox beatmapHitsoundsToggle; public VisualSettings() : base("Visual Settings") @@ -45,7 +44,6 @@ namespace osu.Game.Screens.Play.PlayerSettings showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboard / Video" }, beatmapSkinsToggle = new PlayerCheckbox { LabelText = "Beatmap skins" }, beatmapColorsToggle = new PlayerCheckbox { LabelText = "Beatmap colours" }, - beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } }; } @@ -57,7 +55,6 @@ namespace osu.Game.Screens.Play.PlayerSettings showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); beatmapColorsToggle.Current = config.GetBindable(OsuSetting.BeatmapColours); - beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); } } } From 5e47e35f0d2b9d85b3f01eeaa54e40db1b3fa998 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 15:40:37 +0900 Subject: [PATCH 278/306] Add ability to change distribution of test `HitEvent`s --- .../Ranking/TestSceneHitEventTimingDistributionGraph.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 4bc843096f..221001e40b 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -71,16 +71,16 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - public static List CreateDistributedHitEvents() + public static List CreateDistributedHitEvents(double centre = 0, double range = 25) { var hitEvents = new List(); - for (int i = 0; i < 50; i++) + for (int i = 0; i < range * 2; i++) { - int count = (int)(Math.Pow(25 - Math.Abs(i - 25), 2)); + int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)); for (int j = 0; j < count; j++) - hitEvents.Add(new HitEvent(i - 25, HitResult.Perfect, new HitCircle(), new HitCircle(), null)); + hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, new HitCircle(), new HitCircle(), null)); } return hitEvents; From 1847f69bf95a95fe7dce07ebb2d4119e946aa470 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 15:40:19 +0900 Subject: [PATCH 279/306] Add basic beatmap offset adjustment control --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 27 ++++++ .../PlayerSettings/BeatmapOffsetControl.cs | 88 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs create mode 100644 osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs new file mode 100644 index 0000000000..bd87039797 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Tests.Visual.Ranking; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneBeatmapOffsetControl : OsuTestScene + { + [Test] + public void TestDisplay() + { + Child = new PlayerSettingsGroup("Some settings") + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new BeatmapOffsetControl(TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(-4.5)) + } + }; + } + } +} diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs new file mode 100644 index 0000000000..c05c5beb31 --- /dev/null +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -0,0 +1,88 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Ranking.Statistics; +using osuTK; + +namespace osu.Game.Screens.Play.PlayerSettings +{ + public class BeatmapOffsetControl : CompositeDrawable + { + private readonly SettingsButton useAverageButton; + + private readonly double lastPlayAverage; + + public Bindable Current { get; } = new BindableDouble + { + Default = 0, + Value = 0, + MinValue = -50, + MaxValue = 50, + Precision = 0.1, + }; + + public BeatmapOffsetControl(IReadOnlyList hitEvents) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + FillFlowContainer flow; + + InternalChild = flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + new PlayerSliderBar + { + KeyboardStep = 5, + LabelText = "Beatmap offset", + Current = Current, + }, + } + }; + + if (hitEvents.CalculateAverageHitError() is double average) + { + lastPlayAverage = average; + + flow.AddRange(new Drawable[] + { + new OsuSpriteText + { + Text = "Last play:" + }, + new HitEventTimingDistributionGraph(hitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 50, + }, + new AverageHitError(hitEvents), + useAverageButton = new SettingsButton + { + Text = "Calibrate using last play", + Action = () => Current.Value = lastPlayAverage + }, + }); + } + + Current.BindValueChanged(offset => + { + if (useAverageButton != null) + { + useAverageButton.Enabled.Value = offset.NewValue != lastPlayAverage; + } + }, true); + } + } +} From 350b0b488c048d114cb25adc4353517d75ac2081 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 14:47:06 +0900 Subject: [PATCH 280/306] TODO: Get score from previous play session for further analysis --- osu.Game/Screens/Play/Player.cs | 6 +++++- osu.Game/Screens/Play/PlayerLoader.cs | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index d4b02622d3..86ea412488 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -136,7 +136,11 @@ namespace osu.Game.Screens.Play public readonly PlayerConfiguration Configuration; - protected Score Score { get; private set; } + /// + /// The score for the current play session. + /// Available only after the player is loaded. + /// + public Score Score { get; private set; } /// /// Create a new player instance. diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 20c41958c9..863246cd05 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -23,6 +23,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Input; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Users; @@ -226,6 +227,14 @@ namespace osu.Game.Screens.Play { base.OnResuming(last); + var lastScore = player.Score; + + if (lastScore != null) + { + // TODO: use this + double? lastPlayHitError = lastScore.ScoreInfo.HitEvents.CalculateAverageHitError(); + } + // prepare for a retry. player = null; playerConsumed = false; From 7215f3f66b6c7891f7d9b8de14ff6ba7fe155ed8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 16:02:48 +0900 Subject: [PATCH 281/306] Fix `CalculateAverageHitError` throwing if there are zero `HitEvent`s --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 637d0a872a..fea13cf4b6 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -29,8 +29,15 @@ namespace osu.Game.Rulesets.Scoring /// A non-null value if unstable rate could be calculated, /// and if unstable rate cannot be calculated due to being empty. /// - public static double? CalculateAverageHitError(this IEnumerable hitEvents) => - hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).Average(); + public static double? CalculateAverageHitError(this IEnumerable hitEvents) + { + double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray(); + + if (timeOffsets.Length == 0) + return null; + + return timeOffsets.Average(); + } private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit(); From 2901d2a6505c6a1a44ee86101635cd35837cc231 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 16:14:57 +0900 Subject: [PATCH 282/306] Hook offset adjustment control up to last play via `PlayerLoader` --- osu.Game/Screens/Play/PlayerLoader.cs | 11 +-- .../Play/PlayerSettings/AudioSettings.cs | 10 ++- .../PlayerSettings/BeatmapOffsetControl.cs | 79 +++++++++++-------- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 863246cd05..f6d63a8ec5 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -23,7 +23,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Input; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Users; @@ -62,6 +61,8 @@ namespace osu.Game.Screens.Play protected VisualSettings VisualSettings { get; private set; } + protected AudioSettings AudioSettings { get; private set; } + protected Task LoadTask { get; private set; } protected Task DisposalTask { get; private set; } @@ -168,7 +169,7 @@ namespace osu.Game.Screens.Play Children = new PlayerSettingsGroup[] { VisualSettings = new VisualSettings(), - new AudioSettings(), + AudioSettings = new AudioSettings(), new InputSettings() } }, @@ -229,11 +230,7 @@ namespace osu.Game.Screens.Play var lastScore = player.Score; - if (lastScore != null) - { - // TODO: use this - double? lastPlayHitError = lastScore.ScoreInfo.HitEvents.CalculateAverageHitError(); - } + AudioSettings.ReferenceScore.Value = lastScore?.ScoreInfo; // prepare for a retry. player = null; diff --git a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs index 93457980f3..32de5333e1 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs @@ -2,13 +2,17 @@ // 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.Configuration; +using osu.Game.Scoring; namespace osu.Game.Screens.Play.PlayerSettings { public class AudioSettings : PlayerSettingsGroup { + public Bindable ReferenceScore { get; } = new Bindable(); + private readonly PlayerCheckbox beatmapHitsoundsToggle; public AudioSettings() @@ -16,7 +20,11 @@ namespace osu.Game.Screens.Play.PlayerSettings { Children = new Drawable[] { - beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } + beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" }, + new BeatmapOffsetControl + { + ReferenceScore = { BindTarget = ReferenceScore }, + }, }; } diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index c05c5beb31..5d287a3730 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -1,13 +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.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osuTK; @@ -15,9 +15,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { public class BeatmapOffsetControl : CompositeDrawable { - private readonly SettingsButton useAverageButton; - - private readonly double lastPlayAverage; + public Bindable ReferenceScore { get; } = new Bindable(); public Bindable Current { get; } = new BindableDouble { @@ -28,14 +26,18 @@ namespace osu.Game.Screens.Play.PlayerSettings Precision = 0.1, }; - public BeatmapOffsetControl(IReadOnlyList hitEvents) + private SettingsButton useAverageButton; + + private double lastPlayAverage; + + private readonly FillFlowContainer referenceScoreContainer; + + public BeatmapOffsetControl() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - FillFlowContainer flow; - - InternalChild = flow = new FillFlowContainer + InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -49,32 +51,17 @@ namespace osu.Game.Screens.Play.PlayerSettings LabelText = "Beatmap offset", Current = Current, }, + referenceScoreContainer = new FillFlowContainer + { + Spacing = new Vector2(10), + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, } }; - if (hitEvents.CalculateAverageHitError() is double average) - { - lastPlayAverage = average; - - flow.AddRange(new Drawable[] - { - new OsuSpriteText - { - Text = "Last play:" - }, - new HitEventTimingDistributionGraph(hitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 50, - }, - new AverageHitError(hitEvents), - useAverageButton = new SettingsButton - { - Text = "Calibrate using last play", - Action = () => Current.Value = lastPlayAverage - }, - }); - } + ReferenceScore.BindValueChanged(scoreChanged, true); Current.BindValueChanged(offset => { @@ -84,5 +71,35 @@ namespace osu.Game.Screens.Play.PlayerSettings } }, true); } + + private void scoreChanged(ValueChangedEvent score) + { + if (!(score.NewValue?.HitEvents.CalculateAverageHitError() is double average)) + { + referenceScoreContainer.Clear(); + return; + } + + lastPlayAverage = average; + + referenceScoreContainer.Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Last play:" + }, + new HitEventTimingDistributionGraph(score.NewValue.HitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 50, + }, + new AverageHitError(score.NewValue.HitEvents), + useAverageButton = new SettingsButton + { + Text = "Calibrate using last play", + Action = () => Current.Value = lastPlayAverage + }, + }; + } } } From fab09575ec30d2cec7aef5487f59b6b8fa10732a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 16:15:13 +0900 Subject: [PATCH 283/306] Add full testing flow for `BeatmapOffsetControl` --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index bd87039797..3fb10b2d5c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -1,8 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Overlays.Settings; +using osu.Game.Scoring; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Tests.Visual.Ranking; @@ -10,18 +14,46 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneBeatmapOffsetControl : OsuTestScene { + private BeatmapOffsetControl offsetControl; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create control", () => + { + Child = new PlayerSettingsGroup("Some settings") + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + offsetControl = new BeatmapOffsetControl() + } + }; + }); + } + [Test] public void TestDisplay() { - Child = new PlayerSettingsGroup("Some settings") + const double average_error = -4.5; + + AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + AddStep("Set reference score", () => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] + offsetControl.ReferenceScore.Value = new ScoreInfo { - new BeatmapOffsetControl(TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(-4.5)) - } - }; + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error) + }; + }); + + AddAssert("Has calibration button", () => offsetControl.ChildrenOfType().Any()); + AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); + AddAssert("Offset is adjusted", () => offsetControl.Current.Value == average_error); + + AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } } } From acf8db13acad60857ed919f00399f0f50c80f869 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 16:22:51 +0900 Subject: [PATCH 284/306] Store user settings to realm --- osu.Game/Beatmaps/BeatmapInfo.cs | 3 +++ osu.Game/Beatmaps/BeatmapUserSettings.cs | 19 +++++++++++++++++++ osu.Game/Database/RealmAccess.cs | 3 ++- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Beatmaps/BeatmapUserSettings.cs diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 305b3979a0..c6f69286cd 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -40,6 +40,8 @@ namespace osu.Game.Beatmaps [Backlink(nameof(ScoreInfo.BeatmapInfo))] public IQueryable Scores { get; } = null!; + public BeatmapUserSettings UserSettings { get; set; } = null!; + public BeatmapInfo(RulesetInfo? ruleset = null, BeatmapDifficulty? difficulty = null, BeatmapMetadata? metadata = null) { ID = Guid.NewGuid(); @@ -51,6 +53,7 @@ namespace osu.Game.Beatmaps }; Difficulty = difficulty ?? new BeatmapDifficulty(); Metadata = metadata ?? new BeatmapMetadata(); + UserSettings = new BeatmapUserSettings(); } [UsedImplicitly] diff --git a/osu.Game/Beatmaps/BeatmapUserSettings.cs b/osu.Game/Beatmaps/BeatmapUserSettings.cs new file mode 100644 index 0000000000..5c71bf34b1 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapUserSettings.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. + +#nullable enable +using Realms; + +namespace osu.Game.Beatmaps +{ + /// + /// User settings overrides that are attached to a beatmap. + /// + public class BeatmapUserSettings : EmbeddedObject + { + /// + /// An audio offset that can be used for timing adjustments. + /// + public double Offset { get; set; } + } +} diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index bf2b48ea52..fc2f519ead 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -54,8 +54,9 @@ namespace osu.Game.Database /// 11 2021-11-22 Use ShortName instead of RulesetID for ruleset key bindings. /// 12 2021-11-24 Add Status to RealmBeatmapSet. /// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields). + /// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo. /// - private const int schema_version = 13; + private const int schema_version = 14; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. From 047e801da9f9fe31b9a13d6c8c59b6240626fcc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 16:59:33 +0900 Subject: [PATCH 285/306] Store and retrieve offset from realm --- osu.Game/Database/RealmAccess.cs | 5 +++ .../Play/MasterGameplayClockContainer.cs | 17 +++++---- .../PlayerSettings/BeatmapOffsetControl.cs | 36 +++++++++++++++---- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index fc2f519ead..fb3052d850 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -565,6 +565,11 @@ namespace osu.Game.Database } break; + + case 14: + foreach (var beatmap in migration.NewRealm.All()) + beatmap.UserSettings = new BeatmapUserSettings(); + break; } } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 200921680e..d27a989067 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; namespace osu.Game.Screens.Play { @@ -43,7 +44,7 @@ namespace osu.Game.Screens.Play Precision = 0.1, }; - private double totalAppliedOffset => userOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; + private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); @@ -52,7 +53,8 @@ namespace osu.Game.Screens.Play private readonly bool startAtGameplayStart; private readonly double firstHitObjectTime; - private HardwareCorrectionOffsetClock userOffsetClock; + private HardwareCorrectionOffsetClock userGlobalOffsetClock; + private HardwareCorrectionOffsetClock userBeatmapOffsetClock; private HardwareCorrectionOffsetClock platformOffsetClock; private MasterGameplayClock masterGameplayClock; private Bindable userAudioOffset; @@ -69,10 +71,12 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, RealmAccess realm) { userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); - userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); + userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); + + userBeatmapOffsetClock.Offset = realm.Run(r => r.Find(beatmap.BeatmapInfo.ID).UserSettings?.Offset) ?? 0; // sane default provided by ruleset. startOffset = gameplayStartTime; @@ -161,9 +165,10 @@ namespace osu.Game.Screens.Play platformOffsetClock = new HardwareCorrectionOffsetClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // the final usable gameplay clock with user-set offsets applied. - userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust); + userGlobalOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust); + userBeatmapOffsetClock = new HardwareCorrectionOffsetClock(userGlobalOffsetClock, pauseFreqAdjust); - return masterGameplayClock = new MasterGameplayClock(userOffsetClock); + return masterGameplayClock = new MasterGameplayClock(userBeatmapOffsetClock); } /// diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 5d287a3730..75f8c89d34 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; @@ -60,16 +63,37 @@ namespace osu.Game.Screens.Play.PlayerSettings }, } }; + } + + [Resolved] + private RealmAccess realm { get; set; } + + [Resolved] + private IBindable beatmap { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); ReferenceScore.BindValueChanged(scoreChanged, true); - Current.BindValueChanged(offset => + Current.BindValueChanged(currentChanged); + Current.Value = realm.Run(r => r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings?.Offset) ?? 0; + } + + private void currentChanged(ValueChangedEvent offset) + { + if (useAverageButton != null) { - if (useAverageButton != null) - { - useAverageButton.Enabled.Value = offset.NewValue != lastPlayAverage; - } - }, true); + useAverageButton.Enabled.Value = offset.NewValue != lastPlayAverage; + } + + realm.Write(r => + { + var settings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + + settings.Offset = offset.NewValue; + }); } private void scoreChanged(ValueChangedEvent score) From 071ba5c1dfbf785d45a12713bf3a2d3d3121a710 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 18:28:53 +0900 Subject: [PATCH 286/306] Make writes asynchronously to avoid synchronous overhead --- .../PlayerSettings/BeatmapOffsetControl.cs | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 75f8c89d34..2f2d1b81e5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.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.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -78,9 +80,11 @@ namespace osu.Game.Screens.Play.PlayerSettings ReferenceScore.BindValueChanged(scoreChanged, true); Current.BindValueChanged(currentChanged); - Current.Value = realm.Run(r => r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings?.Offset) ?? 0; + Current.Value = realm.Run(r => r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings.Offset); } + private Task realmWrite; + private void currentChanged(ValueChangedEvent offset) { if (useAverageButton != null) @@ -88,12 +92,25 @@ namespace osu.Game.Screens.Play.PlayerSettings useAverageButton.Enabled.Value = offset.NewValue != lastPlayAverage; } - realm.Write(r => - { - var settings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + Scheduler.AddOnce(updateOffset); - settings.Offset = offset.NewValue; - }); + void updateOffset() + { + // ensure the previous write has completed. + if (realmWrite?.IsCompleted == false) + { + Scheduler.AddOnce(updateOffset); + return; + } + + realmWrite?.WaitSafely(); + realmWrite = realm.WriteAsync(r => + { + var settings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + + settings.Offset = offset.NewValue; + }); + } } private void scoreChanged(ValueChangedEvent score) From bb8caabb8be3e0391fee7adede8f0bed09948737 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 18:29:04 +0900 Subject: [PATCH 287/306] Subscribe to changes in offset --- .../Play/MasterGameplayClockContainer.cs | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index d27a989067..6a90e7adea 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using osu.Framework; using osu.Framework.Allocation; @@ -70,13 +71,38 @@ namespace osu.Game.Screens.Play firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; } - [BackgroundDependencyLoader] - private void load(OsuConfigManager config, RealmAccess realm) + [Resolved] + private RealmAccess realm { get; set; } + + [Resolved] + private OsuConfigManager config { get; set; } + + private IDisposable beatmapOffsetSubscription; + + protected override void LoadComplete() { + base.LoadComplete(); + userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); - userBeatmapOffsetClock.Offset = realm.Run(r => r.Find(beatmap.BeatmapInfo.ID).UserSettings?.Offset) ?? 0; + beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => + { + var userSettings = r.Find(beatmap.BeatmapInfo.ID).UserSettings; + + void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == nameof(BeatmapUserSettings.Offset)) + updateOffset(); + } + + updateOffset(); + userSettings.PropertyChanged += onUserSettingsOnPropertyChanged; + + return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged); + + void updateOffset() => userBeatmapOffsetClock.Offset = userSettings.Offset; + }); // sane default provided by ruleset. startOffset = gameplayStartTime; @@ -214,6 +240,7 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + beatmapOffsetSubscription?.Dispose(); removeSourceClockAdjustments(); } From 99c1ba19aa22f5f88f21f136a27cb3b2c63a392f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:13:40 +0900 Subject: [PATCH 288/306] Allow `BeatmapOffsetControl` to react to external changes to offset --- .../PlayerSettings/BeatmapOffsetControl.cs | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 2f2d1b81e5..487d044f5b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -1,12 +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 System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Sprites; @@ -22,7 +24,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { public Bindable ReferenceScore { get; } = new Bindable(); - public Bindable Current { get; } = new BindableDouble + public BindableDouble Current { get; } = new BindableDouble { Default = 0, Value = 0, @@ -73,42 +75,59 @@ namespace osu.Game.Screens.Play.PlayerSettings [Resolved] private IBindable beatmap { get; set; } + private IDisposable beatmapOffsetSubscription; + protected override void LoadComplete() { base.LoadComplete(); ReferenceScore.BindValueChanged(scoreChanged, true); + beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => + { + var userSettings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + + Current.Value = userSettings.Offset; + userSettings.PropertyChanged += onUserSettingsOnPropertyChanged; + + return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged); + + void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == nameof(BeatmapUserSettings.Offset)) + Current.Value = userSettings.Offset; + } + }); + Current.BindValueChanged(currentChanged); - Current.Value = realm.Run(r => r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings.Offset); } private Task realmWrite; private void currentChanged(ValueChangedEvent offset) { - if (useAverageButton != null) - { - useAverageButton.Enabled.Value = offset.NewValue != lastPlayAverage; - } - Scheduler.AddOnce(updateOffset); void updateOffset() { - // ensure the previous write has completed. + // ensure the previous write has completed. ignoring performance concerns, if we don't do this, the async writes could be out of sequence. if (realmWrite?.IsCompleted == false) { Scheduler.AddOnce(updateOffset); return; } - realmWrite?.WaitSafely(); + if (useAverageButton != null) + useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, Current.Value, Current.Precision); + realmWrite = realm.WriteAsync(r => { var settings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; - settings.Offset = offset.NewValue; + if (Precision.AlmostEquals(settings.Offset, Current.Value)) + return; + + settings.Offset = Current.Value; }); } } @@ -142,5 +161,11 @@ namespace osu.Game.Screens.Play.PlayerSettings }, }; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + beatmapOffsetSubscription?.Dispose(); + } } } From bc2a15db96f945cb0660617027901a4bb784da37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:20:18 +0900 Subject: [PATCH 289/306] Handle cases of beatmaps not existing in realm for tests --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 5 ++++- .../Play/PlayerSettings/BeatmapOffsetControl.cs | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 6a90e7adea..c7c967abfd 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -88,7 +88,10 @@ namespace osu.Game.Screens.Play beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => { - var userSettings = r.Find(beatmap.BeatmapInfo.ID).UserSettings; + var userSettings = r.Find(beatmap.BeatmapInfo.ID)?.UserSettings; + + if (userSettings == null) // only the case for tests. + return null; void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args) { diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 487d044f5b..7fbaaaeffc 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -85,7 +85,10 @@ namespace osu.Game.Screens.Play.PlayerSettings beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => { - var userSettings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + var userSettings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings; + + if (userSettings == null) // only the case for tests. + return null; Current.Value = userSettings.Offset; userSettings.PropertyChanged += onUserSettingsOnPropertyChanged; @@ -122,7 +125,10 @@ namespace osu.Game.Screens.Play.PlayerSettings realmWrite = realm.WriteAsync(r => { - var settings = r.Find(beatmap.Value.BeatmapInfo.ID).UserSettings; + var settings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings; + + if (settings == null) // only the case for tests. + return; if (Precision.AlmostEquals(settings.Offset, Current.Value)) return; From 4d9efe771bd5aab0db19e057410a2d678b882ca5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:31:11 +0900 Subject: [PATCH 290/306] Don't display calibration options when the previous play was too short to be useful --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 14 ++++++ .../PlayerSettings/BeatmapOffsetControl.cs | 46 +++++++++++++++---- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 3fb10b2d5c..7704233adf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -33,6 +33,20 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + [Test] + public void TestTooShortToDisplay() + { + AddStep("Set short reference score", () => + { + offsetControl.ReferenceScore.Value = new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2) + }; + }); + + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + } + [Test] public void TestDisplay() { diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 7fbaaaeffc..a8ed3f562b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -11,6 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; @@ -75,6 +77,9 @@ namespace osu.Game.Screens.Play.PlayerSettings [Resolved] private IBindable beatmap { get; set; } + [Resolved] + private OsuColour colours { get; set; } + private IDisposable beatmapOffsetSubscription; protected override void LoadComplete() @@ -140,32 +145,53 @@ namespace osu.Game.Screens.Play.PlayerSettings private void scoreChanged(ValueChangedEvent score) { - if (!(score.NewValue?.HitEvents.CalculateAverageHitError() is double average)) - { - referenceScoreContainer.Clear(); - return; - } + var hitEvents = score.NewValue?.HitEvents; - lastPlayAverage = average; + referenceScoreContainer.Clear(); + + if (!(hitEvents?.CalculateAverageHitError() is double average)) + return; referenceScoreContainer.Children = new Drawable[] { new OsuSpriteText { - Text = "Last play:" + Text = "Previous play:" }, - new HitEventTimingDistributionGraph(score.NewValue.HitEvents) + }; + + if (hitEvents.Count < 10) + { + referenceScoreContainer.AddRange(new Drawable[] + { + new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Colour = colours.Red1, + Text = "Previous play too short to use for calibration" + }, + }); + + return; + } + + lastPlayAverage = average; + + referenceScoreContainer.AddRange(new Drawable[] + { + new HitEventTimingDistributionGraph(hitEvents) { RelativeSizeAxes = Axes.X, Height = 50, }, - new AverageHitError(score.NewValue.HitEvents), + new AverageHitError(hitEvents), useAverageButton = new SettingsButton { Text = "Calibrate using last play", Action = () => Current.Value = lastPlayAverage }, - }; + }); } protected override void Dispose(bool isDisposing) From 4aee57c9c1f4700116fc740ef6db0c1dddd40860 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:33:30 +0900 Subject: [PATCH 291/306] Add localisation of all beatmap offset strings --- .../BeatmapOffsetControlStrings.cs | 34 +++++++++++++++++++ .../PlayerSettings/BeatmapOffsetControl.cs | 9 ++--- 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Localisation/BeatmapOffsetControlStrings.cs diff --git a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs new file mode 100644 index 0000000000..7b2a9e50b2 --- /dev/null +++ b/osu.Game/Localisation/BeatmapOffsetControlStrings.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 osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class BeatmapOffsetControlStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOffsetControl"; + + /// + /// "Beatmap offset" + /// + public static LocalisableString BeatmapOffset => new TranslatableString(getKey(@"beatmap_offset"), @"Beatmap offset"); + + /// + /// "Previous play:" + /// + public static LocalisableString PreviousPlay => new TranslatableString(getKey(@"previous_play"), @"Previous play:"); + + /// + /// "Previous play too short to use for calibration" + /// + public static LocalisableString PreviousPlayTooShortToUseForCalibration => new TranslatableString(getKey(@"previous_play_too_short_to_use_for_calibration"), @"Previous play too short to use for calibration"); + + /// + /// "Calibrate using last play" + /// + public static LocalisableString CalibrateUsingLastPlay => new TranslatableString(getKey(@"calibrate_using_last_play"), @"Calibrate using last play"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index a8ed3f562b..864a46fc8f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.Play.PlayerSettings { @@ -57,7 +58,7 @@ namespace osu.Game.Screens.Play.PlayerSettings new PlayerSliderBar { KeyboardStep = 5, - LabelText = "Beatmap offset", + LabelText = BeatmapOffsetControlStrings.BeatmapOffset, Current = Current, }, referenceScoreContainer = new FillFlowContainer @@ -156,7 +157,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { new OsuSpriteText { - Text = "Previous play:" + Text = BeatmapOffsetControlStrings.PreviousPlay }, }; @@ -169,7 +170,7 @@ namespace osu.Game.Screens.Play.PlayerSettings RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Colour = colours.Red1, - Text = "Previous play too short to use for calibration" + Text = BeatmapOffsetControlStrings.PreviousPlayTooShortToUseForCalibration }, }); @@ -188,7 +189,7 @@ namespace osu.Game.Screens.Play.PlayerSettings new AverageHitError(hitEvents), useAverageButton = new SettingsButton { - Text = "Calibrate using last play", + Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, Action = () => Current.Value = lastPlayAverage }, }); From 9792f0653ad46fe0b324fa16d341ede9f5f0d0d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:41:51 +0900 Subject: [PATCH 292/306] Don't show calibration controls for autoplay --- .../Play/PlayerSettings/BeatmapOffsetControl.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 864a46fc8f..1cd89f691e 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -20,6 +21,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osuTK; using osu.Game.Localisation; +using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Play.PlayerSettings { @@ -146,11 +148,17 @@ namespace osu.Game.Screens.Play.PlayerSettings private void scoreChanged(ValueChangedEvent score) { - var hitEvents = score.NewValue?.HitEvents; - referenceScoreContainer.Clear(); - if (!(hitEvents?.CalculateAverageHitError() is double average)) + if (score.NewValue == null) + return; + + if (score.NewValue.Mods.Any(m => m is ModAutoplay)) + return; + + var hitEvents = score.NewValue.HitEvents; + + if (!(hitEvents.CalculateAverageHitError() is double average)) return; referenceScoreContainer.Children = new Drawable[] From 7d11cfb301249b20f49ca9f150eaec936f5389fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:51:15 +0900 Subject: [PATCH 293/306] Add detach mapping for `BeatmapUserSettings` --- osu.Game/Database/RealmObjectExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index f89bbbe19d..6dc18df9e0 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -38,6 +38,7 @@ namespace osu.Game.Database c.CreateMap() .ForMember(s => s.Ruleset, cc => cc.Ignore()) .ForMember(s => s.Metadata, cc => cc.Ignore()) + .ForMember(s => s.UserSettings, cc => cc.Ignore()) .ForMember(s => s.Difficulty, cc => cc.Ignore()) .ForMember(s => s.BeatmapSet, cc => cc.Ignore()) .AfterMap((s, d) => @@ -154,6 +155,7 @@ namespace osu.Game.Database c.CreateMap(); c.CreateMap(); + c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); From 6c09237956c6535c54c37ef4ee2877af3cd0f10a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 19:57:09 +0900 Subject: [PATCH 294/306] Reorder fields in `BeatmapOffsetControl` and `MasterGameplayClockContainer` --- .../Play/MasterGameplayClockContainer.cs | 16 +++++++------- .../PlayerSettings/BeatmapOffsetControl.cs | 22 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index c7c967abfd..2b6db5f59e 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -61,6 +61,14 @@ namespace osu.Game.Screens.Play private Bindable userAudioOffset; private double startOffset; + private IDisposable beatmapOffsetSubscription; + + [Resolved] + private RealmAccess realm { get; set; } + + [Resolved] + private OsuConfigManager config { get; set; } + public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) : base(beatmap.Track) { @@ -71,14 +79,6 @@ namespace osu.Game.Screens.Play firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; } - [Resolved] - private RealmAccess realm { get; set; } - - [Resolved] - private OsuConfigManager config { get; set; } - - private IDisposable beatmapOffsetSubscription; - protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 1cd89f691e..02359afd16 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -44,6 +44,17 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly FillFlowContainer referenceScoreContainer; + private IDisposable beatmapOffsetSubscription; + + [Resolved] + private RealmAccess realm { get; set; } + + [Resolved] + private IBindable beatmap { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + public BeatmapOffsetControl() { RelativeSizeAxes = Axes.X; @@ -74,17 +85,6 @@ namespace osu.Game.Screens.Play.PlayerSettings }; } - [Resolved] - private RealmAccess realm { get; set; } - - [Resolved] - private IBindable beatmap { get; set; } - - [Resolved] - private OsuColour colours { get; set; } - - private IDisposable beatmapOffsetSubscription; - protected override void LoadComplete() { base.LoadComplete(); From 222f50d2119b9041c430ba7f4a54ab4c87d86dfe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Mar 2022 20:41:54 +0900 Subject: [PATCH 295/306] Fix calibration being back-to-front --- osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs | 2 +- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 7704233adf..42b579bc89 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("Has calibration button", () => offsetControl.ChildrenOfType().Any()); AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); - AddAssert("Offset is adjusted", () => offsetControl.Current.Value == average_error); + AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error); AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 02359afd16..e6bc510564 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -198,7 +198,7 @@ namespace osu.Game.Screens.Play.PlayerSettings useAverageButton = new SettingsButton { Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, - Action = () => Current.Value = lastPlayAverage + Action = () => Current.Value = -lastPlayAverage }, }); } From 2767dda9d63bf11a9eff75737cd47b53bb705649 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 1 Mar 2022 20:21:32 +0300 Subject: [PATCH 296/306] Add failing test case --- .../Visual/Ranking/TestSceneResultsScreen.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 167acc94c4..cc380df183 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -17,10 +17,13 @@ using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Ranking.Expanded.Statistics; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Tests.Resources; using osuTK; @@ -256,6 +259,23 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("download button is enabled", () => screen.ChildrenOfType().Last().Enabled.Value); } + [Test] + public void TestRulesetWithNoPerformanceCalculator() + { + var ruleset = new RulesetWithNoPerformanceCalculator(); + var score = TestResources.CreateTestScoreInfo(ruleset.RulesetInfo); + + AddStep("load results", () => Child = new TestResultsContainer(createResultsScreen(score))); + AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible); + + AddAssert("PP displayed as 0", () => + { + var performance = this.ChildrenOfType().Single(); + var counter = performance.ChildrenOfType().Single(); + return counter.Current.Value == 0; + }); + } + private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo()); private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo()); @@ -367,5 +387,10 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } } + + private class RulesetWithNoPerformanceCalculator : OsuRuleset + { + public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; + } } } From 97c54de3bff4787aa8161b4a33dd53500dd3bfdc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 1 Mar 2022 20:43:20 +0300 Subject: [PATCH 297/306] Fix performance statistic not handling rulesets with unimplemented calculator --- .../Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 859b42d66d..95f017d625 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics else { performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely().Total)), cancellationTokenSource.Token); + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()?.Total)), cancellationTokenSource.Token); } } From c07f7545653edc5efd3f4f90b1faa292774cc9b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:34:24 +0900 Subject: [PATCH 298/306] Enable `nullable` on `BeatmapOffsetControl` --- .../PlayerSettings/BeatmapOffsetControl.cs | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index e6bc510564..98820cabf8 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -23,6 +23,8 @@ using osuTK; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; +#nullable enable + namespace osu.Game.Screens.Play.PlayerSettings { public class BeatmapOffsetControl : CompositeDrawable @@ -38,22 +40,24 @@ namespace osu.Game.Screens.Play.PlayerSettings Precision = 0.1, }; - private SettingsButton useAverageButton; + private readonly FillFlowContainer referenceScoreContainer; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; private double lastPlayAverage; - private readonly FillFlowContainer referenceScoreContainer; + private SettingsButton? useAverageButton; - private IDisposable beatmapOffsetSubscription; + private IDisposable? beatmapOffsetSubscription; - [Resolved] - private RealmAccess realm { get; set; } - - [Resolved] - private IBindable beatmap { get; set; } - - [Resolved] - private OsuColour colours { get; set; } + private Task? realmWriteTask; public BeatmapOffsetControl() { @@ -113,8 +117,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Current.BindValueChanged(currentChanged); } - private Task realmWrite; - private void currentChanged(ValueChangedEvent offset) { Scheduler.AddOnce(updateOffset); @@ -122,7 +124,7 @@ namespace osu.Game.Screens.Play.PlayerSettings void updateOffset() { // ensure the previous write has completed. ignoring performance concerns, if we don't do this, the async writes could be out of sequence. - if (realmWrite?.IsCompleted == false) + if (realmWriteTask?.IsCompleted == false) { Scheduler.AddOnce(updateOffset); return; @@ -131,7 +133,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (useAverageButton != null) useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, Current.Value, Current.Precision); - realmWrite = realm.WriteAsync(r => + realmWriteTask = realm.WriteAsync(r => { var settings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings; From 3cbcb702f6f4aa9d3d165f63735d6c57b2cf829c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:36:15 +0900 Subject: [PATCH 299/306] Fix calibration button disabled state not checking in corrrect direction --- osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs | 1 + osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 42b579bc89..67f5db548b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -66,6 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error); + AddAssert("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value); AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 98820cabf8..ca683cec66 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.Play.PlayerSettings } if (useAverageButton != null) - useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, Current.Value, Current.Precision); + useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, -Current.Value, Current.Precision); realmWriteTask = realm.WriteAsync(r => { From 8bd66f1ed7302577801926dc55baeeac4d1720d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:36:49 +0900 Subject: [PATCH 300/306] Fix incorrect precision specification for button disable check --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index ca683cec66..63045013b5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.Play.PlayerSettings } if (useAverageButton != null) - useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, -Current.Value, Current.Precision); + useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, -Current.Value, Current.Precision / 2); realmWriteTask = realm.WriteAsync(r => { From e184b26cdd67101488bbbcdf8c03940997a13da3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:39:28 +0900 Subject: [PATCH 301/306] Remove `Precision` call for database write shortcutting Shouldn't be required. --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 63045013b5..f19a326cdf 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (settings == null) // only the case for tests. return; - if (Precision.AlmostEquals(settings.Offset, Current.Value)) + if (settings.Offset == Current.Value) return; settings.Offset = Current.Value; From 763f881d4a0ade9930cfffafb974883c6f6e2385 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:42:08 +0900 Subject: [PATCH 302/306] Use more correct mod check to encompass more than just autoplay --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index f19a326cdf..dc3e80d695 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -15,13 +15,12 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osuTK; -using osu.Game.Localisation; -using osu.Game.Rulesets.Mods; #nullable enable @@ -155,7 +154,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (score.NewValue == null) return; - if (score.NewValue.Mods.Any(m => m is ModAutoplay)) + if (score.NewValue.Mods.Any(m => !m.UserPlayable)) return; var hitEvents = score.NewValue.HitEvents; From ed9ecd695114e1afabd51213767a206798791c92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 14:45:39 +0900 Subject: [PATCH 303/306] Fix test scene failures by ensuring that first `GameplayClock` frame has processed first --- osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index e03c8d7561..b195d2aa74 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -131,9 +131,9 @@ namespace osu.Game.Tests.Visual.Gameplay public double GameplayClockTime => GameplayClockContainer.GameplayClock.CurrentTime; - protected override void Update() + protected override void UpdateAfterChildren() { - base.Update(); + base.UpdateAfterChildren(); if (!FirstFrameClockTime.HasValue) { From 1a474592625ccc97b47b11bdb953dcdbf3fdd8a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 18:38:17 +0900 Subject: [PATCH 304/306] Fix taiko difficulty adjust scroll speed being shown with too low precision --- osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs index 9540e35780..99a064d35f 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { get { - string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N1}"; + string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N2}"; return string.Join(", ", new[] { From f15b8781bb92e9d74c89357a10b32e77a0de84b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 19:19:39 +0900 Subject: [PATCH 305/306] Move editor mode selector out of `EditorMenuBar` to allow for better reuse --- .../Editing/TestSceneEditorScreenModes.cs | 4 +- .../Edit/Components/Menus/EditorMenuBar.cs | 22 ----- osu.Game/Screens/Edit/Editor.cs | 82 +++++++++++-------- 3 files changed, 47 insertions(+), 61 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs index 98d8a41674..2efd125f81 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorScreenModes.cs @@ -4,11 +4,9 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Components.Menus; namespace osu.Game.Tests.Visual.Editing { @@ -22,7 +20,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("switch between all screens at once", () => { foreach (var screen in Enum.GetValues(typeof(EditorScreenMode)).Cast()) - Editor.ChildrenOfType().Single().Mode.Value = screen; + Editor.Mode.Value = screen; }); } } diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index c6787a1fb1..2a8435ff47 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.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.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components.Menus { public class EditorMenuBar : OsuMenu { - public readonly Bindable Mode = new Bindable(); - public EditorMenuBar() : base(Direction.Horizontal, true) { @@ -28,25 +25,6 @@ namespace osu.Game.Screens.Edit.Components.Menus MaskingContainer.CornerRadius = 0; ItemsContainer.Padding = new MarginPadding { Left = 100 }; BackgroundColour = Color4Extensions.FromHex("111"); - - ScreenSelectionTabControl tabControl; - AddRangeInternal(new Drawable[] - { - tabControl = new ScreenSelectionTabControl - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - X = -15 - } - }); - - Mode.BindTo(tabControl.Current); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Mode.TriggerChange(); } protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c2775ae101..dcb7e3a282 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -89,6 +89,8 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private NotificationOverlay notifications { get; set; } + public readonly Bindable Mode = new Bindable(); + public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; private readonly Bindable samplePlaybackDisabled = new Bindable(); @@ -115,8 +117,6 @@ namespace osu.Game.Screens.Edit [CanBeNull] // Should be non-null once it can support custom rulesets. private EditorChangeHandler changeHandler; - private EditorMenuBar menuBar; - private DependencyContainer dependencies; private TestGameplayButton testGameplayButton; @@ -239,40 +239,49 @@ namespace osu.Game.Screens.Edit Name = "Top bar", RelativeSizeAxes = Axes.X, Height = 40, - Child = menuBar = new EditorMenuBar + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Mode = { Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose }, - Items = new[] + new EditorMenuBar { - new MenuItem("File") + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Items = new[] { - Items = createFileMenuItems() - }, - new MenuItem("Edit") - { - Items = new[] + new MenuItem("File") { - undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo), - new EditorMenuItemSpacer(), - cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut), - copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), - pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), - } - }, - new MenuItem("View") - { - Items = new MenuItem[] + Items = createFileMenuItems() + }, + new MenuItem("Edit") { - new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), - new HitAnimationsMenuItem(config.GetBindable(OsuSetting.EditorHitAnimations)) + Items = new[] + { + undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo), + redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo), + new EditorMenuItemSpacer(), + cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), + } + }, + new MenuItem("View") + { + Items = new MenuItem[] + { + new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), + new HitAnimationsMenuItem(config.GetBindable(OsuSetting.EditorHitAnimations)) + } } } - } - } + }, + new ScreenSelectionTabControl + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -15, + Current = Mode, + }, + }, }, new Container { @@ -340,14 +349,15 @@ namespace osu.Game.Screens.Edit changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); - - menuBar.Mode.ValueChanged += onModeChanged; } protected override void LoadComplete() { base.LoadComplete(); setUpClipboardActionAvailability(); + + Mode.Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose; + Mode.BindValueChanged(onModeChanged, true); } /// @@ -517,23 +527,23 @@ namespace osu.Game.Screens.Edit return true; case GlobalAction.EditorComposeMode: - menuBar.Mode.Value = EditorScreenMode.Compose; + Mode.Value = EditorScreenMode.Compose; return true; case GlobalAction.EditorDesignMode: - menuBar.Mode.Value = EditorScreenMode.Design; + Mode.Value = EditorScreenMode.Design; return true; case GlobalAction.EditorTimingMode: - menuBar.Mode.Value = EditorScreenMode.Timing; + Mode.Value = EditorScreenMode.Timing; return true; case GlobalAction.EditorSetupMode: - menuBar.Mode.Value = EditorScreenMode.SongSetup; + Mode.Value = EditorScreenMode.SongSetup; return true; case GlobalAction.EditorVerifyMode: - menuBar.Mode.Value = EditorScreenMode.Verify; + Mode.Value = EditorScreenMode.Verify; return true; case GlobalAction.EditorTestGameplay: From ff7db4f4059f70a67d91f6c37fb683c6a7effa23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 20:05:37 +0900 Subject: [PATCH 306/306] Replace jank buttons with menu in skin editor --- osu.Game/Skinning/Editor/SkinEditor.cs | 88 ++++++++++++-------------- 1 file changed, 39 insertions(+), 49 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 8052f82c93..ae5cbc95f0 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -8,14 +8,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; -using osu.Game.Resources.Localisation.Web; -using osuTK; +using osu.Game.Screens.Edit.Components.Menus; namespace osu.Game.Skinning.Editor { @@ -57,13 +57,43 @@ namespace osu.Game.Skinning.Editor RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - headerText = new OsuTextFlowContainer + new Container { - TextAnchor = Anchor.TopCentre, - Padding = new MarginPadding(20), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X + Name = "Top bar", + RelativeSizeAxes = Axes.X, + Depth = float.MinValue, + Height = 40, + Children = new Drawable[] + { + new EditorMenuBar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Items = new[] + { + new MenuItem("File") + { + Items = new[] + { + new EditorMenuItem("Save", MenuItemType.Standard, Save), + new EditorMenuItem("Revert to default", MenuItemType.Destructive, revert), + new EditorMenuItemSpacer(), + new EditorMenuItem("Exit", MenuItemType.Standard, Hide), + }, + }, + } + }, + headerText = new OsuTextFlowContainer + { + TextAnchor = Anchor.TopRight, + Padding = new MarginPadding(5), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + }, + }, }, new GridContainer { @@ -89,46 +119,6 @@ namespace osu.Game.Skinning.Editor Children = new Drawable[] { new SkinBlueprintContainer(targetScreen), - new TriangleButton - { - Margin = new MarginPadding(10), - Text = CommonStrings.ButtonsClose, - Width = 100, - Action = Hide, - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Spacing = new Vector2(5), - Padding = new MarginPadding - { - Top = 10, - Left = 10, - }, - Margin = new MarginPadding - { - Right = 10, - Bottom = 10, - }, - Children = new Drawable[] - { - new TriangleButton - { - Text = "Save Changes", - Width = 140, - Action = Save, - }, - new DangerousTriangleButton - { - Text = "Revert to default", - Width = 140, - Action = revert, - }, - } - }, } }, } @@ -161,7 +151,7 @@ namespace osu.Game.Skinning.Editor { headerText.Clear(); - headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 24)); + headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 16)); headerText.NewParagraph(); headerText.AddText("Currently editing ", cp => {