mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-15 03:20:27 +00:00
This change pulls back a significant degree of overspecialisation and rigidity in the class structure of `HitWindows` to make subsequent changes to hit windows, whose purpose is to improve replay playback accuracy, possible to do cleanly. Notably: - `HitWindows` is full abstract now. In a few use cases, and as a reference for ruleset implementors, `DefaultHitWindows` is provided as a separate class instead. This fixes the weirdness wherein `HitWindows` always declared 6 fields for result types but some of them would never be set to a non-zero value or read. - `HitWindow.GetRanges()` is deleted because it is overspecialised and prevents being able to adjust hitwindows by ±0.5ms cleanly which will be required later. The fallout of this is that the assertion that used `GetRanges()` in the `HitWindows` ctor must use something else now, and the closest thing to it was `GetAllAvailableWindows()`, which didn't return the miss window - so I made it return the miss window and fixed the one consumer that didn't want it (bar hit error meter) to skip it. - Diff also contains some clean-up around `DifficultyRange` to unify handling of it.
232 lines
9.0 KiB
C#
232 lines
9.0 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.
|
|
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using NUnit.Framework;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Audio;
|
|
using osu.Framework.Timing;
|
|
using osu.Framework.Utils;
|
|
using osu.Game.Audio;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Beatmaps.ControlPoints;
|
|
using osu.Game.Beatmaps.Legacy;
|
|
using osu.Game.Rulesets;
|
|
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.Osu.Objects;
|
|
using osu.Game.Rulesets.Scoring;
|
|
using osu.Game.Rulesets.UI;
|
|
using osu.Game.Storyboards;
|
|
using osuTK;
|
|
using osuTK.Input;
|
|
|
|
namespace osu.Game.Tests.Visual.Gameplay
|
|
{
|
|
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
|
|
{
|
|
protected override bool AllowBackwardsSeeks => true;
|
|
|
|
private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
|
|
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
|
|
|
private Beatmap beatmap = null!;
|
|
|
|
[Resolved]
|
|
private AudioManager audio { get; set; } = null!;
|
|
|
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
|
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
|
|
|
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
|
{
|
|
ControlPointInfo controlPointInfo = new LegacyControlPointInfo();
|
|
|
|
beatmap = new Beatmap
|
|
{
|
|
BeatmapInfo = new BeatmapInfo
|
|
{
|
|
Difficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
|
|
Ruleset = ruleset
|
|
},
|
|
ControlPointInfo = controlPointInfo
|
|
};
|
|
|
|
const double start_offset = 8000;
|
|
const double spacing = 2000;
|
|
|
|
// intentionally start objects a bit late so we can test the case of no alive objects.
|
|
double t = start_offset;
|
|
|
|
beatmap.HitObjects.AddRange(new HitObject[]
|
|
{
|
|
new HitCircle
|
|
{
|
|
HitWindows = new DefaultHitWindows(),
|
|
StartTime = t += spacing,
|
|
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
|
},
|
|
new HitCircle
|
|
{
|
|
HitWindows = new DefaultHitWindows(),
|
|
StartTime = t += spacing,
|
|
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }
|
|
},
|
|
new HitCircle
|
|
{
|
|
HitWindows = new DefaultHitWindows(),
|
|
StartTime = t += spacing,
|
|
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) },
|
|
},
|
|
new HitCircle
|
|
{
|
|
HitWindows = new DefaultHitWindows(),
|
|
StartTime = t += spacing,
|
|
},
|
|
new Slider
|
|
{
|
|
HitWindows = new DefaultHitWindows(),
|
|
StartTime = t += spacing,
|
|
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
|
|
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
|
|
},
|
|
});
|
|
|
|
// Add a change in volume halfway through final slider.
|
|
controlPointInfo.Add(t, new SampleControlPoint
|
|
{
|
|
SampleBank = "normal",
|
|
SampleVolume = 20,
|
|
});
|
|
|
|
return beatmap;
|
|
}
|
|
|
|
public override void SetUpSteps()
|
|
{
|
|
base.SetUpSteps();
|
|
|
|
AddStep("Add trigger source", () => Player.DrawableRuleset.FrameStableComponents.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer)));
|
|
}
|
|
|
|
[Test]
|
|
public void TestCorrectHitObject()
|
|
{
|
|
waitForAliveObjectIndex(null);
|
|
checkValidObjectIndex(0);
|
|
|
|
seekBeforeIndex(0);
|
|
waitForAliveObjectIndex(0);
|
|
checkValidObjectIndex(0);
|
|
|
|
AddAssert("first object not hit", () => getNextAliveObject()?.Entry?.Result?.HasResult != true);
|
|
|
|
AddStep("hit first object", () =>
|
|
{
|
|
var next = getNextAliveObject();
|
|
|
|
if (next != null)
|
|
{
|
|
Debug.Assert(next.Entry?.Result?.HasResult != true);
|
|
|
|
InputManager.MoveMouseTo(next.ScreenSpaceDrawQuad.Centre);
|
|
InputManager.Click(MouseButton.Left);
|
|
}
|
|
});
|
|
|
|
AddAssert("first object hit", () => getNextAliveObject()?.Entry?.Result?.HasResult == true);
|
|
|
|
// next object is too far away, so we still use the already hit object.
|
|
checkValidObjectIndex(0);
|
|
|
|
// still too far away.
|
|
seekBeforeIndex(1, 400);
|
|
checkValidObjectIndex(0);
|
|
|
|
// Still object 1 as it's not hit yet.
|
|
seekBeforeIndex(1);
|
|
waitForAliveObjectIndex(1);
|
|
checkValidObjectIndex(1);
|
|
|
|
seekBeforeIndex(2);
|
|
waitForAliveObjectIndex(2);
|
|
checkValidObjectIndex(2);
|
|
|
|
// test rewinding
|
|
seekBeforeIndex(1);
|
|
waitForAliveObjectIndex(1);
|
|
checkValidObjectIndex(1);
|
|
|
|
seekBeforeIndex(1, 400);
|
|
checkValidObjectIndex(0);
|
|
|
|
seekBeforeIndex(3);
|
|
waitForAliveObjectIndex(3);
|
|
checkValidObjectIndex(3);
|
|
|
|
seekBeforeIndex(4);
|
|
waitForAliveObjectIndex(4);
|
|
|
|
// Even before the object, we should prefer the first nested object's sample.
|
|
// This is because the (parent) object will only play its sample at the final EndTime.
|
|
AddAssert("check valid object is slider's first nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.First()));
|
|
|
|
AddStep("seek to just before slider ends", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[4].GetEndTime() - 100));
|
|
waitForCatchUp();
|
|
AddUntilStep("wait until valid object is slider's last nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.Last()));
|
|
|
|
// After we get far enough away, the samples of the object itself should be used, not any nested object.
|
|
AddStep("seek to further after slider", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[4].GetEndTime() + 1000));
|
|
waitForCatchUp();
|
|
AddUntilStep("wait until valid object is slider itself", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4]));
|
|
|
|
AddStep("Seek into future", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000));
|
|
waitForCatchUp();
|
|
waitForAliveObjectIndex(null);
|
|
checkValidObjectIndex(4);
|
|
}
|
|
|
|
private void seekBeforeIndex(int index, double amount = 100)
|
|
{
|
|
AddStep($"seek to {amount} ms before object {index}", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[index].StartTime - amount));
|
|
waitForCatchUp();
|
|
}
|
|
|
|
private void waitForCatchUp() =>
|
|
AddUntilStep("wait for frame stable clock to catch up", () => Precision.AlmostEquals(Player.GameplayClockContainer.CurrentTime, Player.DrawableRuleset.FrameStableClock.CurrentTime));
|
|
|
|
private void waitForAliveObjectIndex(int? index)
|
|
{
|
|
if (index == null)
|
|
AddUntilStep("wait for no alive objects", getNextAliveObject, () => Is.Null);
|
|
else
|
|
AddUntilStep($"wait for next alive to be {index}", () => getNextAliveObject()?.HitObject, () => Is.EqualTo(beatmap.HitObjects[index.Value]));
|
|
}
|
|
|
|
private void checkValidObjectIndex(int index) =>
|
|
AddAssert($"check object at index {index} is correct", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index]));
|
|
|
|
private DrawableHitObject? getNextAliveObject() =>
|
|
Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault();
|
|
|
|
[Test]
|
|
public void TestSampleTriggering()
|
|
{
|
|
AddRepeatStep("trigger sample", () => sampleTriggerSource.Play(), 10);
|
|
}
|
|
|
|
public partial class TestGameplaySampleTriggerSource : GameplaySampleTriggerSource
|
|
{
|
|
public TestGameplaySampleTriggerSource(HitObjectContainer hitObjectContainer)
|
|
: base(hitObjectContainer)
|
|
{
|
|
}
|
|
|
|
public new HitObject? GetMostValidObject() => base.GetMostValidObject();
|
|
}
|
|
}
|
|
}
|