From 2f33aeac9ff14d075957d7c053dd7b384d2caa37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 14:31:07 +0100 Subject: [PATCH 01/35] Move drawable instatiation to [SetUp] --- .../Skinning/ManiaHitObjectTestScene.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index d24c81dac6..96444fd316 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.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. -using osu.Framework.Allocation; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning /// public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene { - [BackgroundDependencyLoader] - private void load() + [SetUp] + public void SetUp() => Schedule(() => { SetContents(() => new FillFlowContainer { @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning }, } }); - } + }); protected abstract DrawableManiaHitObject CreateHitObject(); } From 2ccc81ccc0ca7406d21e6283c60a77f5849bf681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 14:31:24 +0100 Subject: [PATCH 02/35] Add test case for fading hold note --- .../Skinning/TestSceneHoldNote.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs index 9c4c2b3d5b..e88ff8e2ac 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; @@ -26,6 +27,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning }); } + [Test] + public void TestFadeOnMiss() + { + AddStep("miss tick", () => + { + foreach (var holdNote in holdNotes) + holdNote.ChildrenOfType().First().MissForcefully(); + }); + } + + private IEnumerable holdNotes => CreatedDrawables.SelectMany(d => d.ChildrenOfType()); + protected override DrawableManiaHitObject CreateHitObject() { var note = new HoldNote { Duration = 1000 }; From 55a91dbbe031df2bb677a6518cf035961d4faf1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 15:16:33 +0100 Subject: [PATCH 03/35] Add fading on hit state change --- .../Skinning/LegacyBodyPiece.cs | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index c0f0fcb4af..a9cd7b592f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Mania.Skinning { public class LegacyBodyPiece : LegacyManiaColumnElement { + private DrawableHoldNote holdNote; + private readonly IBindable direction = new Bindable(); private readonly IBindable isHitting = new Bindable(); @@ -38,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject) { + holdNote = (DrawableHoldNote)drawableObject; + string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value ?? $"mania-note{FallbackColumnIndex}L"; @@ -92,11 +96,44 @@ namespace osu.Game.Rulesets.Mania.Skinning InternalChild = bodySprite; direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(onDirectionChanged, true); - - var holdNote = (DrawableHoldNote)drawableObject; isHitting.BindTo(holdNote.IsHitting); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + direction.BindValueChanged(onDirectionChanged, true); isHitting.BindValueChanged(onIsHittingChanged, true); + + holdNote.ApplyCustomUpdateState += applyCustomUpdateState; + applyCustomUpdateState(holdNote, holdNote.State.Value); + } + + /// + /// Stores the start time of the fade animation that plays when any of the nested + /// hitobjects of the hold note are missed. + /// + private double? missFadeTime; + + private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) + { + switch (state) + { + case ArmedState.Miss: + missFadeTime ??= hitObject.StateUpdateTime; + + using (BeginAbsoluteSequence(missFadeTime.Value)) + { + // colour and duration matches stable + // transforms not applied to entire hold note in order to not affect hit lighting + holdNote.Head.FadeColour(Colour4.DarkGray, 60); + bodySprite?.FadeColour(Colour4.DarkGray, 60); + holdNote.Tail.FadeColour(Colour4.DarkGray, 60); + } + + break; + } } private void onIsHittingChanged(ValueChangedEvent isHitting) @@ -162,6 +199,9 @@ namespace osu.Game.Rulesets.Mania.Skinning { base.Dispose(isDisposing); + if (holdNote != null) + holdNote.ApplyCustomUpdateState -= applyCustomUpdateState; + lightContainer?.Expire(); } } From 4777b1be8108462cad86c5b81e2865888a1972e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 15:43:10 +0100 Subject: [PATCH 04/35] Fix fade not applying to tails sometimes --- .../Skinning/LegacyBodyPiece.cs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index a9cd7b592f..a0cbc47df1 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -118,21 +118,22 @@ namespace osu.Game.Rulesets.Mania.Skinning private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) { - switch (state) + if (state == ArmedState.Miss) + missFadeTime = hitObject.StateUpdateTime; + + if (missFadeTime == null) + return; + + // this state update could come from any nested object of the hold note. + // make sure the transforms are consistent across all affected parts + // even if they're idle. + using (BeginAbsoluteSequence(missFadeTime.Value)) { - case ArmedState.Miss: - missFadeTime ??= hitObject.StateUpdateTime; - - using (BeginAbsoluteSequence(missFadeTime.Value)) - { - // colour and duration matches stable - // transforms not applied to entire hold note in order to not affect hit lighting - holdNote.Head.FadeColour(Colour4.DarkGray, 60); - bodySprite?.FadeColour(Colour4.DarkGray, 60); - holdNote.Tail.FadeColour(Colour4.DarkGray, 60); - } - - break; + // colour and duration matches stable + // transforms not applied to entire hold note in order to not affect hit lighting + holdNote.Head.FadeColour(Colour4.DarkGray, 60); + bodySprite?.FadeColour(Colour4.DarkGray, 60); + holdNote.Tail.FadeColour(Colour4.DarkGray, 60); } } From b62bf5798d40d0df7cd0e8ed63e59215dae6a088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 21:14:34 +0100 Subject: [PATCH 05/35] Store time of hold note break --- .../Objects/Drawables/DrawableHoldNote.cs | 8 ++++---- .../Objects/Drawables/DrawableHoldNoteTail.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 59899637f9..a64cc6dc67 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -51,9 +51,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public double? HoldStartTime { get; private set; } /// - /// Whether the hold note has been released too early and shouldn't give full score for the release. + /// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score. /// - public bool HasBroken { get; private set; } + public double? HoldBrokenTime { get; private set; } /// /// Whether the hold note has been released potentially without having caused a break. @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } if (Tail.Judged && !Tail.IsHit) - HasBroken = true; + HoldBrokenTime = Time.Current; } public bool OnPressed(ManiaAction action) @@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // If the key has been released too early, the user should not receive full score for the release if (!Tail.IsHit) - HasBroken = true; + HoldBrokenTime = Time.Current; releaseTime = Time.Current; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index c780c0836e..a4029e7893 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables ApplyResult(r => { // If the head wasn't hit or the hold note was broken, cap the max score to Meh. - if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken)) + if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null)) result = HitResult.Meh; r.Type = result; From a199a957cca03746a93b2ebf1e5ffa0a90b43bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 21:47:55 +0100 Subject: [PATCH 06/35] Use stored hold note break time to fade upon it --- .../Skinning/LegacyBodyPiece.cs | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index a0cbc47df1..9ed25115d8 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -23,6 +23,12 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly IBindable direction = new Bindable(); private readonly IBindable isHitting = new Bindable(); + /// + /// Stores the start time of the fade animation that plays when any of the nested + /// hitobjects of the hold note are missed. + /// + private readonly Bindable missFadeTime = new Bindable(); + [CanBeNull] private Drawable bodySprite; @@ -105,36 +111,16 @@ namespace osu.Game.Rulesets.Mania.Skinning direction.BindValueChanged(onDirectionChanged, true); isHitting.BindValueChanged(onIsHittingChanged, true); + missFadeTime.BindValueChanged(onMissFadeTimeChanged, true); holdNote.ApplyCustomUpdateState += applyCustomUpdateState; applyCustomUpdateState(holdNote, holdNote.State.Value); } - /// - /// Stores the start time of the fade animation that plays when any of the nested - /// hitobjects of the hold note are missed. - /// - private double? missFadeTime; - private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) { if (state == ArmedState.Miss) - missFadeTime = hitObject.StateUpdateTime; - - if (missFadeTime == null) - return; - - // this state update could come from any nested object of the hold note. - // make sure the transforms are consistent across all affected parts - // even if they're idle. - using (BeginAbsoluteSequence(missFadeTime.Value)) - { - // colour and duration matches stable - // transforms not applied to entire hold note in order to not affect hit lighting - holdNote.Head.FadeColour(Colour4.DarkGray, 60); - bodySprite?.FadeColour(Colour4.DarkGray, 60); - holdNote.Tail.FadeColour(Colour4.DarkGray, 60); - } + missFadeTime.Value ??= hitObject.StateUpdateTime; } private void onIsHittingChanged(ValueChangedEvent isHitting) @@ -196,6 +182,29 @@ namespace osu.Game.Rulesets.Mania.Skinning } } + private void onMissFadeTimeChanged(ValueChangedEvent missFadeTimeChange) + { + if (missFadeTimeChange.NewValue == null) + return; + + // this update could come from any nested object of the hold note (or even from an input). + // make sure the transforms are consistent across all affected parts. + using (BeginAbsoluteSequence(missFadeTimeChange.NewValue.Value)) + { + // colour and duration matches stable + // transforms not applied to entire hold note in order to not affect hit lighting + holdNote.Head.FadeColour(Colour4.DarkGray, 60); + holdNote.Tail.FadeColour(Colour4.DarkGray, 60); + bodySprite?.FadeColour(Colour4.DarkGray, 60); + } + } + + protected override void Update() + { + base.Update(); + missFadeTime.Value ??= holdNote.HoldBrokenTime; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From ba30800bf4d242b30f0909bf97a146cced4e6ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 22:21:22 +0100 Subject: [PATCH 07/35] Extract constant --- osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 9ed25115d8..f1f72c8618 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -193,9 +193,11 @@ namespace osu.Game.Rulesets.Mania.Skinning { // colour and duration matches stable // transforms not applied to entire hold note in order to not affect hit lighting - holdNote.Head.FadeColour(Colour4.DarkGray, 60); - holdNote.Tail.FadeColour(Colour4.DarkGray, 60); - bodySprite?.FadeColour(Colour4.DarkGray, 60); + const double fade_duration = 60; + + holdNote.Head.FadeColour(Colour4.DarkGray, fade_duration); + holdNote.Tail.FadeColour(Colour4.DarkGray, fade_duration); + bodySprite?.FadeColour(Colour4.DarkGray, fade_duration); } } From 9a7fdb2b7e31e0495c12e5294c89a24042f8254d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Nov 2020 16:43:17 +0900 Subject: [PATCH 08/35] Move skin deletion logic to OsuGameBase to promote thread safety `CurrentSkinInfo` is used in multiple places expecting thread safety, while ItemRemoved events are explicitly mentioning they are not thread safe. As SkinManager itself doesn't have the ability to schedule to the update thread, I've just moved the logic to `OsuGameBase`. We may want to move the current skin bindable out of the manager class in the future to match things like `BeatmapManager`. Closes https://github.com/ppy/osu/issues/10837. --- osu.Game/OsuGameBase.cs | 11 +++++++++++ osu.Game/Skinning/SkinManager.cs | 10 ---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 193f6fe61b..1147f67ad2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -194,6 +194,17 @@ namespace osu.Game dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); dependencies.CacheAs(SkinManager); + // needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo. + SkinManager.ItemRemoved.BindValueChanged(weakRemovedInfo => + { + if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo)) + { + // check the removed skin is not the current user choice. if it is, switch back to default. + if (removedInfo.ID == SkinManager.CurrentSkinInfo.Value.ID) + Schedule(() => SkinManager.CurrentSkinInfo.Value = SkinInfo.Default); + } + }); + dependencies.CacheAs(API ??= new APIAccess(LocalConfig)); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index bef3e86a4d..9b69a1eecd 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -48,16 +48,6 @@ namespace osu.Game.Skinning this.audio = audio; this.legacyDefaultResources = legacyDefaultResources; - ItemRemoved.BindValueChanged(weakRemovedInfo => - { - if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo)) - { - // check the removed skin is not the current user choice. if it is, switch back to default. - if (removedInfo.ID == CurrentSkinInfo.Value.ID) - CurrentSkinInfo.Value = SkinInfo.Default; - } - }); - CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue); CurrentSkin.ValueChanged += skin => { From 709370c69b366718596ccb6409a992c1ffacf957 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Nov 2020 16:49:31 +0900 Subject: [PATCH 09/35] Move schedule call outwards --- osu.Game/OsuGameBase.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1147f67ad2..e7b5d3304d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -199,9 +199,12 @@ namespace osu.Game { if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo)) { - // check the removed skin is not the current user choice. if it is, switch back to default. - if (removedInfo.ID == SkinManager.CurrentSkinInfo.Value.ID) - Schedule(() => SkinManager.CurrentSkinInfo.Value = SkinInfo.Default); + Schedule(() => + { + // check the removed skin is not the current user choice. if it is, switch back to default. + if (removedInfo.ID == SkinManager.CurrentSkinInfo.Value.ID) + SkinManager.CurrentSkinInfo.Value = SkinInfo.Default; + }); } }); From a53b5ef8b95c2d0f7d3c776025e38f7e845cb4ce Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 16 Nov 2020 19:22:08 +0900 Subject: [PATCH 10/35] Remove `--no-restore` from VSCode build tasks --- .vscode/tasks.json | 17 ----------------- .../.vscode/tasks.json | 11 ----------- .../.vscode/tasks.json | 11 ----------- osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json | 11 ----------- .../.vscode/tasks.json | 11 ----------- osu.Game.Tournament.Tests/.vscode/tasks.json | 11 ----------- 6 files changed, 72 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index aa77d4f055..a70e5ac3a9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Desktop", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Desktop", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -40,7 +38,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tests", "-p:GenerateFullPaths=true", "-m", @@ -55,7 +52,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tests", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -71,7 +67,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests", "-p:GenerateFullPaths=true", "-m", @@ -86,7 +81,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -102,7 +96,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Benchmarks", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -111,16 +104,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore (netcoreapp3.1)", - "type": "shell", - "command": "dotnet", - "args": [ - "restore", - "build/Desktop.proj" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json index 2c915a31b7..d8feacc8a7 100644 --- a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json index ca03924c70..323110b605 100644 --- a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json index 14ffbfb4ae..590bedb8b2 100644 --- a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json index 09340f6f9f..63f25c2402 100644 --- a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Tournament.Tests/.vscode/tasks.json b/osu.Game.Tournament.Tests/.vscode/tasks.json index c69ac0391a..04ec7275ac 100644 --- a/osu.Game.Tournament.Tests/.vscode/tasks.json +++ b/osu.Game.Tournament.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file From 16d25c502204c74eca2e6b21b18ec3ca14e9004b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 16 Nov 2020 19:25:36 +0900 Subject: [PATCH 11/35] Adjast readme for the removed VSCode restore task --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 86c42dae12..c9443ba063 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,6 @@ git pull Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing). - Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations. -- Visual Studio Code users must run the `Restore` task before any build attempt. You can also build and run *osu!* from the command-line with a single command: From 1b1f4c9c09cf8669b957b07a4888cf47dcd67199 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 20:35:22 +0900 Subject: [PATCH 12/35] Refactor user request to fix threadsafety issues --- osu.Game/Database/UserLookupCache.cs | 123 ++++++++++----------------- 1 file changed, 47 insertions(+), 76 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index c85ad6d651..05ba9c882b 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -15,103 +14,75 @@ namespace osu.Game.Database { public class UserLookupCache : MemoryCachingComponent { - private readonly HashSet nextTaskIDs = new HashSet(); - [Resolved] private IAPIProvider api { get; set; } - private readonly object taskAssignmentLock = new object(); - - private Task> pendingRequest; - - /// - /// Whether has already grabbed its IDs. - /// - private bool pendingRequestConsumedIDs; - public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token); protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) - { - var users = await getQueryTaskForUser(lookup); - return users.FirstOrDefault(u => u.Id == lookup); - } + => await queryUser(lookup); - /// - /// Return the task responsible for fetching the provided user. - /// This may be part of a larger batch lookup to reduce web requests. - /// - /// The user to lookup. - /// The task responsible for the lookup. - private Task> getQueryTaskForUser(int userId) + private readonly List<(int id, TaskCompletionSource)> pendingUserTasks = new List<(int, TaskCompletionSource)>(); + private Task pendingRequestTask; + private readonly object taskAssignmentLock = new object(); + + private Task queryUser(int userId) { lock (taskAssignmentLock) { - nextTaskIDs.Add(userId); + var tcs = new TaskCompletionSource(); - // if there's a pending request which hasn't been started yet (and is not yet full), we can wait on it. - if (pendingRequest != null && !pendingRequestConsumedIDs && nextTaskIDs.Count < 50) - return pendingRequest; + // Add to the queue. + pendingUserTasks.Add((userId, tcs)); - return queueNextTask(nextLookup); - } + // Create a request task if there's not already one. + if (pendingRequestTask == null) + createNewTask(); - List nextLookup() - { - int[] lookupItems; - - lock (taskAssignmentLock) - { - pendingRequestConsumedIDs = true; - lookupItems = nextTaskIDs.ToArray(); - nextTaskIDs.Clear(); - - if (lookupItems.Length == 0) - { - queueNextTask(null); - return new List(); - } - } - - var request = new GetUsersRequest(lookupItems); - - // rather than queueing, we maintain our own single-threaded request stream. - api.Perform(request); - - return request.Result?.Users; + return tcs.Task; } } - /// - /// Queues new work at the end of the current work tasks. - /// Ensures the provided work is eventually run. - /// - /// The work to run. Can be null to signify the end of available work. - /// The task tracking this work. - private Task> queueNextTask(Func> work) + private void performLookup() { + var userTasks = new List<(int id, TaskCompletionSource task)>(); + + // Grab at most 50 users from the queue. lock (taskAssignmentLock) { - if (work == null) + while (pendingUserTasks.Count > 0 && userTasks.Count < 50) { - pendingRequest = null; - pendingRequestConsumedIDs = false; - } - else if (pendingRequest == null) - { - // special case for the first request ever. - pendingRequest = Task.Run(work); - pendingRequestConsumedIDs = false; - } - else - { - // append the new request on to the last to be executed. - pendingRequest = pendingRequest.ContinueWith(_ => work()); - pendingRequestConsumedIDs = false; - } + (int id, TaskCompletionSource task) next = pendingUserTasks[^1]; - return pendingRequest; + pendingUserTasks.RemoveAt(pendingUserTasks.Count - 1); + + // Perform a secondary check for existence, in case the user was queried in a previous batch. + if (CheckExists(next.id, out var existing)) + next.task.SetResult(existing); + else + userTasks.Add(next); + } } + + // Query the users. + var request = new GetUsersRequest(userTasks.Select(t => t.id).ToArray()); + + // rather than queueing, we maintain our own single-threaded request stream. + api.Perform(request); + + // Create a new request task if there's still more users to query. + lock (taskAssignmentLock) + { + pendingRequestTask = null; + if (pendingUserTasks.Count > 0) + createNewTask(); + } + + // Notify of completion. + foreach (var (id, task) in userTasks) + task.SetResult(request.Result?.Users?.FirstOrDefault(u => u.Id == id)); } + + private void createNewTask() => pendingRequestTask = Task.Run(performLookup); } } From 87bf168718bd918fb90cb04306216ceae8d0a688 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 20:52:51 +0900 Subject: [PATCH 13/35] Use queue instead of list --- osu.Game/Database/UserLookupCache.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 05ba9c882b..71413d076e 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -22,7 +22,7 @@ namespace osu.Game.Database protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) => await queryUser(lookup); - private readonly List<(int id, TaskCompletionSource)> pendingUserTasks = new List<(int, TaskCompletionSource)>(); + private readonly Queue<(int id, TaskCompletionSource)> pendingUserTasks = new Queue<(int, TaskCompletionSource)>(); private Task pendingRequestTask; private readonly object taskAssignmentLock = new object(); @@ -33,7 +33,7 @@ namespace osu.Game.Database var tcs = new TaskCompletionSource(); // Add to the queue. - pendingUserTasks.Add((userId, tcs)); + pendingUserTasks.Enqueue((userId, tcs)); // Create a request task if there's not already one. if (pendingRequestTask == null) @@ -52,9 +52,7 @@ namespace osu.Game.Database { while (pendingUserTasks.Count > 0 && userTasks.Count < 50) { - (int id, TaskCompletionSource task) next = pendingUserTasks[^1]; - - pendingUserTasks.RemoveAt(pendingUserTasks.Count - 1); + (int id, TaskCompletionSource task) next = pendingUserTasks.Dequeue(); // Perform a secondary check for existence, in case the user was queried in a previous batch. if (CheckExists(next.id, out var existing)) From 85b0f714670215fc85eec8215f393674ccfd062a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 21:17:43 +0900 Subject: [PATCH 14/35] Handle duplicate user IDs within the same batch --- osu.Game/Database/UserLookupCache.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 71413d076e..ba7f2ad98e 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -45,12 +45,15 @@ namespace osu.Game.Database private void performLookup() { + // userTasks may exceed 50 elements, indicating the existence of duplicate user IDs. All duplicated user IDs must be fulfilled. + // userIds contains at most 50 unique user IDs from userTasks, which is used to perform the lookup. var userTasks = new List<(int id, TaskCompletionSource task)>(); + var userIds = new HashSet(); - // Grab at most 50 users from the queue. + // Grab at most 50 unique user IDs from the queue. lock (taskAssignmentLock) { - while (pendingUserTasks.Count > 0 && userTasks.Count < 50) + while (pendingUserTasks.Count > 0 && userIds.Count < 50) { (int id, TaskCompletionSource task) next = pendingUserTasks.Dequeue(); @@ -58,12 +61,15 @@ namespace osu.Game.Database if (CheckExists(next.id, out var existing)) next.task.SetResult(existing); else + { userTasks.Add(next); + userIds.Add(next.id); + } } } // Query the users. - var request = new GetUsersRequest(userTasks.Select(t => t.id).ToArray()); + var request = new GetUsersRequest(userIds.ToArray()); // rather than queueing, we maintain our own single-threaded request stream. api.Perform(request); From cead67d51209a1f3a1869448d15345b9d78577f5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 21:47:33 +0900 Subject: [PATCH 15/35] Add back removed InitialLifetimeOffset removal --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 3b7c8bcc2a..ca49ed9e75 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -201,6 +201,8 @@ namespace osu.Game.Rulesets.Objects.Drawables // Copy any existing result from the entry (required for rewind / judgement revert). Result = lifetimeEntry.Result; } + else + LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; // Ensure this DHO has a result. Result ??= CreateResult(HitObject.CreateJudgement()) @@ -646,6 +648,10 @@ namespace osu.Game.Rulesets.Objects.Drawables /// This is only used as an optimisation to delay the initial update of this and may be tuned more aggressively if required. /// It is indirectly used to decide the automatic transform offset provided to . /// A more accurate should be set for further optimisation (in , for example). + /// + /// Only has an effect if this is not being pooled. + /// For pooled s, use instead. + /// /// protected virtual double InitialLifetimeOffset => 10000; From 4cf6aca8735c43c36d1a9ed90660b0600a591942 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 22:40:25 +0900 Subject: [PATCH 16/35] Fix slider ball tint not working --- .../TestSceneSliderApplication.cs | 45 +++++++++++++++++++ .../Objects/Drawables/DrawableSlider.cs | 11 ++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index fb1ebbb0d0..084af7dafe 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -1,20 +1,30 @@ // 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.Graphics; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Skinning; using osu.Game.Tests.Visual; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSliderApplication : OsuTestScene { + [Resolved] + private SkinManager skinManager { get; set; } + [Test] public void TestApplyNewSlider() { @@ -50,6 +60,41 @@ namespace osu.Game.Rulesets.Osu.Tests }), null)); } + [Test] + public void TestBallTintChangedOnAccentChange() + { + DrawableSlider dho = null; + + AddStep("create slider", () => + { + var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info); + tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1"; + + Child = new SkinProvidingContainer(tintingSkin) + { + RelativeSizeAxes = Axes.Both, + Child = dho = new DrawableSlider(prepareObject(new Slider + { + Position = new Vector2(256, 192), + IndexInCurrentCombo = 0, + StartTime = Time.Current, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(150, 100), + new Vector2(300, 0), + }) + })) + }; + }); + + AddStep("set accent white", () => dho.AccentColour.Value = Color4.White); + AddAssert("ball is white", () => dho.ChildrenOfType().Single().AccentColour == Color4.White); + + AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red); + AddAssert("ball is red", () => dho.ChildrenOfType().Single().AccentColour == Color4.Red); + } + private Slider prepareObject(Slider slider) { slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 04fc755da5..f7b1894058 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -80,6 +80,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { foreach (var drawableHitObject in NestedHitObjects) drawableHitObject.AccentColour.Value = colour.NewValue; + updateBallTint(); }, true); Tracking.BindValueChanged(updateSlidingSample); @@ -244,7 +245,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.ApplySkin(skin, allowFallback); - bool allowBallTint = skin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; + updateBallTint(); + } + + private void updateBallTint() + { + if (CurrentSkin == null) + return; + + bool allowBallTint = CurrentSkin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White; } From e8dbc190f1ce8584980eb25486c04a786d6e218f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 23:30:24 +0900 Subject: [PATCH 17/35] Remove ability to pool DHOs in parent playfields --- osu.Game/Rulesets/UI/Playfield.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 80e33e0ec5..82ec653f31 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -218,9 +218,6 @@ namespace osu.Game.Rulesets.UI #region Pooling support - [Resolved(CanBeNull = true)] - private IPooledHitObjectProvider parentPooledObjectProvider { get; set; } - private readonly Dictionary pools = new Dictionary(); /// @@ -320,10 +317,7 @@ namespace osu.Game.Rulesets.UI } } - if (pool == null) - return parentPooledObjectProvider?.GetPooledDrawableRepresentation(hitObject); - - return (DrawableHitObject)pool.Get(d => + return (DrawableHitObject)pool?.Get(d => { var dho = (DrawableHitObject)d; From f5e12b9d7c0828efdd527706216fbee7efa02375 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 23:53:54 +0900 Subject: [PATCH 18/35] Adjust TestScenePlayerLoader for safety --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 127 ++++++++---------- 1 file changed, 53 insertions(+), 74 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 9b31dd045a..88fbf09ef4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -23,17 +23,15 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestScenePlayerLoader : OsuManualInputManagerTestScene + public class TestScenePlayerLoader : ScreenTestScene { private TestPlayerLoader loader; - private TestPlayerLoaderContainer container; private TestPlayer player; private bool epilepsyWarning; @@ -44,21 +42,46 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private SessionStatics sessionStatics { get; set; } + [Cached] + private readonly NotificationOverlay notificationOverlay; + + [Cached] + private readonly VolumeOverlay volumeOverlay; + + private readonly ChangelogOverlay changelogOverlay; + + public TestScenePlayerLoader() + { + AddRange(new Drawable[] + { + notificationOverlay = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + volumeOverlay = new VolumeOverlay + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, + changelogOverlay = new ChangelogOverlay() + }); + } + + [SetUp] + public void Setup() => Schedule(() => + { + player = null; + audioManager.Volume.SetDefault(); + }); + /// /// Sets the input manager child to a new test player loader container instance. /// /// If the test player should behave like the production one. /// An action to run before player load but after bindable leases are returned. - public void ResetPlayer(bool interactive, Action beforeLoadAction = null) + private void resetPlayer(bool interactive, Action beforeLoadAction = null) { - player = null; - - audioManager.Volume.SetDefault(); - - InputManager.Clear(); - - container = new TestPlayerLoaderContainer(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive))); - beforeLoadAction?.Invoke(); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); @@ -67,13 +90,13 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToTrack(Beatmap.Value.Track); - InputManager.Child = container; + LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive))); } [Test] public void TestEarlyExitBeforePlayerConstruction() { - AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); + AddStep("load dummy beatmap", () => resetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddStep("exit loader", () => loader.Exit()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); @@ -90,7 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestEarlyExitAfterPlayerConstruction() { - AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); + AddStep("load dummy beatmap", () => resetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); AddUntilStep("wait for non-null player", () => player != null); @@ -104,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBlockLoadViaMouseMovement() { - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for load ready", () => @@ -129,20 +152,18 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBlockLoadViaFocus() { - OsuFocusedOverlayContainer overlay = null; - - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddStep("show focused overlay", () => { container.Add(overlay = new ChangelogOverlay { State = { Value = Visibility.Visible } }); }); - AddUntilStep("overlay visible", () => overlay.IsPresent); + AddStep("show focused overlay", () => changelogOverlay.Show()); + AddUntilStep("overlay visible", () => changelogOverlay.IsPresent); - AddUntilStep("wait for load ready", () => player.LoadState == LoadState.Ready); + AddUntilStep("wait for load ready", () => player?.LoadState == LoadState.Ready); AddRepeatStep("twiddle thumbs", () => { }, 20); AddAssert("loader still active", () => loader.IsCurrentScreen()); - AddStep("hide overlay", () => overlay.Hide()); + AddStep("hide overlay", () => changelogOverlay.Hide()); AddUntilStep("loads after idle", () => !loader.IsCurrentScreen()); } @@ -151,15 +172,9 @@ namespace osu.Game.Tests.Visual.Gameplay { SlowLoadPlayer slowPlayer = null; - AddStep("load dummy beatmap", () => ResetPlayer(false)); - AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for player to be current", () => player.IsCurrentScreen()); AddStep("load slow dummy beatmap", () => { - InputManager.Child = container = new TestPlayerLoaderContainer( - loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false))); - + LoadScreen(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false))); Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000); }); @@ -173,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay TestMod playerMod1 = null; TestMod playerMod2 = null; - AddStep("load player", () => { ResetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); }); + AddStep("load player", () => { resetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); }); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); @@ -201,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var testMod = new TestMod(); - AddStep("load player", () => ResetPlayer(true)); + AddStep("load player", () => resetPlayer(true)); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddStep("set test mod in loader", () => loader.Mods.Value = new[] { testMod }); @@ -223,7 +238,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestMutedNotificationMuteButton() { - addVolumeSteps("mute button", () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value); + addVolumeSteps("mute button", () => volumeOverlay.IsMuted.Value = true, () => !volumeOverlay.IsMuted.Value); } /// @@ -236,13 +251,13 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).Value = false); - AddStep("load player", () => ResetPlayer(false, beforeLoad)); + AddStep("load player", () => resetPlayer(false, beforeLoad)); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); - AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1); + AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1); AddStep("click notification", () => { - var scrollContainer = (OsuScrollContainer)container.NotificationOverlay.Children.Last(); + var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last(); var flowContainer = scrollContainer.Children.OfType>().First(); var notification = flowContainer.First(); @@ -260,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEpilepsyWarning(bool warning) { AddStep("change epilepsy warning", () => epilepsyWarning = warning); - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); @@ -277,7 +292,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEpilepsyWarningEarlyExit() { AddStep("set epilepsy warning", () => epilepsyWarning = true); - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); @@ -287,42 +302,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } - private class TestPlayerLoaderContainer : Container - { - [Cached] - public readonly NotificationOverlay NotificationOverlay; - - [Cached] - public readonly VolumeOverlay VolumeOverlay; - - public TestPlayerLoaderContainer(IScreen screen) - { - RelativeSizeAxes = Axes.Both; - - OsuScreenStack stack; - - InternalChildren = new Drawable[] - { - stack = new OsuScreenStack - { - RelativeSizeAxes = Axes.Both, - }, - NotificationOverlay = new NotificationOverlay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - }, - VolumeOverlay = new VolumeOverlay - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - } - }; - - stack.Push(screen); - } - } - private class TestPlayerLoader : PlayerLoader { public new VisualSettings VisualSettings => base.VisualSettings; From e88920442c7f3a2ce09b17d346737018c54bbb8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Nov 2020 20:01:10 +0100 Subject: [PATCH 19/35] Use HitStateUpdateTime instead --- osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index f1f72c8618..328c4e547b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.Skinning private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) { if (state == ArmedState.Miss) - missFadeTime.Value ??= hitObject.StateUpdateTime; + missFadeTime.Value ??= hitObject.HitStateUpdateTime; } private void onIsHittingChanged(ValueChangedEvent isHitting) From 21f29e28e2a11f33b702c3dfbe92d5da32a92b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Nov 2020 20:36:56 +0100 Subject: [PATCH 20/35] Add clarification comment --- osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 328c4e547b..8902d82f33 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -119,6 +119,7 @@ namespace osu.Game.Rulesets.Mania.Skinning private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) { + // ensure that the hold note is also faded out when the head/tail/any tick is missed. if (state == ArmedState.Miss) missFadeTime.Value ??= hitObject.HitStateUpdateTime; } From c6618f08aad3b47cc69ef73153966a5f384b580c Mon Sep 17 00:00:00 2001 From: kamp Date: Mon, 16 Nov 2020 21:26:08 +0100 Subject: [PATCH 21/35] Fix slider control point connections not being updated --- .../PathControlPointConnectionPiece.cs | 12 +++++++++- .../Components/PathControlPointVisualiser.cs | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index ba1d35c35c..45c4a61ce1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -20,7 +20,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private readonly Path path; private readonly Slider slider; - private readonly int controlPointIndex; + private int controlPointIndex; + + public int ControlPointIndex + { + get => controlPointIndex; + set + { + controlPointIndex = value; + updateConnectingPath(); + } + } private IBindable sliderPosition; private IBindable pathVersion; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 17541866ec..14ce0e065b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -66,6 +66,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components switch (e.Action) { case NotifyCollectionChangedAction.Add: + // If inserting in the the path (not appending), + // update indices of existing connections after insert location + if (e.NewStartingIndex < Pieces.Count) + { + foreach (var connection in Connections) + { + if (connection.ControlPointIndex >= e.NewStartingIndex) + connection.ControlPointIndex += e.NewItems.Count; + } + } + for (int i = 0; i < e.NewItems.Count; i++) { var point = (PathControlPoint)e.NewItems[i]; @@ -82,12 +93,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components break; case NotifyCollectionChangedAction.Remove: + int oldSize = Pieces.Count; + foreach (var point in e.OldItems.Cast()) { Pieces.RemoveAll(p => p.ControlPoint == point); Connections.RemoveAll(c => c.ControlPoint == point); } + // If removing before the end of the path, + // update indices of connections after remove location + if (e.OldStartingIndex + e.OldItems.Count < oldSize) + { + foreach (var connection in Connections) + { + if (connection.ControlPointIndex >= e.OldStartingIndex) + connection.ControlPointIndex -= e.OldItems.Count; + } + } + break; } } From 009d666241fb7adfd1c1f11d7603478dbd9166b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Nov 2020 10:57:11 +0900 Subject: [PATCH 22/35] Use dictionary to avoid linq overhead --- osu.Game/Database/UserLookupCache.cs | 36 +++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index ba7f2ad98e..05d6930992 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -45,15 +45,13 @@ namespace osu.Game.Database private void performLookup() { - // userTasks may exceed 50 elements, indicating the existence of duplicate user IDs. All duplicated user IDs must be fulfilled. - // userIds contains at most 50 unique user IDs from userTasks, which is used to perform the lookup. - var userTasks = new List<(int id, TaskCompletionSource task)>(); - var userIds = new HashSet(); + // contains at most 50 unique user IDs from userTasks, which is used to perform the lookup. + var userTasks = new Dictionary>>(); // Grab at most 50 unique user IDs from the queue. lock (taskAssignmentLock) { - while (pendingUserTasks.Count > 0 && userIds.Count < 50) + while (pendingUserTasks.Count > 0 && userTasks.Count < 50) { (int id, TaskCompletionSource task) next = pendingUserTasks.Dequeue(); @@ -62,14 +60,16 @@ namespace osu.Game.Database next.task.SetResult(existing); else { - userTasks.Add(next); - userIds.Add(next.id); + if (userTasks.TryGetValue(next.id, out var tasks)) + tasks.Add(next.task); + else + userTasks[next.id] = new List> { next.task }; } } } // Query the users. - var request = new GetUsersRequest(userIds.ToArray()); + var request = new GetUsersRequest(userTasks.Keys.ToArray()); // rather than queueing, we maintain our own single-threaded request stream. api.Perform(request); @@ -82,9 +82,23 @@ namespace osu.Game.Database createNewTask(); } - // Notify of completion. - foreach (var (id, task) in userTasks) - task.SetResult(request.Result?.Users?.FirstOrDefault(u => u.Id == id)); + foreach (var user in request.Result.Users) + { + if (userTasks.TryGetValue(user.Id, out var tasks)) + { + foreach (var task in tasks) + task.SetResult(user); + + userTasks.Remove(user.Id); + } + } + + // if any tasks remain which were not satisfied, return null. + foreach (var tasks in userTasks.Values) + { + foreach (var task in tasks) + task.SetResult(null); + } } private void createNewTask() => pendingRequestTask = Task.Run(performLookup); From 77942af3a653aa3f8672d44f5370e56236c03df6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 13:37:58 +0900 Subject: [PATCH 23/35] Fix hold note judgements displaying incorrectly --- .../Objects/Drawables/DrawableHoldNoteTick.cs | 2 -- osu.Game.Rulesets.Mania/UI/Column.cs | 2 +- osu.Game.Rulesets.Mania/UI/Stage.cs | 4 ++++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index f265419aa0..98931dceed 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableHoldNoteTick : DrawableManiaHitObject { - public override bool DisplayResult => false; - /// /// References the time at which the user started holding the hold note. /// diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index c28a1c13d8..9aabcc6699 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI if (result.IsHit) hitPolicy.HandleHit(judgedObject); - if (!result.IsHit || !DisplayJudgements.Value) + if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result))); diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index e7a2de266d..3d7960ffe3 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -167,6 +167,10 @@ namespace osu.Game.Rulesets.Mania.UI if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; + // Tick judgements should not display text. + if (judgedObject is DrawableHoldNoteTick) + return; + judgements.Clear(false); judgements.Add(judgementPool.Get(j => { From eebce1f9145813b525400517e170d3931a1f8de9 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 17 Nov 2020 18:13:32 +0900 Subject: [PATCH 24/35] Fix TestSceneFruitObjects --- .../TestSceneFruitObjects.cs | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 385d8ed7fa..e9dabd30ed 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.Catch.Tests @@ -36,17 +37,29 @@ namespace osu.Game.Rulesets.Catch.Tests Scale = 1.5f, }; - return new DrawableTinyDroplet(droplet) + return new TestDrawableTinyDroplet(droplet) { Anchor = Anchor.Centre, RelativePositionAxes = Axes.None, Position = Vector2.Zero, - Alpha = 1, - LifetimeStart = double.NegativeInfinity, - LifetimeEnd = double.PositiveInfinity, }; } + private class TestDrawableTinyDroplet : DrawableTinyDroplet + { + public TestDrawableTinyDroplet(TinyDroplet tinyDroplet) + : base(tinyDroplet) + { + } + + protected override void OnApply(HitObject hitObject) + { + base.OnApply(hitObject); + LifetimeStart = double.NegativeInfinity; + LifetimeEnd = double.PositiveInfinity; + } + } + private Drawable createDrawableDroplet(bool hyperdash = false) { var droplet = new TestCatchDroplet @@ -55,17 +68,29 @@ namespace osu.Game.Rulesets.Catch.Tests HyperDashTarget = hyperdash ? new Banana() : null }; - return new DrawableDroplet(droplet) + return new TestDrawableDroplet(droplet) { Anchor = Anchor.Centre, RelativePositionAxes = Axes.None, Position = Vector2.Zero, - Alpha = 1, - LifetimeStart = double.NegativeInfinity, - LifetimeEnd = double.PositiveInfinity, }; } + private class TestDrawableDroplet : DrawableDroplet + { + public TestDrawableDroplet(Droplet droplet) + : base(droplet) + { + } + + protected override void OnApply(HitObject hitObject) + { + base.OnApply(hitObject); + LifetimeStart = double.NegativeInfinity; + LifetimeEnd = double.PositiveInfinity; + } + } + private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false) { Fruit fruit = new TestCatchFruit(rep) @@ -74,17 +99,29 @@ namespace osu.Game.Rulesets.Catch.Tests HyperDashTarget = hyperdash ? new Banana() : null }; - return new DrawableFruit(fruit) + return new TestDrawableFruit(fruit) { Anchor = Anchor.Centre, RelativePositionAxes = Axes.None, Position = Vector2.Zero, - Alpha = 1, - LifetimeStart = double.NegativeInfinity, - LifetimeEnd = double.PositiveInfinity, }; } + private class TestDrawableFruit : DrawableFruit + { + public TestDrawableFruit(Fruit fruit) + : base(fruit) + { + } + + protected override void OnApply(HitObject hitObject) + { + base.OnApply(hitObject); + LifetimeStart = double.NegativeInfinity; + LifetimeEnd = double.PositiveInfinity; + } + } + public class TestCatchFruit : Fruit { public TestCatchFruit(FruitVisualRepresentation rep) From 58c8184ad7f7b008d001a048cfcbf998f14e1423 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 22:56:21 +0900 Subject: [PATCH 25/35] Define blueprint order similarly to hitobjects --- .../Compose/Components/BlueprintContainer.cs | 6 +- .../Components/SelectionBlueprintContainer.cs | 76 +++++++++++++++++++ .../Timeline/TimelineBlueprintContainer.cs | 6 +- 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 53b6e14940..3aaa0c7d89 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -118,8 +118,8 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - protected virtual Container CreateSelectionBlueprintContainer() => - new Container { RelativeSizeAxes = Axes.Both }; + protected virtual SelectionBlueprintContainer CreateSelectionBlueprintContainer() => + new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; /// /// Creates a which outlines s and handles movement of selections. @@ -338,7 +338,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. private bool beginClickSelection(MouseButtonEvent e) { - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse()) { if (!blueprint.IsHovered) continue; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs new file mode 100644 index 0000000000..54932f6252 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs @@ -0,0 +1,76 @@ +// 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.Rulesets.Edit; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public class SelectionBlueprintContainer : Container + { + public override void Add(SelectionBlueprint drawable) + { + base.Add(drawable); + + if (Content == this) + bindStartTime(drawable); + } + + public override bool Remove(SelectionBlueprint drawable) + { + if (!base.Remove(drawable)) + return false; + + if (Content == this) + unbindStartTime(drawable); + return true; + } + + public override void Clear(bool disposeChildren) + { + base.Clear(disposeChildren); + unbindAllStartTimes(); + } + + private readonly Dictionary startTimeMap = new Dictionary(); + + private void bindStartTime(SelectionBlueprint blueprint) + { + var bindable = blueprint.HitObject.StartTimeBindable.GetBoundCopy(); + + bindable.BindValueChanged(_ => + { + if (LoadState >= LoadState.Ready) + SortInternal(); + }); + + startTimeMap[blueprint] = bindable; + } + + private void unbindStartTime(SelectionBlueprint blueprint) + { + startTimeMap[blueprint].UnbindAll(); + startTimeMap.Remove(blueprint); + } + + private void unbindAllStartTimes() + { + foreach (var kvp in startTimeMap) + kvp.Value.UnbindAll(); + startTimeMap.Clear(); + } + + protected override int Compare(Drawable x, Drawable y) + { + if (!(x is SelectionBlueprint xObj) || !(y is SelectionBlueprint yObj)) + return base.Compare(x, y); + + // Put earlier blueprints towards the end of the list, so they handle input first + int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); + return i == 0 ? CompareReverseChildID(x, y) : i; + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index eef02e61a6..2bd4ac2f91 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - protected override Container CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; protected override void OnDrag(DragEvent e) { @@ -195,13 +195,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - protected class TimelineSelectionBlueprintContainer : Container + protected class TimelineSelectionBlueprintContainer : SelectionBlueprintContainer { protected override Container Content { get; } public TimelineSelectionBlueprintContainer() { - AddInternal(new TimelinePart(Content = new Container { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); + AddInternal(new TimelinePart(Content = new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); } } } From 04805b78c31718a451737bd78b090e96e39a3859 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 23:19:59 +0900 Subject: [PATCH 26/35] Tighten osu! ruleset lifetime expiry for past hitobjects --- .../Objects/Drawables/DrawableHitCircle.cs | 2 ++ .../Objects/Drawables/DrawableOsuHitObject.cs | 5 +++++ .../Objects/Drawables/DrawableSlider.cs | 16 ++++++++-------- .../Objects/Drawables/DrawableSpinner.cs | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 2e63160d36..d1ceca6d8f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -180,6 +180,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.Delay(800).FadeOut(); break; } + + Expire(); } public Drawable ProxiedLayer => ApproachCircle; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index bcaf73d34f..c962d191a6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -91,6 +92,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Manually set to reduce the number of future alive objects to a bare minimum. LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; + + // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts. + // An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry. + LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; } /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index f7b1894058..14c494d909 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -193,13 +193,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return base.CreateNestedHitObject(hitObject); } - protected override void UpdateInitialTransforms() - { - base.UpdateInitialTransforms(); - - Body.FadeInFromZero(HitObject.TimeFadeIn); - } - public readonly Bindable Tracking = new Bindable(); protected override void Update() @@ -273,6 +266,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.PlaySamples(); } + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); + + Body.FadeInFromZero(HitObject.TimeFadeIn); + } + protected override void UpdateStartTimeStateTransforms() { base.UpdateStartTimeStateTransforms(); @@ -297,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables break; } - this.FadeOut(fade_out_time, Easing.OutQuint); + this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); } public Drawable ProxiedLayer => HeadCircle.ProxiedLayer; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 87c7146a64..2a14a7c975 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateHitStateTransforms(state); - this.FadeOut(160); + this.FadeOut(160).Expire(); // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback. isSpinning?.TriggerChange(); From ce4baf328dba51f3f5006bbc69f0ad58b9d91dd6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 23:35:36 +0900 Subject: [PATCH 27/35] Move into OnApply() to resolve one-frame issues --- .../Objects/Drawables/DrawableOsuHitObject.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index c962d191a6..a26db06ede 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -61,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables PositionBindable.BindTo(HitObject.PositionBindable); StackHeightBindable.BindTo(HitObject.StackHeightBindable); ScaleBindable.BindTo(HitObject.ScaleBindable); + + // Manually set to reduce the number of future alive objects to a bare minimum. + LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; + + // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts. + // An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry. + LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; } protected override void OnFree(HitObject hitObject) @@ -86,18 +93,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength); - protected override void UpdateInitialTransforms() - { - base.UpdateInitialTransforms(); - - // Manually set to reduce the number of future alive objects to a bare minimum. - LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; - - // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts. - // An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry. - LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; - } - /// /// Causes this to get missed, disregarding all conditions in implementations of . /// From c360533e4cce0875dbb5175ef6a27bac869759f1 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 17 Nov 2020 23:40:30 +0900 Subject: [PATCH 28/35] Simplify code of TestSceneFruitObjects --- .../TestSceneFruitObjects.cs | 131 ++++-------------- 1 file changed, 24 insertions(+), 107 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index e9dabd30ed..89063319d6 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.Catch.Tests @@ -19,107 +18,42 @@ namespace osu.Game.Rulesets.Catch.Tests base.LoadComplete(); foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) - AddStep($"show {rep}", () => SetContents(() => createDrawable(rep))); + AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep))); AddStep("show droplet", () => SetContents(() => createDrawableDroplet())); AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) - AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true))); + AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true))); AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true))); } - private Drawable createDrawableTinyDroplet() + private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) => + setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash); + + private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash); + + private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet())); + + private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false) { - var droplet = new TestCatchTinyDroplet + var hitObject = d.HitObject; + hitObject.StartTime = 1000000000000; + hitObject.Scale = 1.5f; + + if (hyperdash) + hitObject.HyperDashTarget = new Banana(); + + d.Anchor = Anchor.Centre; + d.RelativePositionAxes = Axes.None; + d.Position = Vector2.Zero; + d.HitObjectApplied += _ => { - Scale = 1.5f, + d.LifetimeStart = double.NegativeInfinity; + d.LifetimeEnd = double.PositiveInfinity; }; - - return new TestDrawableTinyDroplet(droplet) - { - Anchor = Anchor.Centre, - RelativePositionAxes = Axes.None, - Position = Vector2.Zero, - }; - } - - private class TestDrawableTinyDroplet : DrawableTinyDroplet - { - public TestDrawableTinyDroplet(TinyDroplet tinyDroplet) - : base(tinyDroplet) - { - } - - protected override void OnApply(HitObject hitObject) - { - base.OnApply(hitObject); - LifetimeStart = double.NegativeInfinity; - LifetimeEnd = double.PositiveInfinity; - } - } - - private Drawable createDrawableDroplet(bool hyperdash = false) - { - var droplet = new TestCatchDroplet - { - Scale = 1.5f, - HyperDashTarget = hyperdash ? new Banana() : null - }; - - return new TestDrawableDroplet(droplet) - { - Anchor = Anchor.Centre, - RelativePositionAxes = Axes.None, - Position = Vector2.Zero, - }; - } - - private class TestDrawableDroplet : DrawableDroplet - { - public TestDrawableDroplet(Droplet droplet) - : base(droplet) - { - } - - protected override void OnApply(HitObject hitObject) - { - base.OnApply(hitObject); - LifetimeStart = double.NegativeInfinity; - LifetimeEnd = double.PositiveInfinity; - } - } - - private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false) - { - Fruit fruit = new TestCatchFruit(rep) - { - Scale = 1.5f, - HyperDashTarget = hyperdash ? new Banana() : null - }; - - return new TestDrawableFruit(fruit) - { - Anchor = Anchor.Centre, - RelativePositionAxes = Axes.None, - Position = Vector2.Zero, - }; - } - - private class TestDrawableFruit : DrawableFruit - { - public TestDrawableFruit(Fruit fruit) - : base(fruit) - { - } - - protected override void OnApply(HitObject hitObject) - { - base.OnApply(hitObject); - LifetimeStart = double.NegativeInfinity; - LifetimeEnd = double.PositiveInfinity; - } + return d; } public class TestCatchFruit : Fruit @@ -127,26 +61,9 @@ namespace osu.Game.Rulesets.Catch.Tests public TestCatchFruit(FruitVisualRepresentation rep) { VisualRepresentation = rep; - StartTime = 1000000000000; } public override FruitVisualRepresentation VisualRepresentation { get; } } - - public class TestCatchDroplet : Droplet - { - public TestCatchDroplet() - { - StartTime = 1000000000000; - } - } - - public class TestCatchTinyDroplet : TinyDroplet - { - public TestCatchTinyDroplet() - { - StartTime = 1000000000000; - } - } } } From c8fb49d540d4876fb3b7800c4159da6f5239fe24 Mon Sep 17 00:00:00 2001 From: kamp Date: Tue, 17 Nov 2020 22:23:46 +0100 Subject: [PATCH 29/35] Apply suggestions and remove redundant updateConnectingPath call --- .../PathControlPointConnectionPiece.cs | 16 +++------------- .../Components/PathControlPointVisualiser.cs | 6 ++---- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 45c4a61ce1..eb7011e8b0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -20,17 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private readonly Path path; private readonly Slider slider; - private int controlPointIndex; - - public int ControlPointIndex - { - get => controlPointIndex; - set - { - controlPointIndex = value; - updateConnectingPath(); - } - } + public int ControlPointIndex { get; set; } private IBindable sliderPosition; private IBindable pathVersion; @@ -38,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public PathControlPointConnectionPiece(Slider slider, int controlPointIndex) { this.slider = slider; - this.controlPointIndex = controlPointIndex; + ControlPointIndex = controlPointIndex; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; @@ -74,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components path.ClearVertices(); - int nextIndex = controlPointIndex + 1; + int nextIndex = ControlPointIndex + 1; if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count) return; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 14ce0e065b..e551be4af3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components switch (e.Action) { case NotifyCollectionChangedAction.Add: - // If inserting in the the path (not appending), + // If inserting in the path (not appending), // update indices of existing connections after insert location if (e.NewStartingIndex < Pieces.Count) { @@ -93,8 +93,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components break; case NotifyCollectionChangedAction.Remove: - int oldSize = Pieces.Count; - foreach (var point in e.OldItems.Cast()) { Pieces.RemoveAll(p => p.ControlPoint == point); @@ -103,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // If removing before the end of the path, // update indices of connections after remove location - if (e.OldStartingIndex + e.OldItems.Count < oldSize) + if (e.OldStartingIndex + e.OldItems.Count < Pieces.Count + e.OldItems.Count) { foreach (var connection in Connections) { From 2d66423fbdc37cba19f675897297a8efa2131784 Mon Sep 17 00:00:00 2001 From: kamp Date: Tue, 17 Nov 2020 23:04:38 +0100 Subject: [PATCH 30/35] Simplify inequality --- .../Blueprints/Sliders/Components/PathControlPointVisualiser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index e551be4af3..7375c0e981 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // If removing before the end of the path, // update indices of connections after remove location - if (e.OldStartingIndex + e.OldItems.Count < Pieces.Count + e.OldItems.Count) + if (e.OldStartingIndex < Pieces.Count) { foreach (var connection in Connections) { From 783c172b5de26e53b1ff0982f8f49a9bcd7bbad1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Nov 2020 13:33:22 +0900 Subject: [PATCH 31/35] Make sealed and cleanup comparator --- .../Edit/Compose/Components/BlueprintContainer.cs | 3 +-- ...ner.cs => HitObjectOrderedSelectionContainer.cs} | 13 +++++-------- .../Timeline/TimelineBlueprintContainer.cs | 6 +++--- 3 files changed, 9 insertions(+), 13 deletions(-) rename osu.Game/Screens/Edit/Compose/Components/{SelectionBlueprintContainer.cs => HitObjectOrderedSelectionContainer.cs} (85%) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 3aaa0c7d89..4b98d42c7c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -118,8 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - protected virtual SelectionBlueprintContainer CreateSelectionBlueprintContainer() => - new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + protected virtual Container CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }; /// /// Creates a which outlines s and handles movement of selections. diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs similarity index 85% rename from osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs rename to osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 54932f6252..ae50b0fd61 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -9,14 +9,12 @@ using osu.Game.Rulesets.Edit; namespace osu.Game.Screens.Edit.Compose.Components { - public class SelectionBlueprintContainer : Container + public sealed class HitObjectOrderedSelectionContainer : Container { public override void Add(SelectionBlueprint drawable) { base.Add(drawable); - - if (Content == this) - bindStartTime(drawable); + bindStartTime(drawable); } public override bool Remove(SelectionBlueprint drawable) @@ -24,8 +22,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (!base.Remove(drawable)) return false; - if (Content == this) - unbindStartTime(drawable); + unbindStartTime(drawable); return true; } @@ -65,8 +62,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override int Compare(Drawable x, Drawable y) { - if (!(x is SelectionBlueprint xObj) || !(y is SelectionBlueprint yObj)) - return base.Compare(x, y); + var xObj = (SelectionBlueprint)x; + var yObj = (SelectionBlueprint)y; // Put earlier blueprints towards the end of the list, so they handle input first int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 2bd4ac2f91..2f14c607c2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + protected override Container CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; protected override void OnDrag(DragEvent e) { @@ -195,13 +195,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - protected class TimelineSelectionBlueprintContainer : SelectionBlueprintContainer + protected class TimelineSelectionBlueprintContainer : Container { protected override Container Content { get; } public TimelineSelectionBlueprintContainer() { - AddInternal(new TimelinePart(Content = new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); + AddInternal(new TimelinePart(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); } } } From f00c23b4a07367179d67c7291e1305f1ca01ab3d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Nov 2020 13:37:15 +0900 Subject: [PATCH 32/35] Add comment + xmldoc --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 1 + .../Compose/Components/HitObjectOrderedSelectionContainer.cs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 4b98d42c7c..df9cadebfc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -337,6 +337,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. private bool beginClickSelection(MouseButtonEvent e) { + // Iterate from the top of the input stack (blueprints closest to the front of the screen first). foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse()) { if (!blueprint.IsHovered) continue; diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index ae50b0fd61..9e95fe4fa1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -6,9 +6,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Edit.Compose.Components { + /// + /// A container for ordered by their start times. + /// public sealed class HitObjectOrderedSelectionContainer : Container { public override void Add(SelectionBlueprint drawable) From efc18887c8182d6414ff2b8efe47c1b4f709525e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 15:51:09 +0900 Subject: [PATCH 33/35] Update framework --- osu.Android.props | 2 +- osu.Desktop/OsuGameDesktop.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 6e3d5eec1f..4657896fac 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index b17611f23f..0feab9a717 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -139,7 +139,7 @@ namespace osu.Desktop // SDL2 DesktopWindow case DesktopWindow desktopWindow: - desktopWindow.CursorState.Value |= CursorState.Hidden; + desktopWindow.CursorState |= CursorState.Hidden; desktopWindow.SetIconFromStream(iconStream); desktopWindow.Title = Name; desktopWindow.DragDrop += f => fileDrop(new[] { f }); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1850ee3488..704ac5a611 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 2ac23f1503..346bd892b0 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From bb1aacb360758c2c5e21e172bcb9f1985933a2b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 16:18:27 +0900 Subject: [PATCH 34/35] Fix SkinnableSprite initialising a drawable even when the texture is not available --- osu.Game/Skinning/SkinnableSprite.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 5352928ec6..1340d1474c 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -24,7 +24,15 @@ namespace osu.Game.Skinning { } - protected override Drawable CreateDefault(ISkinComponent component) => new Sprite { Texture = textures.Get(component.LookupName) }; + protected override Drawable CreateDefault(ISkinComponent component) + { + var texture = textures.Get(component.LookupName); + + if (texture == null) + return null; + + return new Sprite { Texture = texture }; + } private class SpriteComponent : ISkinComponent { From cb5d1d0d77f1dbf87a01d113074dcdb1f31e7b57 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Nov 2020 21:26:35 +0900 Subject: [PATCH 35/35] Remove obsolete method --- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index d8c6da86f9..ba38c7f77d 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -10,14 +10,6 @@ namespace osu.Game.Rulesets.Objects { public static class SliderEventGenerator { - [Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115 - // ReSharper disable once RedundantOverload.Global - public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, - double? legacyLastTickOffset) - { - return Generate(startTime, spanDuration, velocity, tickDistance, totalDistance, spanCount, legacyLastTickOffset, default); - } - // ReSharper disable once MethodOverloadWithOptionalParameter public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, double? legacyLastTickOffset, CancellationToken cancellationToken = default)