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; } } }