From 4022a8b06cc056b210047f55bff4b3a59d7c24e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Jun 2024 10:11:04 +0200 Subject: [PATCH] Implement automatic break period generation --- osu.Game/Beatmaps/Timing/BreakPeriod.cs | 32 ++++++++-- osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs | 14 ++-- .../Components/Timeline/TimelineBreak.cs | 4 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 +- .../Screens/Edit/EditorBeatmapProcessor.cs | 64 +++++++++++++++++++ osu.Game/Screens/Edit/ManualBreakPeriod.cs | 15 +++++ 6 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 osu.Game/Screens/Edit/EditorBeatmapProcessor.cs create mode 100644 osu.Game/Screens/Edit/ManualBreakPeriod.cs diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index f16a3c27a1..89f0fd6a55 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs @@ -6,22 +6,39 @@ using osu.Game.Screens.Play; namespace osu.Game.Beatmaps.Timing { - public readonly struct BreakPeriod : IEquatable + public record BreakPeriod { + /// + /// The minimum gap between the start of the break and the previous object. + /// + public const double GAP_BEFORE_BREAK = 200; + + /// + /// The minimum gap between the end of the break and the next object. + /// Based on osu! preempt time at AR=10. + /// See also: https://github.com/ppy/osu/issues/14330#issuecomment-1002158551 + /// + public const double GAP_AFTER_BREAK = 450; + /// /// The minimum duration required for a break to have any effect. /// public const double MIN_BREAK_DURATION = 650; + /// + /// The minimum required duration of a gap between two objects such that a break can be placed between them. + /// + public const double MIN_GAP_DURATION = GAP_BEFORE_BREAK + MIN_BREAK_DURATION + GAP_AFTER_BREAK; + /// /// The break start time. /// - public double StartTime { get; init; } + public double StartTime { get; } /// /// The break end time. /// - public double EndTime { get; init; } + public double EndTime { get; } /// /// The break duration. @@ -51,8 +68,13 @@ namespace osu.Game.Beatmaps.Timing /// Whether the time falls within this . public bool Contains(double time) => time >= StartTime && time <= EndTime - BreakOverlay.BREAK_FADE_DURATION; - public bool Equals(BreakPeriod other) => StartTime.Equals(other.StartTime) && EndTime.Equals(other.EndTime); - public override bool Equals(object? obj) => obj is BreakPeriod other && Equals(other); + public bool Intersects(BreakPeriod other) => StartTime <= other.EndTime && EndTime >= other.StartTime; + + public virtual bool Equals(BreakPeriod? other) => + other != null + && StartTime == other.StartTime + && EndTime == other.EndTime; + public override int GetHashCode() => HashCode.Combine(StartTime, EndTime); } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs index 0842ff5453..f7be36beab 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBreaks.cs @@ -13,13 +13,7 @@ namespace osu.Game.Rulesets.Edit.Checks { // Breaks may be off by 1 ms. private const int leniency_threshold = 1; - private const double minimum_gap_before_break = 200; - // Break end time depends on the upcoming object's pre-empt time. - // As things stand, "pre-empt time" is only defined for osu! standard - // This is a generic value representing AR=10 - // Relevant: https://github.com/ppy/osu/issues/14330#issuecomment-1002158551 - private const double min_end_threshold = 450; public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Events, "Breaks not achievable using the editor"); public IEnumerable PossibleTemplates => new IssueTemplate[] @@ -45,8 +39,8 @@ namespace osu.Game.Rulesets.Edit.Checks if (previousObjectEndTimeIndex >= 0) { double gapBeforeBreak = breakPeriod.StartTime - endTimes[previousObjectEndTimeIndex]; - if (gapBeforeBreak < minimum_gap_before_break - leniency_threshold) - yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, minimum_gap_before_break - gapBeforeBreak); + if (gapBeforeBreak < BreakPeriod.GAP_BEFORE_BREAK - leniency_threshold) + yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, BreakPeriod.GAP_BEFORE_BREAK - gapBeforeBreak); } int nextObjectStartTimeIndex = startTimes.BinarySearch(breakPeriod.EndTime); @@ -55,8 +49,8 @@ namespace osu.Game.Rulesets.Edit.Checks if (nextObjectStartTimeIndex < startTimes.Count) { double gapAfterBreak = startTimes[nextObjectStartTimeIndex] - breakPeriod.EndTime; - if (gapAfterBreak < min_end_threshold - leniency_threshold) - yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - gapAfterBreak); + if (gapAfterBreak < BreakPeriod.GAP_AFTER_BREAK - leniency_threshold) + yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, BreakPeriod.GAP_AFTER_BREAK - gapAfterBreak); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs index cec4b9b659..b9651ccd81 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBreak.cs @@ -54,14 +54,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Break = { BindTarget = Break }, Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Action = (time, breakPeriod) => breakPeriod with { StartTime = time }, + Action = (time, breakPeriod) => new ManualBreakPeriod(time, breakPeriod.EndTime), }, new DragHandle(isStartHandle: false) { Break = { BindTarget = Break }, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Action = (time, breakPeriod) => breakPeriod with { EndTime = time }, + Action = (time, breakPeriod) => new ManualBreakPeriod(breakPeriod.StartTime, time), }, }; } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index f4be987547..80586a923d 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Edit BeatmapSkin.BeatmapSkinChanged += SaveState; } - beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapProcessor(this); + beatmapProcessor = new EditorBeatmapProcessor(this, playableBeatmap.BeatmapInfo.Ruleset.CreateInstance()); foreach (var obj in HitObjects) trackStartTime(obj); diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs new file mode 100644 index 0000000000..5b1cf281bb --- /dev/null +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Screens.Edit +{ + public class EditorBeatmapProcessor : IBeatmapProcessor + { + public IBeatmap Beatmap { get; } + + private readonly IBeatmapProcessor? rulesetBeatmapProcessor; + + public EditorBeatmapProcessor(IBeatmap beatmap, Ruleset ruleset) + { + Beatmap = beatmap; + rulesetBeatmapProcessor = ruleset.CreateBeatmapProcessor(beatmap); + } + + public void PreProcess() + { + rulesetBeatmapProcessor?.PreProcess(); + } + + public void PostProcess() + { + rulesetBeatmapProcessor?.PostProcess(); + + autoGenerateBreaks(); + } + + private void autoGenerateBreaks() + { + Beatmap.Breaks.RemoveAll(b => b is not ManualBreakPeriod); + + for (int i = 1; i < Beatmap.HitObjects.Count; ++i) + { + double previousObjectEndTime = Beatmap.HitObjects[i - 1].GetEndTime(); + double nextObjectStartTime = Beatmap.HitObjects[i].StartTime; + + if (nextObjectStartTime - previousObjectEndTime < BreakPeriod.MIN_GAP_DURATION) + continue; + + double breakStartTime = previousObjectEndTime + BreakPeriod.GAP_BEFORE_BREAK; + double breakEndTime = nextObjectStartTime - Math.Max(BreakPeriod.GAP_AFTER_BREAK, Beatmap.ControlPointInfo.TimingPointAt(nextObjectStartTime).BeatLength * 2); + + if (breakEndTime - breakStartTime < BreakPeriod.MIN_BREAK_DURATION) + continue; + + var breakPeriod = new BreakPeriod(breakStartTime, breakEndTime); + + if (Beatmap.Breaks.Any(b => b.Intersects(breakPeriod))) + continue; + + Beatmap.Breaks.Add(breakPeriod); + } + } + } +} diff --git a/osu.Game/Screens/Edit/ManualBreakPeriod.cs b/osu.Game/Screens/Edit/ManualBreakPeriod.cs new file mode 100644 index 0000000000..719784b500 --- /dev/null +++ b/osu.Game/Screens/Edit/ManualBreakPeriod.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps.Timing; + +namespace osu.Game.Screens.Edit +{ + public record ManualBreakPeriod : BreakPeriod + { + public ManualBreakPeriod(double startTime, double endTime) + : base(startTime, endTime) + { + } + } +}