1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 04:13:00 +08:00

Implement automatic break period generation

This commit is contained in:
Bartłomiej Dach 2024-06-19 10:11:04 +02:00
parent 1f692f5fc7
commit 4022a8b06c
No known key found for this signature in database
6 changed files with 113 additions and 18 deletions

View File

@ -6,22 +6,39 @@ using osu.Game.Screens.Play;
namespace osu.Game.Beatmaps.Timing
{
public readonly struct BreakPeriod : IEquatable<BreakPeriod>
public record BreakPeriod
{
/// <summary>
/// The minimum gap between the start of the break and the previous object.
/// </summary>
public const double GAP_BEFORE_BREAK = 200;
/// <summary>
/// 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
/// </summary>
public const double GAP_AFTER_BREAK = 450;
/// <summary>
/// The minimum duration required for a break to have any effect.
/// </summary>
public const double MIN_BREAK_DURATION = 650;
/// <summary>
/// The minimum required duration of a gap between two objects such that a break can be placed between them.
/// </summary>
public const double MIN_GAP_DURATION = GAP_BEFORE_BREAK + MIN_BREAK_DURATION + GAP_AFTER_BREAK;
/// <summary>
/// The break start time.
/// </summary>
public double StartTime { get; init; }
public double StartTime { get; }
/// <summary>
/// The break end time.
/// </summary>
public double EndTime { get; init; }
public double EndTime { get; }
/// <summary>
/// The break duration.
@ -51,8 +68,13 @@ namespace osu.Game.Beatmaps.Timing
/// <returns>Whether the time falls within this <see cref="BreakPeriod"/>.</returns>
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);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,64 @@
// 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.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);
}
}
}
}

View File

@ -0,0 +1,15 @@
// 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 osu.Game.Beatmaps.Timing;
namespace osu.Game.Screens.Edit
{
public record ManualBreakPeriod : BreakPeriod
{
public ManualBreakPeriod(double startTime, double endTime)
: base(startTime, endTime)
{
}
}
}