2023-10-16 09:31:01 +00:00
|
|
|
// 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.
|
|
|
|
|
2023-10-30 20:15:04 +00:00
|
|
|
using System;
|
2023-10-16 09:31:01 +00:00
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Linq;
|
|
|
|
using NUnit.Framework;
|
|
|
|
using osu.Framework.Screens;
|
|
|
|
using osu.Framework.Testing;
|
|
|
|
using osu.Framework.Timing;
|
|
|
|
using osu.Game.Beatmaps;
|
|
|
|
using osu.Game.Replays;
|
|
|
|
using osu.Game.Rulesets.Judgements;
|
2023-10-30 20:15:04 +00:00
|
|
|
using osu.Game.Rulesets.Mods;
|
|
|
|
using osu.Game.Rulesets.Osu.Mods;
|
2023-10-16 09:31:01 +00:00
|
|
|
using osu.Game.Rulesets.Osu.Objects;
|
|
|
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
|
|
|
using osu.Game.Rulesets.Osu.Replays;
|
|
|
|
using osu.Game.Rulesets.Replays;
|
|
|
|
using osu.Game.Rulesets.Scoring;
|
|
|
|
using osu.Game.Rulesets.UI;
|
|
|
|
using osu.Game.Scoring;
|
|
|
|
using osu.Game.Screens.Play;
|
|
|
|
using osu.Game.Storyboards;
|
|
|
|
using osu.Game.Tests.Visual;
|
|
|
|
using osuTK;
|
|
|
|
|
|
|
|
namespace osu.Game.Rulesets.Osu.Tests
|
|
|
|
{
|
|
|
|
public partial class TestSceneSpinnerInput : RateAdjustedBeatmapTestScene
|
|
|
|
{
|
|
|
|
private const int centre_x = 256;
|
|
|
|
private const int centre_y = 192;
|
|
|
|
private const double time_spinner_start = 1500;
|
|
|
|
private const double time_spinner_end = 8000;
|
|
|
|
|
|
|
|
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
|
|
|
|
|
|
|
|
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
|
|
|
private ManualClock? manualClock;
|
|
|
|
|
|
|
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
|
|
|
{
|
|
|
|
return manualClock == null
|
|
|
|
? base.CreateWorkingBeatmap(beatmap, storyboard)
|
|
|
|
: new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio);
|
|
|
|
}
|
|
|
|
|
|
|
|
[SetUp]
|
|
|
|
public void Setup() => Schedule(() =>
|
|
|
|
{
|
|
|
|
manualClock = null;
|
2023-10-30 20:15:04 +00:00
|
|
|
SelectedMods.Value = Array.Empty<Mod>();
|
2023-10-16 09:31:01 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
|
|
|
/// </summary>
|
|
|
|
[Test]
|
|
|
|
public void TestVibrateWithoutSpinningOffCentre()
|
|
|
|
{
|
|
|
|
List<ReplayFrame> frames = new List<ReplayFrame>();
|
|
|
|
|
|
|
|
const int vibrate_time = 50;
|
|
|
|
const float y_pos = centre_y - 50;
|
|
|
|
|
|
|
|
int direction = -1;
|
|
|
|
|
|
|
|
for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time)
|
|
|
|
{
|
|
|
|
frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, y_pos), OsuAction.LeftButton));
|
|
|
|
frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, y_pos), OsuAction.LeftButton));
|
|
|
|
|
|
|
|
direction *= -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
performTest(frames);
|
|
|
|
|
|
|
|
assertTicksHit(0);
|
|
|
|
assertSpinnerHit(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
|
|
|
/// </summary>
|
|
|
|
[Test]
|
|
|
|
public void TestVibrateWithoutSpinningOnCentre()
|
|
|
|
{
|
|
|
|
List<ReplayFrame> frames = new List<ReplayFrame>();
|
|
|
|
|
|
|
|
const int vibrate_time = 50;
|
|
|
|
|
|
|
|
int direction = -1;
|
|
|
|
|
|
|
|
for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time)
|
|
|
|
{
|
|
|
|
frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton));
|
|
|
|
frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton));
|
|
|
|
|
|
|
|
direction *= -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
performTest(frames);
|
|
|
|
|
|
|
|
assertTicksHit(0);
|
|
|
|
assertSpinnerHit(false);
|
|
|
|
}
|
2023-10-30 20:15:04 +00:00
|
|
|
|
|
|
|
[Test]
|
|
|
|
public void TestVibrateWithoutSpinningOnCentreWithDoubleTime()
|
|
|
|
{
|
|
|
|
List<ReplayFrame> frames = new List<ReplayFrame>();
|
|
|
|
|
|
|
|
const int rate = 2;
|
|
|
|
// the track clock is going to be playing twice as fast,
|
|
|
|
// so the vibration time in clock time needs to be twice as long
|
|
|
|
// to keep constant speed in real time.
|
|
|
|
const int vibrate_time = 50 * rate;
|
|
|
|
|
|
|
|
int direction = -1;
|
|
|
|
|
|
|
|
for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time)
|
|
|
|
{
|
|
|
|
frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton));
|
|
|
|
frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton));
|
|
|
|
|
|
|
|
direction *= -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
AddStep("set DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = rate } } });
|
|
|
|
performTest(frames);
|
|
|
|
|
|
|
|
assertSpinnerHit(false);
|
|
|
|
}
|
2023-10-16 09:31:01 +00:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Spins in a single direction.
|
|
|
|
/// </summary>
|
2023-10-16 09:34:56 +00:00
|
|
|
[TestCase(180, 0)]
|
|
|
|
[TestCase(-180, 0)]
|
|
|
|
[TestCase(360, 1)]
|
|
|
|
[TestCase(-360, 1)]
|
|
|
|
[TestCase(540, 1)]
|
|
|
|
[TestCase(-540, 1)]
|
|
|
|
[TestCase(720, 2)]
|
|
|
|
[TestCase(-720, 2)]
|
2023-10-16 09:31:01 +00:00
|
|
|
public void TestSpinSingleDirection(float amount, int expectedTicks)
|
|
|
|
{
|
|
|
|
performTest(new SpinFramesGenerator(time_spinner_start)
|
|
|
|
.Spin(amount, 500)
|
|
|
|
.Build());
|
|
|
|
|
|
|
|
assertTicksHit(expectedTicks);
|
|
|
|
assertSpinnerHit(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Spin half-way clockwise then perform one full spin counter-clockwise.
|
|
|
|
/// No ticks should be hit since the total rotation is -0.5 (0.5 CW + 1 CCW = 0.5 CCW).
|
|
|
|
/// </summary>
|
|
|
|
[Test]
|
|
|
|
public void TestSpinHalfBothDirections()
|
|
|
|
{
|
|
|
|
performTest(new SpinFramesGenerator(time_spinner_start)
|
2023-10-16 09:34:56 +00:00
|
|
|
.Spin(180, 500) // Rotate to +0.5.
|
|
|
|
.Spin(-360, 500) // Rotate to -0.5
|
2023-10-16 09:31:01 +00:00
|
|
|
.Build());
|
|
|
|
|
|
|
|
assertTicksHit(0);
|
|
|
|
assertSpinnerHit(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Spin in one direction then spin in the other.
|
|
|
|
/// </summary>
|
2023-10-16 09:34:56 +00:00
|
|
|
[TestCase(180, -540, 1)]
|
|
|
|
[TestCase(-180, 540, 1)]
|
|
|
|
[TestCase(180, -900, 2)]
|
|
|
|
[TestCase(-180, 900, 2)]
|
2023-10-16 09:31:01 +00:00
|
|
|
public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks)
|
|
|
|
{
|
|
|
|
performTest(new SpinFramesGenerator(time_spinner_start)
|
|
|
|
.Spin(direction1, 500)
|
|
|
|
.Spin(direction2, 500)
|
|
|
|
.Build());
|
|
|
|
|
|
|
|
assertTicksHit(expectedTicks);
|
|
|
|
assertSpinnerHit(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
public void TestRewind()
|
|
|
|
{
|
2023-10-23 11:28:12 +00:00
|
|
|
AddStep("set manual clock", () => manualClock = new ManualClock
|
|
|
|
{
|
|
|
|
// Avoids interpolation trying to run ahead during testing.
|
|
|
|
Rate = 0
|
|
|
|
});
|
2023-10-16 09:31:01 +00:00
|
|
|
|
2023-10-17 10:12:12 +00:00
|
|
|
List<ReplayFrame> frames =
|
|
|
|
new SpinFramesGenerator(time_spinner_start)
|
|
|
|
// 1500ms start
|
|
|
|
.Spin(360, 500)
|
|
|
|
// 2000ms -> 1 full CW spin
|
|
|
|
.Spin(-180, 500)
|
|
|
|
// 2500ms -> 1 full CW spin + 0.5 CCW spins
|
|
|
|
.Spin(90, 500)
|
|
|
|
// 3000ms -> 1 full CW spin + 0.25 CCW spins
|
|
|
|
.Spin(450, 500)
|
|
|
|
// 3500ms -> 2 full CW spins
|
|
|
|
.Spin(180, 500)
|
|
|
|
// 4000ms -> 2 full CW spins + 0.5 CW spins
|
|
|
|
.Build();
|
2023-10-16 09:31:01 +00:00
|
|
|
|
|
|
|
loadPlayer(frames);
|
|
|
|
|
|
|
|
GameplayClockContainer clock = null!;
|
|
|
|
DrawableRuleset drawableRuleset = null!;
|
|
|
|
AddStep("get gameplay objects", () =>
|
|
|
|
{
|
|
|
|
clock = currentPlayer.ChildrenOfType<GameplayClockContainer>().Single();
|
|
|
|
drawableRuleset = currentPlayer.ChildrenOfType<DrawableRuleset>().Single();
|
|
|
|
});
|
|
|
|
|
|
|
|
addSeekStep(frames.Last().Time);
|
|
|
|
|
|
|
|
DrawableSpinner drawableSpinner = null!;
|
|
|
|
AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType<DrawableSpinner>().Single()) != null);
|
|
|
|
|
2023-10-23 11:29:46 +00:00
|
|
|
assertFinalRotationCorrect();
|
2023-10-16 09:31:01 +00:00
|
|
|
assertTotalRotation(3750, 810);
|
|
|
|
assertTotalRotation(3500, 720);
|
|
|
|
assertTotalRotation(3250, 530);
|
2023-10-17 10:12:12 +00:00
|
|
|
assertTotalRotation(3000, 450);
|
2023-10-16 09:31:01 +00:00
|
|
|
assertTotalRotation(2750, 540);
|
|
|
|
assertTotalRotation(2500, 540);
|
2023-10-17 10:12:12 +00:00
|
|
|
assertTotalRotation(2250, 450);
|
|
|
|
assertTotalRotation(2000, 360);
|
2023-10-16 09:31:01 +00:00
|
|
|
assertTotalRotation(1500, 0);
|
|
|
|
|
2023-10-23 11:29:46 +00:00
|
|
|
// same thing but always returning to final time to check.
|
|
|
|
assertFinalRotationCorrect();
|
|
|
|
assertTotalRotation(3750, 810);
|
|
|
|
assertFinalRotationCorrect();
|
|
|
|
assertTotalRotation(3500, 720);
|
|
|
|
assertFinalRotationCorrect();
|
|
|
|
assertTotalRotation(3250, 530);
|
|
|
|
assertFinalRotationCorrect();
|
|
|
|
assertTotalRotation(3000, 450);
|
|
|
|
assertFinalRotationCorrect();
|
|
|
|
assertTotalRotation(2750, 540);
|
|
|
|
assertFinalRotationCorrect();
|
|
|
|
assertTotalRotation(2500, 540);
|
|
|
|
assertFinalRotationCorrect();
|
|
|
|
assertTotalRotation(2250, 450);
|
|
|
|
assertFinalRotationCorrect();
|
|
|
|
assertTotalRotation(2000, 360);
|
|
|
|
assertFinalRotationCorrect();
|
|
|
|
assertTotalRotation(1500, 0);
|
|
|
|
|
2023-10-16 09:31:01 +00:00
|
|
|
void assertTotalRotation(double time, float expected)
|
|
|
|
{
|
|
|
|
addSeekStep(time);
|
|
|
|
AddAssert($"total rotation @ {time} is {expected}", () => drawableSpinner.Result.TotalRotation,
|
|
|
|
() => Is.EqualTo(expected).Within(MathHelper.RadiansToDegrees(SpinFramesGenerator.SPIN_ERROR * 2)));
|
|
|
|
}
|
|
|
|
|
|
|
|
void addSeekStep(double time)
|
|
|
|
{
|
|
|
|
AddStep($"seek to {time}", () => clock.Seek(time));
|
2023-10-17 10:12:12 +00:00
|
|
|
// Lenience is required due to interpolation running slightly ahead on a stalled clock.
|
2023-10-23 11:28:12 +00:00
|
|
|
AddUntilStep("wait for seek to finish", () => drawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time));
|
2023-10-16 09:31:01 +00:00
|
|
|
}
|
2023-10-23 11:29:46 +00:00
|
|
|
|
|
|
|
void assertFinalRotationCorrect() => assertTotalRotation(4000, 900);
|
2023-10-16 09:31:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private void assertTicksHit(int count)
|
|
|
|
{
|
|
|
|
AddAssert($"{count} ticks hit", () => judgementResults.Where(r => r.HitObject is SpinnerTick).Count(r => r.IsHit), () => Is.EqualTo(count));
|
|
|
|
}
|
|
|
|
|
|
|
|
private void assertSpinnerHit(bool shouldBeHit)
|
|
|
|
{
|
|
|
|
AddAssert($"spinner is {(shouldBeHit ? "hit" : "missed")}", () => judgementResults.Single(r => r.HitObject is Spinner).IsHit, () => Is.EqualTo(shouldBeHit));
|
|
|
|
}
|
|
|
|
|
|
|
|
private void loadPlayer(List<ReplayFrame> frames)
|
|
|
|
{
|
|
|
|
AddStep("load player", () =>
|
|
|
|
{
|
|
|
|
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
|
|
|
{
|
|
|
|
HitObjects =
|
|
|
|
{
|
|
|
|
new Spinner
|
|
|
|
{
|
|
|
|
StartTime = time_spinner_start,
|
|
|
|
EndTime = time_spinner_end,
|
|
|
|
Position = new Vector2(centre_x, centre_y)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
BeatmapInfo =
|
|
|
|
{
|
|
|
|
Difficulty = new BeatmapDifficulty(),
|
|
|
|
Ruleset = new OsuRuleset().RulesetInfo
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
|
|
|
|
|
|
|
p.OnLoadComplete += _ =>
|
|
|
|
{
|
|
|
|
p.ScoreProcessor.NewJudgement += result =>
|
|
|
|
{
|
|
|
|
if (currentPlayer == p) judgementResults.Add(result);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
LoadScreen(currentPlayer = p);
|
|
|
|
judgementResults.Clear();
|
|
|
|
});
|
|
|
|
|
|
|
|
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
|
|
|
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
|
|
|
}
|
|
|
|
|
|
|
|
private void performTest(List<ReplayFrame> frames)
|
|
|
|
{
|
|
|
|
loadPlayer(frames);
|
|
|
|
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
|
|
|
}
|
|
|
|
|
|
|
|
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
|
|
|
{
|
|
|
|
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
|
|
|
|
|
|
|
protected override bool PauseOnFocusLost => false;
|
|
|
|
|
|
|
|
public ScoreAccessibleReplayPlayer(Score score)
|
|
|
|
: base(score, new PlayerConfiguration
|
|
|
|
{
|
|
|
|
AllowPause = false,
|
|
|
|
ShowResults = false,
|
|
|
|
})
|
|
|
|
{
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|