mirror of https://github.com/ppy/osu
516 lines
19 KiB
C#
516 lines
19 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
#nullable disable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using NUnit.Framework;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Shapes;
|
|
using osu.Framework.Input;
|
|
using osu.Framework.Utils;
|
|
using osu.Framework.Timing;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Beatmaps.ControlPoints;
|
|
using osu.Game.Configuration;
|
|
using osu.Game.Rulesets;
|
|
using osu.Game.Rulesets.Difficulty;
|
|
using osu.Game.Rulesets.Mods;
|
|
using osu.Game.Rulesets.Objects;
|
|
using osu.Game.Rulesets.Objects.Drawables;
|
|
using osu.Game.Rulesets.Objects.Types;
|
|
using osu.Game.Rulesets.Osu;
|
|
using osu.Game.Rulesets.UI;
|
|
using osu.Game.Rulesets.UI.Scrolling;
|
|
using osuTK;
|
|
using osuTK.Graphics;
|
|
using JetBrains.Annotations;
|
|
|
|
namespace osu.Game.Tests.Visual.Gameplay
|
|
{
|
|
public partial class TestSceneDrawableScrollingRuleset : OsuTestScene
|
|
{
|
|
/// <summary>
|
|
/// The amount of time visible by the "view window" of the playfield.
|
|
/// All hitobjects added through <see cref="createBeatmap"/> are spaced apart by this value, such that for a beat length of 1000,
|
|
/// there will be at most 2 hitobjects visible in the "view window".
|
|
/// </summary>
|
|
private const double time_range = 1000;
|
|
|
|
private readonly ManualClock testClock = new ManualClock();
|
|
private TestDrawableScrollingRuleset drawableRuleset;
|
|
|
|
[SetUp]
|
|
public void Setup() => Schedule(() => testClock.CurrentTime = 0);
|
|
|
|
[TestCase("pooled")]
|
|
[TestCase("non-pooled")]
|
|
public void TestHitObjectLifetime(string pooled)
|
|
{
|
|
var beatmap = createBeatmap(_ => pooled == "pooled" ? new TestPooledHitObject() : new TestHitObject());
|
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
|
createTest(beatmap);
|
|
|
|
assertPosition(0, 0f);
|
|
assertDead(3);
|
|
|
|
setTime(3 * time_range);
|
|
assertPosition(3, 0f);
|
|
assertDead(0);
|
|
|
|
setTime(0 * time_range);
|
|
assertPosition(0, 0f);
|
|
assertDead(3);
|
|
}
|
|
|
|
[TestCase("pooled")]
|
|
[TestCase("non-pooled")]
|
|
public void TestNestedHitObject(string pooled)
|
|
{
|
|
var beatmap = createBeatmap(i =>
|
|
{
|
|
var h = pooled == "pooled" ? new TestPooledParentHitObject() : new TestParentHitObject();
|
|
h.Duration = 300;
|
|
h.ChildTimeOffset = i % 3 * 100;
|
|
return h;
|
|
});
|
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
|
createTest(beatmap);
|
|
|
|
assertPosition(0, 0f);
|
|
assertHeight(0);
|
|
assertChildPosition(0);
|
|
|
|
setTime(5 * time_range);
|
|
assertPosition(5, 0f);
|
|
assertHeight(5);
|
|
assertChildPosition(5);
|
|
}
|
|
|
|
[TestCase("pooled")]
|
|
[TestCase("non-pooled")]
|
|
public void TestLifetimeRecomputedWhenTimeRangeChanges(string pooled)
|
|
{
|
|
var beatmap = createBeatmap(_ => pooled == "pooled" ? new TestPooledHitObject() : new TestHitObject());
|
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
|
createTest(beatmap);
|
|
|
|
assertDead(3);
|
|
|
|
AddStep("increase time range", () => drawableRuleset.TimeRange.Value = 3 * time_range);
|
|
assertPosition(3, 1);
|
|
}
|
|
|
|
[Test]
|
|
public void TestRelativeBeatLengthScaleSingleTimingPoint()
|
|
{
|
|
var beatmap = createBeatmap();
|
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 });
|
|
|
|
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
|
|
|
assertPosition(0, 0f);
|
|
|
|
// The single timing point is 1x speed relative to itself, such that the hitobject occurring time_range milliseconds later should appear
|
|
// at the bottom of the view window regardless of the timing point's beat length
|
|
assertPosition(1, 1f);
|
|
}
|
|
|
|
[Test]
|
|
public void TestRelativeBeatLengthScaleTimingPointBeyondEndDoesNotBecomeDominant()
|
|
{
|
|
var beatmap = createBeatmap();
|
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range / 2 });
|
|
beatmap.ControlPointInfo.Add(12000, new TimingControlPoint { BeatLength = time_range });
|
|
beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = time_range });
|
|
|
|
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
|
|
|
assertPosition(0, 0f);
|
|
assertPosition(1, 1f);
|
|
}
|
|
|
|
[Test]
|
|
public void TestRelativeBeatLengthScaleFromSecondTimingPoint()
|
|
{
|
|
var beatmap = createBeatmap();
|
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
|
beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 });
|
|
|
|
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
|
|
|
// The first timing point should have a relative velocity of 2
|
|
assertPosition(0, 0f);
|
|
assertPosition(1, 0.5f);
|
|
assertPosition(2, 1f);
|
|
|
|
// Move to the second timing point
|
|
setTime(3 * time_range);
|
|
assertPosition(3, 0f);
|
|
|
|
// As above, this is the timing point that is 1x speed relative to itself, so the hitobject occurring time_range milliseconds later should be at the bottom of the view window
|
|
assertPosition(4, 1f);
|
|
}
|
|
|
|
[Test]
|
|
public void TestNonRelativeScale()
|
|
{
|
|
var beatmap = createBeatmap();
|
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
|
beatmap.ControlPointInfo.Add(3 * time_range, new TimingControlPoint { BeatLength = time_range / 2 });
|
|
|
|
createTest(beatmap);
|
|
|
|
assertPosition(0, 0f);
|
|
assertPosition(1, 1);
|
|
|
|
// Move to the second timing point
|
|
setTime(3 * time_range);
|
|
assertPosition(3, 0f);
|
|
|
|
// For a beat length of 500, the view window of this timing point is elongated 2x (1000 / 500), such that the second hitobject is two TimeRanges away (offscreen)
|
|
// To bring it on-screen, half TimeRange is added to the current time, bringing the second half of the view window into view, and the hitobject should appear at the bottom
|
|
setTime(3 * time_range + time_range / 2);
|
|
assertPosition(4, 1f);
|
|
}
|
|
|
|
[Test]
|
|
public void TestSliderMultiplierDoesNotAffectRelativeBeatLength()
|
|
{
|
|
var beatmap = createBeatmap();
|
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
|
beatmap.Difficulty.SliderMultiplier = 2;
|
|
|
|
createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
|
|
AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 5000);
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
assertPosition(i, i / 5f);
|
|
}
|
|
|
|
[Test]
|
|
public void TestSliderMultiplierAffectsNonRelativeBeatLength()
|
|
{
|
|
var beatmap = createBeatmap();
|
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
|
|
beatmap.BeatmapInfo.Difficulty.SliderMultiplier = 2;
|
|
|
|
createTest(beatmap);
|
|
AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 2000);
|
|
|
|
assertPosition(0, 0);
|
|
assertPosition(1, 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a <see cref="DrawableTestHitObject" /> corresponding to the <paramref name="index"/>'th <see cref="TestHitObject"/>.
|
|
/// When the hit object is not alive, `null` is returned.
|
|
/// </summary>
|
|
[CanBeNull]
|
|
private DrawableTestHitObject getDrawableHitObject(int index)
|
|
{
|
|
var hitObject = drawableRuleset.Beatmap.HitObjects.ElementAt(index);
|
|
return (DrawableTestHitObject)drawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(obj => obj.HitObject == hitObject);
|
|
}
|
|
|
|
private float yScale => drawableRuleset.Playfield.HitObjectContainer.DrawHeight;
|
|
|
|
private void assertDead(int index) => AddAssert($"hitobject {index} is dead", () => getDrawableHitObject(index) == null);
|
|
|
|
private void assertHeight(int index) => AddAssert($"hitobject {index} height", () =>
|
|
{
|
|
var d = getDrawableHitObject(index);
|
|
return d != null && Precision.AlmostEquals(d.DrawHeight, yScale * (float)(d.HitObject.Duration / time_range), 0.1f);
|
|
});
|
|
|
|
private void assertChildPosition(int index) => AddAssert($"hitobject {index} child position", () =>
|
|
{
|
|
var d = getDrawableHitObject(index);
|
|
return d is DrawableTestParentHitObject && Precision.AlmostEquals(
|
|
d.NestedHitObjects.First().DrawPosition.Y,
|
|
yScale * (float)((TestParentHitObject)d.HitObject).ChildTimeOffset / time_range, 0.1f);
|
|
});
|
|
|
|
private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}",
|
|
() => getDrawableHitObject(index)?.DrawPosition.Y / yScale ?? -1, () => Is.EqualTo(relativeY).Within(Precision.FLOAT_EPSILON));
|
|
|
|
private void setTime(double time)
|
|
{
|
|
AddStep($"set time = {time}", () => testClock.CurrentTime = time);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IBeatmap"/>, containing 10 hitobjects and user-provided timing points.
|
|
/// The hitobjects are spaced <see cref="time_range"/> milliseconds apart.
|
|
/// </summary>
|
|
/// <returns>The <see cref="IBeatmap"/>.</returns>
|
|
private IBeatmap createBeatmap(Func<int, TestHitObject> createAction = null)
|
|
{
|
|
var beatmap = new Beatmap<TestHitObject>
|
|
{
|
|
BeatmapInfo =
|
|
{
|
|
Difficulty = new BeatmapDifficulty
|
|
{
|
|
SliderMultiplier = 1
|
|
},
|
|
Ruleset = new OsuRuleset().RulesetInfo
|
|
}
|
|
};
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
var h = createAction?.Invoke(i) ?? new TestHitObject();
|
|
h.StartTime = i * time_range;
|
|
beatmap.HitObjects.Add(h);
|
|
}
|
|
|
|
return beatmap;
|
|
}
|
|
|
|
private void createTest(IBeatmap beatmap, Action<TestDrawableScrollingRuleset> overrideAction = null)
|
|
{
|
|
AddStep("create test", () =>
|
|
{
|
|
var ruleset = new TestScrollingRuleset();
|
|
|
|
drawableRuleset = (TestDrawableScrollingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo));
|
|
drawableRuleset.FrameStablePlayback = false;
|
|
|
|
overrideAction?.Invoke(drawableRuleset);
|
|
|
|
Child = new Container
|
|
{
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
RelativeSizeAxes = Axes.Y,
|
|
Height = 0.75f,
|
|
Width = 400,
|
|
Masking = true,
|
|
Clock = new FramedClock(testClock),
|
|
Child = drawableRuleset
|
|
};
|
|
});
|
|
}
|
|
|
|
#region Ruleset
|
|
|
|
private class TestScrollingRuleset : Ruleset
|
|
{
|
|
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
|
|
|
|
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new TestDrawableScrollingRuleset(this, beatmap, mods);
|
|
|
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap, null);
|
|
|
|
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
|
|
|
|
public override string Description { get; } = string.Empty;
|
|
|
|
public override string ShortName { get; } = string.Empty;
|
|
}
|
|
|
|
private partial class TestDrawableScrollingRuleset : DrawableScrollingRuleset<TestHitObject>
|
|
{
|
|
public bool RelativeScaleBeatLengthsOverride { get; set; }
|
|
|
|
protected override bool RelativeScaleBeatLengths => RelativeScaleBeatLengthsOverride;
|
|
|
|
public new Bindable<double> TimeRange => base.TimeRange;
|
|
|
|
public TestDrawableScrollingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
|
: base(ruleset, beatmap, mods)
|
|
{
|
|
TimeRange.Value = time_range;
|
|
VisualisationMethod = ScrollVisualisationMethod.Overlapping;
|
|
}
|
|
|
|
public override DrawableHitObject<TestHitObject> CreateDrawableRepresentation(TestHitObject h)
|
|
{
|
|
switch (h)
|
|
{
|
|
case TestPooledHitObject:
|
|
case TestPooledParentHitObject:
|
|
return null;
|
|
|
|
case TestParentHitObject p:
|
|
return new DrawableTestParentHitObject(p);
|
|
|
|
default:
|
|
return new DrawableTestHitObject(h);
|
|
}
|
|
}
|
|
|
|
protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager();
|
|
|
|
protected override Playfield CreatePlayfield() => new TestPlayfield();
|
|
}
|
|
|
|
private partial class TestPlayfield : ScrollingPlayfield
|
|
{
|
|
public TestPlayfield()
|
|
{
|
|
AddInternal(new Container
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Children = new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Alpha = 0.2f,
|
|
},
|
|
new Container
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Padding = new MarginPadding { Top = 150 },
|
|
Children = new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
Anchor = Anchor.TopCentre,
|
|
Origin = Anchor.Centre,
|
|
RelativeSizeAxes = Axes.X,
|
|
Height = 2,
|
|
Colour = Color4.Green
|
|
},
|
|
HitObjectContainer
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
RegisterPool<TestPooledHitObject, DrawableTestPooledHitObject>(1);
|
|
RegisterPool<TestPooledParentHitObject, DrawableTestPooledParentHitObject>(1);
|
|
}
|
|
}
|
|
|
|
private class TestBeatmapConverter : BeatmapConverter<TestHitObject>
|
|
{
|
|
public TestBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
|
|
: base(beatmap, ruleset)
|
|
{
|
|
}
|
|
|
|
public override bool CanConvert() => true;
|
|
|
|
protected override IEnumerable<TestHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) =>
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region HitObject
|
|
|
|
private class TestHitObject : HitObject, IHasDuration
|
|
{
|
|
public double EndTime => StartTime + Duration;
|
|
|
|
public double Duration { get; set; } = 100;
|
|
}
|
|
|
|
private class TestPooledHitObject : TestHitObject
|
|
{
|
|
}
|
|
|
|
private class TestParentHitObject : TestHitObject
|
|
{
|
|
public double ChildTimeOffset;
|
|
|
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
|
{
|
|
AddNested(new TestHitObject { StartTime = StartTime + ChildTimeOffset });
|
|
}
|
|
}
|
|
|
|
private class TestPooledParentHitObject : TestParentHitObject
|
|
{
|
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
|
{
|
|
AddNested(new TestPooledHitObject { StartTime = StartTime + ChildTimeOffset });
|
|
}
|
|
}
|
|
|
|
private partial class DrawableTestHitObject : DrawableHitObject<TestHitObject>
|
|
{
|
|
public DrawableTestHitObject([CanBeNull] TestHitObject hitObject)
|
|
: base(hitObject)
|
|
{
|
|
Anchor = Anchor.TopCentre;
|
|
Origin = Anchor.TopCentre;
|
|
|
|
Size = new Vector2(100, 25);
|
|
|
|
AddRangeInternal(new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Colour = Color4.LightPink
|
|
},
|
|
new Box
|
|
{
|
|
Origin = Anchor.CentreLeft,
|
|
RelativeSizeAxes = Axes.X,
|
|
Height = 2,
|
|
Colour = Color4.Red
|
|
}
|
|
});
|
|
}
|
|
|
|
protected override void Update() => LifetimeEnd = HitObject.EndTime;
|
|
}
|
|
|
|
private partial class DrawableTestPooledHitObject : DrawableTestHitObject
|
|
{
|
|
public DrawableTestPooledHitObject()
|
|
: base(null)
|
|
{
|
|
InternalChildren[0].Colour = Color4.LightSkyBlue;
|
|
InternalChildren[1].Colour = Color4.Blue;
|
|
}
|
|
}
|
|
|
|
private partial class DrawableTestParentHitObject : DrawableTestHitObject
|
|
{
|
|
private readonly Container<DrawableHitObject> container;
|
|
|
|
public DrawableTestParentHitObject([CanBeNull] TestHitObject hitObject)
|
|
: base(hitObject)
|
|
{
|
|
InternalChildren[0].Colour = Color4.LightYellow;
|
|
InternalChildren[1].Colour = Color4.Yellow;
|
|
|
|
AddInternal(container = new Container<DrawableHitObject>
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
});
|
|
}
|
|
|
|
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) =>
|
|
new DrawableTestHitObject((TestHitObject)hitObject);
|
|
|
|
protected override void AddNestedHitObject(DrawableHitObject hitObject) => container.Add(hitObject);
|
|
|
|
protected override void ClearNestedHitObjects() => container.Clear(false);
|
|
}
|
|
|
|
private partial class DrawableTestPooledParentHitObject : DrawableTestParentHitObject
|
|
{
|
|
public DrawableTestPooledParentHitObject()
|
|
: base(null)
|
|
{
|
|
InternalChildren[0].Colour = Color4.LightSeaGreen;
|
|
InternalChildren[1].Colour = Color4.Green;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|