diff --git a/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs b/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs
index e9c29d36c2..2d49b2d0f9 100644
--- a/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs
+++ b/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs
@@ -3,19 +3,23 @@
using System;
using System.Collections.Generic;
+using NUnit.Framework;
using OpenTK;
using osu.Desktop.Tests.Beatmaps;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI;
namespace osu.Desktop.Tests.Visual
@@ -23,6 +27,7 @@ namespace osu.Desktop.Tests.Visual
///
/// The most minimal implementation of a playfield with scrolling hit objects.
///
+ [TestFixture]
public class TestCaseScrollingPlayfield : OsuTestCase
{
public TestCaseScrollingPlayfield()
@@ -64,6 +69,66 @@ namespace osu.Desktop.Tests.Visual
});
}
+ [Test]
+ public void TestSpeedAdjustmentOrdering()
+ {
+ var hitObjectContainer = new ScrollingPlayfield.ScrollingHitObjectContainer(Axes.X);
+
+ var speedAdjustments = new[]
+ {
+ new SpeedAdjustmentContainer(new MultiplierControlPoint()),
+ new SpeedAdjustmentContainer(new MultiplierControlPoint(1000)
+ {
+ TimingPoint = new TimingControlPoint { BeatLength = 500 }
+ }),
+ new SpeedAdjustmentContainer(new MultiplierControlPoint(2000)
+ {
+ TimingPoint = new TimingControlPoint { BeatLength = 1000 },
+ DifficultyPoint = new DifficultyControlPoint { SpeedMultiplier = 2}
+ }),
+ new SpeedAdjustmentContainer(new MultiplierControlPoint(3000)
+ {
+ TimingPoint = new TimingControlPoint { BeatLength = 1000 },
+ DifficultyPoint = new DifficultyControlPoint { SpeedMultiplier = 1}
+ }),
+ };
+
+ var hitObjects = new[]
+ {
+ new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = -1000 }),
+ new DrawableTestHitObject(Axes.X, new TestHitObject()),
+ new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 1000 }),
+ new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 2000 }),
+ new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 3000 }),
+ new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 4000 }),
+ };
+
+ hitObjects.ForEach(h => hitObjectContainer.Add(h));
+ speedAdjustments.ForEach(hitObjectContainer.AddSpeedAdjustment);
+
+ // The 0th index in hitObjectContainer.SpeedAdjustments is the "default" control point
+ // Check multiplier of the default speed adjustment
+ Assert.AreEqual(1, hitObjectContainer.SpeedAdjustments[0].ControlPoint.Multiplier);
+ Assert.AreEqual(1, speedAdjustments[0].ControlPoint.Multiplier);
+ Assert.AreEqual(2, speedAdjustments[1].ControlPoint.Multiplier);
+ Assert.AreEqual(2, speedAdjustments[2].ControlPoint.Multiplier);
+ Assert.AreEqual(1, speedAdjustments[3].ControlPoint.Multiplier);
+
+ // Check insertion of hit objects
+ Assert.IsTrue(hitObjectContainer.SpeedAdjustments[0].Contains(hitObjects[0]));
+ Assert.IsTrue(speedAdjustments[0].Contains(hitObjects[1]));
+ Assert.IsTrue(speedAdjustments[1].Contains(hitObjects[2]));
+ Assert.IsTrue(speedAdjustments[2].Contains(hitObjects[3]));
+ Assert.IsTrue(speedAdjustments[3].Contains(hitObjects[4]));
+ Assert.IsTrue(speedAdjustments[3].Contains(hitObjects[5]));
+
+ hitObjectContainer.RemoveSpeedAdjustment(speedAdjustments[1]);
+
+ // The hit object contained in this speed adjustment should be resorted into the previous one
+
+ Assert.IsTrue(speedAdjustments[0].Contains(hitObjects[2]));
+ }
+
private class TestRulesetContainer : ScrollingRulesetContainer
{
private readonly Axes scrollingAxes;
@@ -158,4 +223,4 @@ namespace osu.Desktop.Tests.Visual
public override string MaxResultString { get { throw new NotImplementedException(); } }
}
}
-}
\ No newline at end of file
+}
diff --git a/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs b/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs
index 5d6c03b9de..3b13fdf00a 100644
--- a/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs
+++ b/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs
@@ -1,7 +1,6 @@
// Copyright (c) 2007-2017 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -27,7 +26,7 @@ namespace osu.Game.Rulesets.Timing
public readonly BindableBool Reversed = new BindableBool();
protected override Container Content => content;
- private Container content;
+ private readonly Container content;
///
/// The axes which the content of this container will scroll through.
@@ -41,7 +40,7 @@ namespace osu.Game.Rulesets.Timing
///
public readonly MultiplierControlPoint ControlPoint;
- private ScrollingContainer scrollingContainer;
+ private readonly ScrollingContainer scrollingContainer;
///
/// Creates a new .
@@ -51,11 +50,7 @@ namespace osu.Game.Rulesets.Timing
{
ControlPoint = controlPoint;
RelativeSizeAxes = Axes.Both;
- }
- [BackgroundDependencyLoader]
- private void load()
- {
scrollingContainer = CreateScrollingContainer();
scrollingContainer.ScrollingAxes = ScrollingAxes;
diff --git a/osu.Game/Rulesets/UI/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/ScrollingPlayfield.cs
index 0a8469c928..524665487d 100644
--- a/osu.Game/Rulesets/UI/ScrollingPlayfield.cs
+++ b/osu.Game/Rulesets/UI/ScrollingPlayfield.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.UI
///
/// The container that contains the s and s.
///
- internal new readonly ScrollingHitObjectContainer HitObjects;
+ public new readonly ScrollingHitObjectContainer HitObjects;
///
/// Creates a new .
@@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.UI
///
/// A container that provides the foundation for sorting s into s.
///
- internal class ScrollingHitObjectContainer : HitObjectContainer
+ public class ScrollingHitObjectContainer : HitObjectContainer
{
///
/// Gets or sets the range of time that is visible by the length of the scrolling axes.
@@ -152,6 +152,9 @@ namespace osu.Game.Rulesets.UI
public readonly BindableBool Reversed = new BindableBool();
private readonly Container speedAdjustments;
+ public IReadOnlyList SpeedAdjustments => speedAdjustments;
+
+ private readonly SpeedAdjustmentContainer defaultSpeedAdjustment;
private readonly Axes scrollingAxes;
@@ -166,7 +169,7 @@ namespace osu.Game.Rulesets.UI
AddInternal(speedAdjustments = new Container { RelativeSizeAxes = Axes.Both });
// Default speed adjustment
- AddSpeedAdjustment(new SpeedAdjustmentContainer(new MultiplierControlPoint(0)));
+ AddSpeedAdjustment(defaultSpeedAdjustment = new SpeedAdjustmentContainer(new MultiplierControlPoint(0)));
}
///
@@ -181,18 +184,44 @@ namespace osu.Game.Rulesets.UI
speedAdjustments.Add(speedAdjustment);
// We now need to re-sort the hit objects in the last speed adjustment prior to this one, to see if they need a new parent
- var previousSpeedAdjustment = speedAdjustments.LastOrDefault(s => s.ControlPoint.StartTime < speedAdjustment.ControlPoint.StartTime);
+ var previousSpeedAdjustment = speedAdjustments.LastOrDefault(s => s != speedAdjustment && s.ControlPoint.StartTime <= speedAdjustment.ControlPoint.StartTime);
if (previousSpeedAdjustment == null)
return;
- foreach (DrawableHitObject h in previousSpeedAdjustment.Children)
+ for (int i = 0; i < previousSpeedAdjustment.Children.Count; i++)
{
- var newSpeedAdjustment = adjustmentContainerFor(h);
+ DrawableHitObject hitObject = previousSpeedAdjustment[i];
+
+ var newSpeedAdjustment = adjustmentContainerFor(hitObject);
if (newSpeedAdjustment == previousSpeedAdjustment)
continue;
- previousSpeedAdjustment.Remove(h);
- newSpeedAdjustment.Add(h);
+ previousSpeedAdjustment.Remove(hitObject);
+ newSpeedAdjustment.Add(hitObject);
+
+ i--;
+ }
+ }
+
+ ///
+ /// Removes a from this container, re-sorting all hit objects
+ /// which it contained into new s.
+ ///
+ /// The to remove.
+ public void RemoveSpeedAdjustment(SpeedAdjustmentContainer speedAdjustment)
+ {
+ if (speedAdjustment == defaultSpeedAdjustment)
+ throw new InvalidOperationException($"The default {nameof(SpeedAdjustmentContainer)} must not be removed.");
+
+ if (!speedAdjustments.Remove(speedAdjustment))
+ return;
+
+ while (speedAdjustment.Count > 0)
+ {
+ DrawableHitObject hitObject = speedAdjustment[0];
+
+ speedAdjustment.Remove(hitObject);
+ Add(hitObject);
}
}
@@ -208,11 +237,7 @@ namespace osu.Game.Rulesets.UI
if (!(hitObject is IScrollingHitObject))
throw new InvalidOperationException($"Hit objects added to a {nameof(ScrollingHitObjectContainer)} must implement {nameof(IScrollingHitObject)}.");
- var target = adjustmentContainerFor(hitObject);
- if (target == null)
- throw new InvalidOperationException($"A {nameof(SpeedAdjustmentContainer)} to container {hitObject} could not be found.");
-
- target.Add(hitObject);
+ adjustmentContainerFor(hitObject).Add(hitObject);
}
public override bool Remove(DrawableHitObject hitObject) => speedAdjustments.Any(s => s.Remove(hitObject));
@@ -224,7 +249,7 @@ namespace osu.Game.Rulesets.UI
///
/// The hit object to find the active for.
/// The active at 's start time. Null if there are no speed adjustments.
- private SpeedAdjustmentContainer adjustmentContainerFor(DrawableHitObject hitObject) => speedAdjustments.FirstOrDefault(c => c.CanContain(hitObject)) ?? speedAdjustments.LastOrDefault();
+ private SpeedAdjustmentContainer adjustmentContainerFor(DrawableHitObject hitObject) => speedAdjustments.LastOrDefault(c => c.CanContain(hitObject)) ?? defaultSpeedAdjustment;
///
/// Finds the which provides the speed adjustment active at a time.
@@ -232,7 +257,7 @@ namespace osu.Game.Rulesets.UI
///
/// The time to find the active at.
/// The active at . Null if there are no speed adjustments.
- private SpeedAdjustmentContainer adjustmentContainerAt(double time) => speedAdjustments.FirstOrDefault(c => c.CanContain(time)) ?? speedAdjustments.LastOrDefault();
+ private SpeedAdjustmentContainer adjustmentContainerAt(double time) => speedAdjustments.LastOrDefault(c => c.CanContain(time)) ?? defaultSpeedAdjustment;
}
}
}