From 84637b59ef836b88f61842e4543920f3729d02be Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 25 Aug 2021 07:40:41 +0300 Subject: [PATCH 01/11] Define `DifficultyBindableWithCurrent` and use in `SliderControl` --- .../Mods/DifficultyAdjustSettingsControl.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs index 3978378c3a..67b24d24d0 100644 --- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.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.Bindables; using osu.Framework.Graphics; @@ -91,7 +92,7 @@ namespace osu.Game.Rulesets.Mods { // This is required as SettingsItem relies heavily on this bindable for internal use. // The actual update flow is done via the bindable provided in the constructor. - private readonly BindableWithCurrent current = new BindableWithCurrent(); + private readonly DifficultyBindableWithCurrent current = new DifficultyBindableWithCurrent(); public Bindable Current { @@ -114,5 +115,30 @@ namespace osu.Game.Rulesets.Mods RelativeSizeAxes = Axes.X; } } + + private class DifficultyBindableWithCurrent : DifficultyBindable, IHasCurrentValue + { + private Bindable currentBound; + + public Bindable Current + { + get => this; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (currentBound != null) UnbindFrom(currentBound); + BindTo(currentBound = value); + } + } + + public DifficultyBindableWithCurrent(float? defaultValue = default) + : base(defaultValue) + { + } + + protected override Bindable CreateInstance() => new DifficultyBindableWithCurrent(); + } } } From f9b25a01590ac076ac3dfc98eda9e7dab552d359 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 25 Aug 2021 15:09:57 +0300 Subject: [PATCH 02/11] Add test case for switching to each screen in editor test scenes --- osu.Game/Tests/Visual/EditorTestScene.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index a393802309..2644daa3a4 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -3,6 +3,7 @@ using System.Linq; using JetBrains.Annotations; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.IO.Stores; @@ -28,6 +29,9 @@ namespace osu.Game.Tests.Visual protected EditorClock EditorClock { get; private set; } + [Resolved] + private SkinManager skins { get; set; } + /// /// Whether any saves performed by the editor should be isolate (and not persist) to the underlying . /// @@ -57,6 +61,12 @@ namespace osu.Game.Tests.Visual AddStep("get clock", () => EditorClock = Editor.ChildrenOfType().Single()); } + [Test] + public void TestLegacySkin() + { + AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info); + } + protected virtual void LoadEditor() { LoadScreen(Editor = CreateEditor()); From b4d6495f99a60194a8609cf3d35d048166001b03 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 25 Aug 2021 15:03:20 +0300 Subject: [PATCH 03/11] Fix editor skin providing container not providing playable beatmap --- osu.Game/Screens/Edit/EditorSkinProvidingContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorSkinProvidingContainer.cs b/osu.Game/Screens/Edit/EditorSkinProvidingContainer.cs index 27563b5a0f..decfa879a8 100644 --- a/osu.Game/Screens/Edit/EditorSkinProvidingContainer.cs +++ b/osu.Game/Screens/Edit/EditorSkinProvidingContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit private readonly EditorBeatmapSkin? beatmapSkin; public EditorSkinProvidingContainer(EditorBeatmap editorBeatmap) - : base(editorBeatmap.PlayableBeatmap.BeatmapInfo.Ruleset.CreateInstance(), editorBeatmap, editorBeatmap.BeatmapSkin) + : base(editorBeatmap.PlayableBeatmap.BeatmapInfo.Ruleset.CreateInstance(), editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin) { beatmapSkin = editorBeatmap.BeatmapSkin; } From 956e3c554bd420371fa9b7d3242643184fc6efd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Aug 2021 00:50:55 +0900 Subject: [PATCH 04/11] Avoid skip overlay attempting to show when it is already invalid --- osu.Game/Screens/Play/SkipOverlay.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 4a74fa1d4f..dd6ba3c2f1 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -37,6 +37,8 @@ namespace osu.Game.Screens.Play private FadeContainer fadeContainer; private double displayTime; + private bool isClickable; + [Resolved] private GameplayClock gameplayClock { get; set; } @@ -124,17 +126,18 @@ namespace osu.Game.Screens.Play { base.Update(); - var progress = fadeOutBeginTime <= displayTime ? 1 : Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); + double progress = fadeOutBeginTime <= displayTime ? 1 : Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); - button.Enabled.Value = progress > 0; - buttonContainer.State.Value = progress > 0 ? Visibility.Visible : Visibility.Hidden; + isClickable = progress > 0; + button.Enabled.Value = isClickable; + buttonContainer.State.Value = isClickable ? Visibility.Visible : Visibility.Hidden; } protected override bool OnMouseMove(MouseMoveEvent e) { - if (!e.HasAnyButtonPressed) + if (isClickable && !e.HasAnyButtonPressed) fadeContainer.Show(); return base.OnMouseMove(e); From b1a261c9029e1b6e5cf0b92da39a5fc3d64c4213 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Aug 2021 01:02:27 +0900 Subject: [PATCH 05/11] Avoid using scheduled delegates at all for skip overload input handling --- osu.Game/Screens/Play/SkipOverlay.cs | 46 ++++++++++++++++++---------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index dd6ba3c2f1..71a70991d2 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -120,6 +119,8 @@ namespace osu.Game.Screens.Play button.Action = () => RequestSkip?.Invoke(); displayTime = gameplayClock.CurrentTime; + + fadeContainer.TriggerShow(); } protected override void Update() @@ -138,7 +139,7 @@ namespace osu.Game.Screens.Play protected override bool OnMouseMove(MouseMoveEvent e) { if (isClickable && !e.HasAnyButtonPressed) - fadeContainer.Show(); + fadeContainer.TriggerShow(); return base.OnMouseMove(e); } @@ -167,34 +168,45 @@ namespace osu.Game.Screens.Play public event Action StateChanged; private Visibility state; - private ScheduledDelegate scheduledHide; + private double? nextHideTime; public override bool IsPresent => true; + public void TriggerShow() + { + Show(); + + if (!IsHovered && !IsDragged) + nextHideTime = Time.Current + 1000; + else + nextHideTime = null; + } + + protected override void Update() + { + base.Update(); + + if (nextHideTime != null && nextHideTime <= Time.Current) + { + Hide(); + nextHideTime = null; + } + } + public Visibility State { get => state; set { - bool stateChanged = value != state; + if (value == state) + return; state = value; - scheduledHide?.Cancel(); - switch (state) { case Visibility.Visible: - // we may be triggered to become visible multiple times but we only want to transform once. - if (stateChanged) - this.FadeIn(500, Easing.OutExpo); - - if (!IsHovered && !IsDragged) - { - using (BeginDelayedSequence(1000)) - scheduledHide = Schedule(Hide); - } - + this.FadeIn(500, Easing.OutExpo); break; case Visibility.Hidden: @@ -215,7 +227,7 @@ namespace osu.Game.Screens.Play protected override bool OnMouseDown(MouseDownEvent e) { Show(); - scheduledHide?.Cancel(); + nextHideTime = null; return true; } From 69064c1938e6f574ca8908939fc144aeb35cafae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Aug 2021 03:41:58 +0900 Subject: [PATCH 06/11] Avoid unnecessary unbind operations when constructing `FollowPointLifetimeEntry` --- .../Connections/FollowPointLifetimeEntry.cs | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs index 82bca0a4e2..c0d3572adf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Objects; @@ -20,8 +21,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { Start = start; LifetimeStart = Start.StartTime; - - bindEvents(); } private OsuHitObject? end; @@ -41,31 +40,39 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections } } + private bool wasBound = false; + private void bindEvents() { UnbindEvents(); + if (End == null) + return; + // Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects. Start.DefaultsApplied += onDefaultsApplied; Start.PositionBindable.ValueChanged += onPositionChanged; - if (End != null) - { - End.DefaultsApplied += onDefaultsApplied; - End.PositionBindable.ValueChanged += onPositionChanged; - } + End.DefaultsApplied += onDefaultsApplied; + End.PositionBindable.ValueChanged += onPositionChanged; + + wasBound = true; } public void UnbindEvents() { + if (!wasBound) + return; + + Debug.Assert(End != null); + Start.DefaultsApplied -= onDefaultsApplied; Start.PositionBindable.ValueChanged -= onPositionChanged; - if (End != null) - { - End.DefaultsApplied -= onDefaultsApplied; - End.PositionBindable.ValueChanged -= onPositionChanged; - } + End.DefaultsApplied -= onDefaultsApplied; + End.PositionBindable.ValueChanged -= onPositionChanged; + + wasBound = false; } private void onDefaultsApplied(HitObject obj) => refreshLifetimes(); From f4199958d93995b0db1235fc986e1f07d1ef80ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Aug 2021 04:01:37 +0900 Subject: [PATCH 07/11] Avoid unnecessary array/LINQ operations when replay frames have no action changes --- osu.Game/Input/Handlers/ReplayInputHandler.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs index cd76000f98..e4aec4edac 100644 --- a/osu.Game/Input/Handlers/ReplayInputHandler.cs +++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs @@ -42,9 +42,24 @@ namespace osu.Game.Input.Handlers if (!(state is RulesetInputManagerInputState inputState)) throw new InvalidOperationException($"{nameof(ReplayState)} should only be applied to a {nameof(RulesetInputManagerInputState)}"); - var lastPressed = inputState.LastReplayState?.PressedActions ?? new List(); - var released = lastPressed.Except(PressedActions).ToArray(); - var pressed = PressedActions.Except(lastPressed).ToArray(); + T[] released = Array.Empty(); + T[] pressed = Array.Empty(); + + var lastPressed = inputState.LastReplayState?.PressedActions; + + if (lastPressed == null || lastPressed.Count == 0) + { + pressed = PressedActions.ToArray(); + } + else if (PressedActions.Count == 0) + { + released = lastPressed.ToArray(); + } + else if (!lastPressed.SequenceEqual(PressedActions)) + { + released = lastPressed.Except(PressedActions).ToArray(); + pressed = PressedActions.Except(lastPressed).ToArray(); + } inputState.LastReplayState = this; From e32933eb54f5df45e18367387a53c9d32401601f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Aug 2021 04:19:49 +0900 Subject: [PATCH 08/11] Avoid `Enum.GetValues` in each score population pass --- osu.Game/Rulesets/Scoring/HitResult.cs | 7 +++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index eaa1f95744..5599ed96a3 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; using System.Diagnostics; +using System.Linq; using osu.Framework.Utils; namespace osu.Game.Rulesets.Scoring @@ -171,6 +173,11 @@ namespace osu.Game.Rulesets.Scoring /// public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss; + /// + /// An array of all scorable s. + /// + public static readonly HitResult[] SCORABLE_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).Where(r => r.IsScorable()).ToArray(); + /// /// Whether a is valid within a given range. /// diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 6a2601170c..16f2607bad 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -339,7 +339,7 @@ namespace osu.Game.Rulesets.Scoring score.Accuracy = Accuracy.Value; score.Rank = Rank.Value; - foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.IsScorable())) + foreach (var result in HitResultExtensions.SCORABLE_TYPES) score.Statistics[result] = GetStatistic(result); score.HitEvents = hitEvents; From e633b2716d99270bb722521ec71718e43c7600be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Aug 2021 13:58:23 +0900 Subject: [PATCH 09/11] Fix regression in outro skip handling logic --- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 71a70991d2..a2145b7014 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Play public override void Show() { base.Show(); - fadeContainer.Show(); + fadeContainer.TriggerShow(); } protected override void LoadComplete() From b9ea984c360bebb3d95dac301a9fd01c4ca63900 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Aug 2021 08:18:58 +0300 Subject: [PATCH 10/11] Remove redundant default value --- .../Objects/Drawables/Connections/FollowPointLifetimeEntry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs index c0d3572adf..30ff6b8984 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections } } - private bool wasBound = false; + private bool wasBound; private void bindEvents() { From 15812520bd6d99105fb71c4b0448fb6bd545cd6f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Aug 2021 09:44:53 +0300 Subject: [PATCH 11/11] Replace global editor test case with mania compose screen test scene --- .../Editor/TestSceneManiaComposeScreen.cs | 64 +++++++++++++++++++ osu.Game/Tests/Visual/EditorTestScene.cs | 10 --- 2 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs new file mode 100644 index 0000000000..0f520215a1 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.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; +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.Rulesets.Edit; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Editor +{ + public class TestSceneManiaComposeScreen : EditorClockTestScene + { + [Resolved] + private SkinManager skins { get; set; } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("setup compose screen", () => + { + var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })) + { + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, + }; + + Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(EditorBeatmap), editorBeatmap), + (typeof(IBeatSnapProvider), editorBeatmap), + }, + Child = new ComposeScreen { State = { Value = Visibility.Visible } }, + }; + }); + + AddUntilStep("wait for composer", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + } + + [Test] + public void TestDefaultSkin() + { + AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default); + } + + [Test] + public void TestLegacySkin() + { + AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info); + } + } +} diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 2644daa3a4..a393802309 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -3,7 +3,6 @@ using System.Linq; using JetBrains.Annotations; -using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.IO.Stores; @@ -29,9 +28,6 @@ namespace osu.Game.Tests.Visual protected EditorClock EditorClock { get; private set; } - [Resolved] - private SkinManager skins { get; set; } - /// /// Whether any saves performed by the editor should be isolate (and not persist) to the underlying . /// @@ -61,12 +57,6 @@ namespace osu.Game.Tests.Visual AddStep("get clock", () => EditorClock = Editor.ChildrenOfType().Single()); } - [Test] - public void TestLegacySkin() - { - AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info); - } - protected virtual void LoadEditor() { LoadScreen(Editor = CreateEditor());