public override IEnumerable <Issue> GetIssues(Beatmap beatmap) { List <ObservedDistance> observedDistances = new List <ObservedDistance>(); double ratioProblemThreshold = 15.0; double ratioWarningThreshold = 4.0; double ratioMinorThreshold = 2.0; double snapLeniencyMs = 5; double deltaTime; double distance; foreach (HitObject hitObject in beatmap.hitObjects) { HitObject nextObject = hitObject.Next(); // Ignore spinners, since they have no clear start or end. if (hitObject is Spinner || nextObject is Spinner || nextObject == null) { continue; } deltaTime = nextObject.GetPrevDeltaTime(); // Ignore objects ~1/2 or more beats apart (assuming 160 bpm), since they're unlikely to be an issue. if (deltaTime > 180) { continue; } distance = nextObject.GetPrevDistance(); if (distance < 20) { distance = 20; } List <ObservedDistance> sameSnappedDistances = observedDistances .FindAll(observedDistance => deltaTime <= observedDistance.deltaTime + snapLeniencyMs && deltaTime >= observedDistance.deltaTime - snapLeniencyMs && // Count the distances of sliders separately, as these have leniency unlike circles. observedDistance.hitObject is Slider == hitObject is Slider); ObservedDistance observedDistance = new ObservedDistance(deltaTime, distance, hitObject); observedDistances.Add(observedDistance); if (!sameSnappedDistances.Any() || distance / deltaTime < sameSnappedDistances.Max(obvDist => obvDist.distance / obvDist.deltaTime * Decay(hitObject, obvDist))) { continue; } if (distance <= beatmap.difficultySettings.GetCircleRadius() * 4) { continue; } if (sameSnappedDistances.Count < 3) { // Too few samples, probably going to get inaccurate readings. continue; } double expectedDistance = sameSnappedDistances.Sum(obvDist => obvDist.distance * Decay(hitObject, obvDist)) / sameSnappedDistances.Count(); double expectedDeltaTime = sameSnappedDistances.Sum(obvDist => obvDist.deltaTime * Decay(hitObject, obvDist)) / sameSnappedDistances.Count(); if (hitObject is Slider) { // Account for slider follow circle leniency. distance -= Math.Min(beatmap.difficultySettings.GetCircleRadius() * 3, distance); } double actualExpectedRatio = (distance / deltaTime) / (expectedDistance / expectedDeltaTime); if (actualExpectedRatio <= ratioMinorThreshold) { continue; } IEnumerable <string> comparisonTimestamps = sameSnappedDistances.Select(obvDist => Timestamp.Get(obvDist.hitObject, obvDist.hitObject.Next()) ).TakeLast(3); string templateName = "Minor"; if (actualExpectedRatio > ratioProblemThreshold) { templateName = "Problem"; } else if (actualExpectedRatio > ratioWarningThreshold) { templateName = "Warning"; } yield return(new Issue(GetTemplate(templateName), beatmap, Timestamp.Get(hitObject, nextObject), Math.Round(actualExpectedRatio * 10) / 10, string.Join("", comparisonTimestamps))); } }