1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 01:27:29 +08:00

Merge pull request #13874 from Naxesss/short-object-checks

Add object duration checks
This commit is contained in:
Dean Herbert 2021-07-13 19:01:02 +09:00 committed by GitHub
commit 2436ebb6d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 516 additions and 2 deletions

View File

@ -0,0 +1,145 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Checks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
{
[TestFixture]
public class CheckTooShortSlidersTest
{
private CheckTooShortSliders check;
[SetUp]
public void Setup()
{
check = new CheckTooShortSliders();
}
[Test]
public void TestLongSlider()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(100, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { slider });
}
[Test]
public void TestShortSlider()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(25, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { slider });
}
[Test]
public void TestTooShortSliderExpert()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(10, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { slider }, DifficultyRating.Expert);
}
[Test]
public void TestTooShortSlider()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(10, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertTooShort(new List<HitObject> { slider });
}
[Test]
public void TestTooShortSliderWithRepeats()
{
// Would be ok if we looked at the duration, but not if we look at the span duration.
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 2,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(10, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertTooShort(new List<HitObject> { slider });
}
private void assertOk(List<HitObject> hitObjects, DifficultyRating difficultyRating = DifficultyRating.Easy)
{
Assert.That(check.Run(getContext(hitObjects, difficultyRating)), Is.Empty);
}
private void assertTooShort(List<HitObject> hitObjects, DifficultyRating difficultyRating = DifficultyRating.Easy)
{
var issues = check.Run(getContext(hitObjects, difficultyRating)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckTooShortSliders.IssueTemplateTooShort);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, DifficultyRating difficultyRating)
{
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
}
}
}

View File

@ -0,0 +1,116 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Checks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
{
[TestFixture]
public class CheckTooShortSpinnersTest
{
private CheckTooShortSpinners check;
private BeatmapDifficulty difficulty;
[SetUp]
public void Setup()
{
check = new CheckTooShortSpinners();
difficulty = new BeatmapDifficulty();
}
[Test]
public void TestLongSpinner()
{
Spinner spinner = new Spinner { StartTime = 0, Duration = 4000 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertOk(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestShortSpinner()
{
Spinner spinner = new Spinner { StartTime = 0, Duration = 750 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertOk(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestVeryShortSpinner()
{
// Spinners at a certain duration only get 1000 points if approached by auto at a certain angle, making it difficult to determine.
Spinner spinner = new Spinner { StartTime = 0, Duration = 475 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertVeryShort(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestTooShortSpinner()
{
Spinner spinner = new Spinner { StartTime = 0, Duration = 400 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertTooShort(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestTooShortSpinnerVaryingOd()
{
const double duration = 450;
var difficultyLowOd = new BeatmapDifficulty { OverallDifficulty = 1 };
Spinner spinnerLowOd = new Spinner { StartTime = 0, Duration = duration };
spinnerLowOd.ApplyDefaults(new ControlPointInfo(), difficultyLowOd);
var difficultyHighOd = new BeatmapDifficulty { OverallDifficulty = 10 };
Spinner spinnerHighOd = new Spinner { StartTime = 0, Duration = duration };
spinnerHighOd.ApplyDefaults(new ControlPointInfo(), difficultyHighOd);
assertOk(new List<HitObject> { spinnerLowOd }, difficultyLowOd);
assertTooShort(new List<HitObject> { spinnerHighOd }, difficultyHighOd);
}
private void assertOk(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty);
}
private void assertVeryShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort);
}
private void assertTooShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = hitObjects,
BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty }
};
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,48 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit.Checks
{
public class CheckTooShortSliders : ICheck
{
/// <summary>
/// The shortest acceptable duration between the head and tail of the slider (so ignoring repeats).
/// </summary>
private const double span_duration_threshold = 125; // 240 BPM 1/2
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Too short sliders");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooShort(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
if (context.InterpretedDifficulty > DifficultyRating.Easy)
yield break;
foreach (var hitObject in context.Beatmap.HitObjects)
{
if (hitObject is Slider slider && slider.SpanDuration < span_duration_threshold)
yield return new IssueTemplateTooShort(this).Create(slider);
}
}
public class IssueTemplateTooShort : IssueTemplate
{
public IssueTemplateTooShort(ICheck check)
: base(check, IssueType.Problem, "This slider is too short ({0:0} ms), expected at least {1:0} ms.")
{
}
public Issue Create(Slider slider) => new Issue(slider, this, slider.SpanDuration, span_duration_threshold);
}
}
}

View File

@ -0,0 +1,61 @@
// 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.Collections.Generic;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit.Checks
{
public class CheckTooShortSpinners : ICheck
{
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Too short spinners");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooShort(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
// These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner.
// It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners.
double warningThreshold = 500 + (od < 5 ? (5 - od) * -21.8 : (od - 5) * 20); // Anything above this is always ok.
double problemThreshold = 450 + (od < 5 ? (5 - od) * -17 : (od - 5) * 17); // Anything below this is never ok.
foreach (var hitObject in context.Beatmap.HitObjects)
{
if (!(hitObject is Spinner spinner))
continue;
if (spinner.Duration < problemThreshold)
yield return new IssueTemplateTooShort(this).Create(spinner);
else if (spinner.Duration < warningThreshold)
yield return new IssueTemplateVeryShort(this).Create(spinner);
}
}
public class IssueTemplateTooShort : IssueTemplate
{
public IssueTemplateTooShort(ICheck check)
: base(check, IssueType.Problem, "This spinner is too short. Auto cannot achieve 1000 points on this.")
{
}
public Issue Create(Spinner spinner) => new Issue(spinner, this);
}
public class IssueTemplateVeryShort : IssueTemplate
{
public IssueTemplateVeryShort(ICheck check)
: base(check, IssueType.Warning, "This spinner may be too short. Ensure auto can achieve 1000 points on this.")
{
}
public Issue Create(Spinner spinner) => new Issue(spinner, this);
}
}
}

View File

@ -15,10 +15,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
// Compose
new CheckOffscreenObjects(),
new CheckTooShortSpinners(),
// Spread
new CheckTimeDistanceEquality(),
new CheckLowDiffOverlaps()
new CheckLowDiffOverlaps(),
new CheckTooShortSliders(),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)

View File

@ -0,0 +1,94 @@
// 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.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckZeroLengthObjectsTest
{
private CheckZeroLengthObjects check;
[SetUp]
public void Setup()
{
check = new CheckZeroLengthObjects();
}
[Test]
public void TestCircle()
{
assertOk(new List<HitObject>
{
new HitCircle { StartTime = 1000, Position = new Vector2(0, 0) }
});
}
[Test]
public void TestRegularSlider()
{
assertOk(new List<HitObject>
{
getSliderMock(1000).Object
});
}
[Test]
public void TestZeroLengthSlider()
{
assertZeroLength(new List<HitObject>
{
getSliderMock(0).Object
});
}
[Test]
public void TestNegativeLengthSlider()
{
assertZeroLength(new List<HitObject>
{
getSliderMock(-1000).Object
});
}
private Mock<Slider> getSliderMock(double duration)
{
var mockSlider = new Mock<Slider>();
mockSlider.As<IHasDuration>().Setup(d => d.Duration).Returns(duration);
return mockSlider;
}
private void assertOk(List<HitObject> hitObjects)
{
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
}
private void assertZeroLength(List<HitObject> hitObjects)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckZeroLengthObjects.IssueTemplateZeroLength);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
{
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Edit
// Compose
new CheckUnsnappedObjects(),
new CheckConcurrentObjects()
new CheckConcurrentObjects(),
new CheckZeroLengthObjects(),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)

View File

@ -0,0 +1,47 @@
// 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.Collections.Generic;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckZeroLengthObjects : ICheck
{
/// <summary>
/// The duration can be this low before being treated as having no length, in case of precision errors. Unit is milliseconds.
/// </summary>
private const double leniency = 0.5d;
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Zero-length hitobjects");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateZeroLength(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
foreach (var hitObject in context.Beatmap.HitObjects)
{
if (!(hitObject is IHasDuration hasDuration))
continue;
if (hasDuration.Duration < leniency)
yield return new IssueTemplateZeroLength(this).Create(hitObject, hasDuration.Duration);
}
}
public class IssueTemplateZeroLength : IssueTemplate
{
public IssueTemplateZeroLength(ICheck check)
: base(check, IssueType.Problem, "{0} has a duration of {1:0}.")
{
}
public Issue Create(HitObject hitobject, double duration) => new Issue(hitobject, this, hitobject.GetType(), duration);
}
}
}