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/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: 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.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.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 385d8ed7fa..89063319d6 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -18,71 +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 - { - Scale = 1.5f, - }; + var hitObject = d.HitObject; + hitObject.StartTime = 1000000000000; + hitObject.Scale = 1.5f; - return new DrawableTinyDroplet(droplet) - { - Anchor = Anchor.Centre, - RelativePositionAxes = Axes.None, - Position = Vector2.Zero, - Alpha = 1, - LifetimeStart = double.NegativeInfinity, - LifetimeEnd = double.PositiveInfinity, - }; - } + if (hyperdash) + hitObject.HyperDashTarget = new Banana(); - private Drawable createDrawableDroplet(bool hyperdash = false) - { - var droplet = new TestCatchDroplet + d.Anchor = Anchor.Centre; + d.RelativePositionAxes = Axes.None; + d.Position = Vector2.Zero; + d.HitObjectApplied += _ => { - Scale = 1.5f, - HyperDashTarget = hyperdash ? new Banana() : null - }; - - return new DrawableDroplet(droplet) - { - Anchor = Anchor.Centre, - RelativePositionAxes = Axes.None, - Position = Vector2.Zero, - Alpha = 1, - 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 DrawableFruit(fruit) - { - Anchor = Anchor.Centre, - RelativePositionAxes = Axes.None, - Position = Vector2.Zero, - Alpha = 1, - LifetimeStart = double.NegativeInfinity, - LifetimeEnd = double.PositiveInfinity, + d.LifetimeStart = double.NegativeInfinity; + d.LifetimeEnd = double.PositiveInfinity; }; + return d; } public class TestCatchFruit : Fruit @@ -90,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; - } - } } } 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.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(); } 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 }; 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; 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/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index c0f0fcb4af..8902d82f33 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -18,9 +18,17 @@ 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(); + /// + /// 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; @@ -38,6 +46,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 +102,26 @@ 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); + missFadeTime.BindValueChanged(onMissFadeTimeChanged, true); + + holdNote.ApplyCustomUpdateState += applyCustomUpdateState; + applyCustomUpdateState(holdNote, holdNote.State.Value); + } + + 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; } private void onIsHittingChanged(ValueChangedEvent isHitting) @@ -158,10 +183,38 @@ 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 + 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); + } + } + + protected override void Update() + { + base.Update(); + missFadeTime.Value ??= holdNote.HoldBrokenTime; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + if (holdNote != null) + holdNote.ApplyCustomUpdateState -= applyCustomUpdateState; + lightContainer?.Expire(); } } 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 => { 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.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/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index ba1d35c35c..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,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private readonly Path path; private readonly Slider slider; - private readonly int controlPointIndex; + public int ControlPointIndex { get; set; } private IBindable sliderPosition; private IBindable pathVersion; @@ -28,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; @@ -64,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 17541866ec..7375c0e981 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 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]; @@ -88,6 +99,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Connections.RemoveAll(c => c.ControlPoint == point); } + // If removing before the end of the path, + // update indices of connections after remove location + if (e.OldStartingIndex < Pieces.Count) + { + foreach (var connection in Connections) + { + if (connection.ControlPointIndex >= e.OldStartingIndex) + connection.ControlPointIndex -= e.OldItems.Count; + } + } + break; } } 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..a26db06ede 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 @@ -60,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) @@ -85,14 +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; - } - /// /// Causes this to get missed, disregarding all conditions in implementations of . /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 04fc755da5..14c494d909 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); @@ -192,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() @@ -244,7 +238,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; } @@ -264,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(); @@ -288,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(); 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.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; 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 diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index c85ad6d651..05d6930992 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,93 @@ 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 Queue<(int id, TaskCompletionSource)> pendingUserTasks = new Queue<(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.Enqueue((userId, tcs)); - return queueNextTask(nextLookup); + // Create a request task if there's not already one. + if (pendingRequestTask == null) + createNewTask(); + + return tcs.Task; } + } - List nextLookup() + private void performLookup() + { + // 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) { - int[] lookupItems; - - lock (taskAssignmentLock) + while (pendingUserTasks.Count > 0 && userTasks.Count < 50) { - pendingRequestConsumedIDs = true; - lookupItems = nextTaskIDs.ToArray(); - nextTaskIDs.Clear(); + (int id, TaskCompletionSource task) next = pendingUserTasks.Dequeue(); - if (lookupItems.Length == 0) + // 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 { - queueNextTask(null); - return new List(); + if (userTasks.TryGetValue(next.id, out var tasks)) + tasks.Add(next.task); + else + userTasks[next.id] = new List> { next.task }; } } - - var request = new GetUsersRequest(lookupItems); - - // rather than queueing, we maintain our own single-threaded request stream. - api.Perform(request); - - return request.Result?.Users; } - } - /// - /// 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) - { + // Query the users. + var request = new GetUsersRequest(userTasks.Keys.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) { - if (work == null) - { - 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; - } + pendingRequestTask = null; + if (pendingUserTasks.Count > 0) + createNewTask(); + } - return pendingRequest; + 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); } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 193f6fe61b..e7b5d3304d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -194,6 +194,20 @@ 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)) + { + 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; + }); + } + }); + dependencies.CacheAs(API ??= new APIAccess(LocalConfig)); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); 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; 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) 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; diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 53b6e14940..df9cadebfc 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 Container CreateSelectionBlueprintContainer() => - new Container { RelativeSizeAxes = Axes.Both }; + protected virtual Container CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }; /// /// Creates a which outlines s and handles movement of selections. @@ -338,7 +337,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. private bool beginClickSelection(MouseButtonEvent e) { - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) + // 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 new file mode 100644 index 0000000000..9e95fe4fa1 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -0,0 +1,77 @@ +// 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; +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) + { + base.Add(drawable); + bindStartTime(drawable); + } + + public override bool Remove(SelectionBlueprint drawable) + { + if (!base.Remove(drawable)) + return false; + + 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) + { + 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); + 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..2f14c607c2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -201,7 +201,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public TimelineSelectionBlueprintContainer() { - AddInternal(new TimelinePart(Content = new Container { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); + AddInternal(new TimelinePart(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); } } } 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 => { 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 @@ - +