mirror of
https://github.com/osukey/osukey.git
synced 2025-08-03 14:46:38 +09:00
Merge pull request #13606 from Naxesss/low-diff-checks
Add low difficulty spacing checks
This commit is contained in:
@ -0,0 +1,260 @@
|
||||
// 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.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
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 CheckLowDiffOverlapsTest
|
||||
{
|
||||
private CheckLowDiffOverlaps check;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckLowDiffOverlaps();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoOverlapFarApart()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(200, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoOverlapClose()
|
||||
{
|
||||
assertShouldProbablyOverlap(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 167, Position = new Vector2(200, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoOverlapTooClose()
|
||||
{
|
||||
assertShouldOverlap(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(200, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoOverlapTooCloseExpert()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(200, 0) }
|
||||
}
|
||||
}, DifficultyRating.Expert);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlapClose()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 167, Position = new Vector2(20, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlapFarApart()
|
||||
{
|
||||
assertShouldNotOverlap(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(20, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAlmostOverlapFarApart()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
// Default circle diameter is 128 px, but part of that is the fade/border of the circle.
|
||||
// We want this to only be a problem when it actually looks like an overlap.
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(125, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAlmostNotOverlapFarApart()
|
||||
{
|
||||
assertShouldNotOverlap(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(110, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlapFarApartExpert()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(20, 0) }
|
||||
}
|
||||
}, DifficultyRating.Expert);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlapTooFarApart()
|
||||
{
|
||||
// Far apart enough to where the objects are not visible at the same time, and so overlapping is fine.
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 2000, Position = new Vector2(20, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderTailOverlapFarApart()
|
||||
{
|
||||
assertShouldNotOverlap(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 0, endTime: 500, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(120, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderTailOverlapClose()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 0, endTime: 900, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(120, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderTailNoOverlapFarApart()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 0, endTime: 500, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(300, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderTailNoOverlapClose()
|
||||
{
|
||||
// If these were circles they would need to overlap, but overlapping with slider tails is not required.
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 0, endTime: 900, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(300, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mock<Slider> getSliderMock(double startTime, double endTime, Vector2 startPosition, Vector2 endPosition)
|
||||
{
|
||||
var mockSlider = new Mock<Slider>();
|
||||
mockSlider.SetupGet(s => s.StartTime).Returns(startTime);
|
||||
mockSlider.SetupGet(s => s.Position).Returns(startPosition);
|
||||
mockSlider.SetupGet(s => s.EndPosition).Returns(endPosition);
|
||||
mockSlider.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
|
||||
|
||||
return mockSlider;
|
||||
}
|
||||
|
||||
private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating = DifficultyRating.Easy)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertShouldProbablyOverlap(IBeatmap beatmap, int count = 1)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldProbablyOverlap));
|
||||
}
|
||||
|
||||
private void assertShouldOverlap(IBeatmap beatmap, int count = 1)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldOverlap));
|
||||
}
|
||||
|
||||
private void assertShouldNotOverlap(IBeatmap beatmap, int count = 1)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldNotOverlap));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,324 @@
|
||||
// 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.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
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 CheckTimeDistanceEqualityTest
|
||||
{
|
||||
private CheckTimeDistanceEquality check;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckTimeDistanceEquality();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesEquidistant()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(100, 0) },
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(150, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesOneSlightlyOff()
|
||||
{
|
||||
assertWarning(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(80, 0) }, // Distance a quite low compared to previous.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(130, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesOneOff()
|
||||
{
|
||||
assertProblem(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Twice the regular spacing.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesTwoOff()
|
||||
{
|
||||
assertProblem(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Twice the regular spacing.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(250, 0) } // Also twice the regular spacing.
|
||||
}
|
||||
}, count: 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesStacked()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(50, 0) }, // Stacked, is fine.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesStacking()
|
||||
{
|
||||
assertWarning(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(50, 0), StackHeight = 1 },
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(50, 0), StackHeight = 2 },
|
||||
new HitCircle { StartTime = 2000, Position = new Vector2(50, 0), StackHeight = 3 },
|
||||
new HitCircle { StartTime = 2500, Position = new Vector2(50, 0), StackHeight = 4 }, // Ends up far from (50; 0), causing irregular spacing.
|
||||
new HitCircle { StartTime = 3000, Position = new Vector2(100, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesHalfStack()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(55, 0) }, // Basically stacked, so is fine.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(105, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesPartialOverlap()
|
||||
{
|
||||
assertProblem(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(65, 0) }, // Really low distance compared to previous.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(115, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesSlightlyDifferent()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
// Does not need to be perfect, as long as the distance is approximately correct it's sight-readable.
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(52, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(97, 0) },
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(165, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesSlowlyChanging()
|
||||
{
|
||||
const float multiplier = 1.2f;
|
||||
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(50 + 50 * multiplier, 0) },
|
||||
// This gap would be a warning if it weren't for the previous pushing the average spacing up.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(50 + 50 * multiplier + 50 * multiplier * multiplier, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesQuicklyChanging()
|
||||
{
|
||||
const float multiplier = 1.6f;
|
||||
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(50 + 50 * multiplier, 0) }, // Warning
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(50 + 50 * multiplier + 50 * multiplier * multiplier, 0) } // Problem
|
||||
}
|
||||
};
|
||||
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(2));
|
||||
Assert.That(issues.First().Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingWarning);
|
||||
Assert.That(issues.Last().Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingProblem);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesTooFarApart()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 4000, Position = new Vector2(200, 0) }, // 2 seconds apart from previous, so can start from wherever.
|
||||
new HitCircle { StartTime = 4500, Position = new Vector2(250, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesOneOffExpert()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Jumps are allowed in higher difficulties.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }
|
||||
}
|
||||
}, DifficultyRating.Expert);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSpinner()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new Spinner { StartTime = 500, EndTime = 1000 }, // Distance to and from the spinner should be ignored. If it isn't this should give a problem.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) },
|
||||
new HitCircle { StartTime = 2000, Position = new Vector2(150, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliders()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
getSliderMock(startTime: 1000, endTime: 1500, startPosition: new Vector2(100, 0), endPosition: new Vector2(150, 0)).Object,
|
||||
getSliderMock(startTime: 2000, endTime: 2500, startPosition: new Vector2(200, 0), endPosition: new Vector2(250, 0)).Object,
|
||||
new HitCircle { StartTime = 2500, Position = new Vector2(300, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSlidersOneOff()
|
||||
{
|
||||
assertProblem(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
getSliderMock(startTime: 1000, endTime: 1500, startPosition: new Vector2(100, 0), endPosition: new Vector2(150, 0)).Object,
|
||||
getSliderMock(startTime: 2000, endTime: 2500, startPosition: new Vector2(250, 0), endPosition: new Vector2(300, 0)).Object, // Twice the spacing.
|
||||
new HitCircle { StartTime = 2500, Position = new Vector2(300, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mock<Slider> getSliderMock(double startTime, double endTime, Vector2 startPosition, Vector2 endPosition)
|
||||
{
|
||||
var mockSlider = new Mock<Slider>();
|
||||
mockSlider.SetupGet(s => s.StartTime).Returns(startTime);
|
||||
mockSlider.SetupGet(s => s.Position).Returns(startPosition);
|
||||
mockSlider.SetupGet(s => s.EndPosition).Returns(endPosition);
|
||||
mockSlider.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
|
||||
|
||||
return mockSlider;
|
||||
}
|
||||
|
||||
private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating = DifficultyRating.Easy)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertWarning(IBeatmap beatmap, int count = 1)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingWarning));
|
||||
}
|
||||
|
||||
private void assertProblem(IBeatmap beatmap, int count = 1)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingProblem));
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
109
osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs
Normal file
109
osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs
Normal file
@ -0,0 +1,109 @@
|
||||
// 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.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
{
|
||||
public class CheckLowDiffOverlaps : ICheck
|
||||
{
|
||||
// For the lowest difficulties, the osu! Ranking Criteria encourages overlapping ~180 BPM 1/2, but discourages ~180 BPM 1/1.
|
||||
private const double should_overlap_threshold = 150; // 200 BPM 1/2
|
||||
private const double should_probably_overlap_threshold = 175; // 170 BPM 1/2
|
||||
private const double should_not_overlap_threshold = 250; // 120 BPM 1/2 = 240 BPM 1/1
|
||||
|
||||
/// <summary>
|
||||
/// Objects need to overlap this much before being treated as an overlap, else it may just be the borders slightly touching.
|
||||
/// </summary>
|
||||
private const double overlap_leniency = 5;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Missing or unexpected overlaps");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateShouldOverlap(this),
|
||||
new IssueTemplateShouldProbablyOverlap(this),
|
||||
new IssueTemplateShouldNotOverlap(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
// TODO: This should also apply to *lowest difficulty* Normals - they are skipped for now.
|
||||
if (context.InterpretedDifficulty > DifficultyRating.Easy)
|
||||
yield break;
|
||||
|
||||
var hitObjects = context.Beatmap.HitObjects;
|
||||
|
||||
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||
{
|
||||
if (!(hitObjects[i] is OsuHitObject hitObject) || hitObject is Spinner)
|
||||
continue;
|
||||
|
||||
if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner)
|
||||
continue;
|
||||
|
||||
var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
|
||||
if (deltaTime >= hitObject.TimeFadeIn + hitObject.TimePreempt)
|
||||
// The objects are not visible at the same time (without mods), hence skipping.
|
||||
continue;
|
||||
|
||||
var distanceSq = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthSquared;
|
||||
var diameter = (hitObject.Radius - overlap_leniency) * 2;
|
||||
var diameterSq = diameter * diameter;
|
||||
|
||||
bool areOverlapping = distanceSq < diameterSq;
|
||||
|
||||
// Slider ends do not need to be overlapped because of slider leniency.
|
||||
if (!areOverlapping && !(hitObject is Slider))
|
||||
{
|
||||
if (deltaTime < should_overlap_threshold)
|
||||
yield return new IssueTemplateShouldOverlap(this).Create(deltaTime, hitObject, nextHitObject);
|
||||
else if (deltaTime < should_probably_overlap_threshold)
|
||||
yield return new IssueTemplateShouldProbablyOverlap(this).Create(deltaTime, hitObject, nextHitObject);
|
||||
}
|
||||
|
||||
if (areOverlapping && deltaTime > should_not_overlap_threshold)
|
||||
yield return new IssueTemplateShouldNotOverlap(this).Create(deltaTime, hitObject, nextHitObject);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class IssueTemplateOverlap : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateOverlap(ICheck check, IssueType issueType, string unformattedMessage)
|
||||
: base(check, issueType, unformattedMessage)
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(double deltaTime, params HitObject[] hitObjects) => new Issue(hitObjects, this, deltaTime);
|
||||
}
|
||||
|
||||
public class IssueTemplateShouldOverlap : IssueTemplateOverlap
|
||||
{
|
||||
public IssueTemplateShouldOverlap(ICheck check)
|
||||
: base(check, IssueType.Problem, "These are {0} ms apart and so should be overlapping.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateShouldProbablyOverlap : IssueTemplateOverlap
|
||||
{
|
||||
public IssueTemplateShouldProbablyOverlap(ICheck check)
|
||||
: base(check, IssueType.Warning, "These are {0} ms apart and so should probably be overlapping.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateShouldNotOverlap : IssueTemplateOverlap
|
||||
{
|
||||
public IssueTemplateShouldNotOverlap(ICheck check)
|
||||
: base(check, IssueType.Problem, "These are {0} ms apart and so should NOT be overlapping.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
179
osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs
Normal file
179
osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs
Normal file
@ -0,0 +1,179 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
{
|
||||
public class CheckTimeDistanceEquality : ICheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Two objects this many ms apart or more are skipped. (200 BPM 2/1)
|
||||
/// </summary>
|
||||
private const double pattern_lifetime = 600;
|
||||
|
||||
/// <summary>
|
||||
/// Two objects this distance apart or less are skipped.
|
||||
/// </summary>
|
||||
private const double stack_leniency = 12;
|
||||
|
||||
/// <summary>
|
||||
/// How long an observation is relevant for comparison. (120 BPM 8/1)
|
||||
/// </summary>
|
||||
private const double observation_lifetime = 4000;
|
||||
|
||||
/// <summary>
|
||||
/// How different two delta times can be to still be compared. (240 BPM 1/16)
|
||||
/// </summary>
|
||||
private const double similar_time_leniency = 16;
|
||||
|
||||
/// <summary>
|
||||
/// How many pixels are subtracted from the difference between current and expected distance.
|
||||
/// </summary>
|
||||
private const double distance_leniency_absolute_warning = 10;
|
||||
|
||||
/// <summary>
|
||||
/// How much of the current distance that the difference can make out.
|
||||
/// </summary>
|
||||
private const double distance_leniency_percent_warning = 0.15;
|
||||
|
||||
private const double distance_leniency_absolute_problem = 20;
|
||||
private const double distance_leniency_percent_problem = 0.3;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Object too close or far away from previous");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateIrregularSpacingProblem(this),
|
||||
new IssueTemplateIrregularSpacingWarning(this)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Represents an observation of the time and distance between two objects.
|
||||
/// </summary>
|
||||
private readonly struct ObservedTimeDistance
|
||||
{
|
||||
public readonly double ObservationTime;
|
||||
public readonly double DeltaTime;
|
||||
public readonly double Distance;
|
||||
|
||||
public ObservedTimeDistance(double observationTime, double deltaTime, double distance)
|
||||
{
|
||||
ObservationTime = observationTime;
|
||||
DeltaTime = deltaTime;
|
||||
Distance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
if (context.InterpretedDifficulty > DifficultyRating.Normal)
|
||||
yield break;
|
||||
|
||||
var prevObservedTimeDistances = new List<ObservedTimeDistance>();
|
||||
var hitObjects = context.Beatmap.HitObjects;
|
||||
|
||||
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||
{
|
||||
if (!(hitObjects[i] is OsuHitObject hitObject) || hitObject is Spinner)
|
||||
continue;
|
||||
|
||||
if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner)
|
||||
continue;
|
||||
|
||||
var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
|
||||
|
||||
// Ignore objects that are far enough apart in time to not be considered the same pattern.
|
||||
if (deltaTime > pattern_lifetime)
|
||||
continue;
|
||||
|
||||
// Relying on FastInvSqrt is probably good enough here. We'll be taking the difference between distances later, hence square not being sufficient.
|
||||
var distance = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthFast;
|
||||
|
||||
// Ignore stacks and half-stacks, as these are close enough to where they can't be confused for being time-distanced.
|
||||
if (distance < stack_leniency)
|
||||
continue;
|
||||
|
||||
var observedTimeDistance = new ObservedTimeDistance(nextHitObject.StartTime, deltaTime, distance);
|
||||
var expectedDistance = getExpectedDistance(prevObservedTimeDistances, observedTimeDistance);
|
||||
|
||||
if (expectedDistance == 0)
|
||||
{
|
||||
// There was nothing relevant to compare to.
|
||||
prevObservedTimeDistances.Add(observedTimeDistance);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((Math.Abs(expectedDistance - distance) - distance_leniency_absolute_problem) / distance > distance_leniency_percent_problem)
|
||||
yield return new IssueTemplateIrregularSpacingProblem(this).Create(expectedDistance, distance, hitObject, nextHitObject);
|
||||
else if ((Math.Abs(expectedDistance - distance) - distance_leniency_absolute_warning) / distance > distance_leniency_percent_warning)
|
||||
yield return new IssueTemplateIrregularSpacingWarning(this).Create(expectedDistance, distance, hitObject, nextHitObject);
|
||||
else
|
||||
{
|
||||
// We use `else` here to prevent issues from cascading; an object spaced too far could cause regular spacing to be considered "too short" otherwise.
|
||||
prevObservedTimeDistances.Add(observedTimeDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double getExpectedDistance(IEnumerable<ObservedTimeDistance> prevObservedTimeDistances, ObservedTimeDistance observedTimeDistance)
|
||||
{
|
||||
var observations = prevObservedTimeDistances.Count();
|
||||
|
||||
int count = 0;
|
||||
double sum = 0;
|
||||
|
||||
// Looping this in reverse allows us to break before going through all elements, as we're only interested in the most recent ones.
|
||||
for (int i = observations - 1; i >= 0; --i)
|
||||
{
|
||||
var prevObservedTimeDistance = prevObservedTimeDistances.ElementAt(i);
|
||||
|
||||
// Only consider observations within the last few seconds - this allows the map to build spacing up/down over time, but prevents it from being too sudden.
|
||||
if (observedTimeDistance.ObservationTime - prevObservedTimeDistance.ObservationTime > observation_lifetime)
|
||||
break;
|
||||
|
||||
// Only consider observations which have a similar time difference - this leniency allows handling of multi-BPM maps which speed up/down slowly.
|
||||
if (Math.Abs(observedTimeDistance.DeltaTime - prevObservedTimeDistance.DeltaTime) > similar_time_leniency)
|
||||
break;
|
||||
|
||||
count += 1;
|
||||
sum += prevObservedTimeDistance.Distance / Math.Max(prevObservedTimeDistance.DeltaTime, 1);
|
||||
}
|
||||
|
||||
return sum / Math.Max(count, 1) * observedTimeDistance.DeltaTime;
|
||||
}
|
||||
|
||||
public abstract class IssueTemplateIrregularSpacing : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateIrregularSpacing(ICheck check, IssueType issueType)
|
||||
: base(check, issueType, "Expected {0:0} px spacing like previous objects, currently {1:0}.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(double expected, double actual, params HitObject[] hitObjects) => new Issue(hitObjects, this, expected, actual);
|
||||
}
|
||||
|
||||
public class IssueTemplateIrregularSpacingProblem : IssueTemplateIrregularSpacing
|
||||
{
|
||||
public IssueTemplateIrregularSpacingProblem(ICheck check)
|
||||
: base(check, IssueType.Problem)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateIrregularSpacingWarning : IssueTemplateIrregularSpacing
|
||||
{
|
||||
public IssueTemplateIrregularSpacingWarning(ICheck check)
|
||||
: base(check, IssueType.Warning)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,7 +13,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
new CheckOffscreenObjects()
|
||||
// Compose
|
||||
new CheckOffscreenObjects(),
|
||||
|
||||
// Spread
|
||||
new CheckTimeDistanceEquality(),
|
||||
new CheckLowDiffOverlaps()
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
|
Reference in New Issue
Block a user