// Copyright (c) ppy Pty Ltd . 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.Checks.Components; using osu.Game.Rulesets.Osu.Objects; using osuTK; namespace osu.Game.Rulesets.Osu.Edit.Checks { public class CheckOffscreenObjects : ICheck { // A close approximation for the bounding box of the screen in gameplay on 4:3 aspect ratio. // Uses gameplay space coordinates (512 x 384 playfield / 640 x 480 screen area). // See https://github.com/ppy/osu/pull/12361#discussion_r612199777 for reference. private const int min_x = -67; private const int min_y = -60; private const int max_x = 579; private const int max_y = 428; // The amount of milliseconds to step through a slider path at a time // (higher = more performant, but higher false-negative chance). private const int path_step_size = 5; public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Offscreen hitobjects"); public IEnumerable PossibleTemplates => new IssueTemplate[] { new IssueTemplateOffscreenCircle(this), new IssueTemplateOffscreenSlider(this) }; public IEnumerable Run(WorkingBeatmap workingBeatmap) { foreach (var hitobject in workingBeatmap.Beatmap.HitObjects) { switch (hitobject) { case Slider slider: { foreach (var issue in sliderIssues(slider)) yield return issue; break; } case HitCircle circle: { if (isOffscreen(circle.StackedPosition, circle.Radius)) yield return new IssueTemplateOffscreenCircle(this).Create(circle); break; } } } } /// /// Steps through points on the slider to ensure the entire path is on-screen. /// Returns at most one issue. /// /// The slider whose path to check. /// private IEnumerable sliderIssues(Slider slider) { for (int i = 0; i < slider.Distance; i += path_step_size) { double progress = i / slider.Distance; Vector2 position = slider.StackedPositionAt(progress); if (!isOffscreen(position, slider.Radius)) continue; // `SpanDuration` ensures we don't include reverses. double time = slider.StartTime + progress * slider.SpanDuration; yield return new IssueTemplateOffscreenSlider(this).Create(slider, time); yield break; } // Above loop may skip the last position in the slider due to step size. if (!isOffscreen(slider.StackedEndPosition, slider.Radius)) yield break; yield return new IssueTemplateOffscreenSlider(this).Create(slider, slider.EndTime); } private bool isOffscreen(Vector2 position, double radius) { return position.X - radius < min_x || position.X + radius > max_x || position.Y - radius < min_y || position.Y + radius > max_y; } public class IssueTemplateOffscreenCircle : IssueTemplate { public IssueTemplateOffscreenCircle(ICheck check) : base(check, IssueType.Problem, "This circle goes offscreen on a 4:3 aspect ratio.") { } public Issue Create(HitCircle circle) => new Issue(circle, this); } public class IssueTemplateOffscreenSlider : IssueTemplate { public IssueTemplateOffscreenSlider(ICheck check) : base(check, IssueType.Problem, "This slider goes offscreen here on a 4:3 aspect ratio.") { } public Issue Create(Slider slider, double offscreenTime) => new Issue(slider, this) { Time = offscreenTime }; } } }