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); }