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 @@
-
+