diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 52c42dfddbdc..329055b3ddd5 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Objects { - public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation + public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation, IHasTimePreempt { public const float OBJECT_RADIUS = 64; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 6c77d9189caf..1b0993b69889 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects { - public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition + public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition, IHasTimePreempt { /// /// The radius of hit objects (ie. the radius of a ). @@ -46,7 +46,7 @@ public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPositi /// public const double PREEMPT_MAX = 1800; - public double TimePreempt = 600; + public double TimePreempt { get; set; } = 600; public double TimeFadeIn = 400; private HitObjectProperty position; diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs index 251099c0e270..bbcf6aac2cac 100644 --- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs +++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs @@ -45,7 +45,7 @@ public void TestSingleObjectBeatmap() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, + new Note { StartTime = 1000 }, } }); @@ -67,8 +67,8 @@ public void TestTwoObjectsCloseTogether() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 2000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 2000 }, } }); @@ -136,8 +136,8 @@ public void TestTwoObjectsFarApart() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 5000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 5000 }, } }); @@ -164,8 +164,8 @@ public void TestBreaksAreFused() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -197,9 +197,9 @@ public void TestBreaksAreSplit() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 5000 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 5000 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -232,8 +232,8 @@ public void TestBreaksAreNudged() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1100 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1100 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -264,8 +264,8 @@ public void TestManualBreaksAreNotFused() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -299,9 +299,9 @@ public void TestManualBreaksAreSplit() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 5000 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 5000 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -334,8 +334,8 @@ public void TestManualBreaksAreNotNudged() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 9000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 9000 }, }, Breaks = { @@ -366,8 +366,8 @@ public void TestBreaksAtEndOfBeatmapAreRemoved() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 2000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 2000 }, }, Breaks = { @@ -393,8 +393,8 @@ public void TestManualBreaksAtEndOfBeatmapAreRemoved() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 1000 }, - new HitCircle { StartTime = 2000 }, + new Note { StartTime = 1000 }, + new Note { StartTime = 2000 }, }, Breaks = { @@ -447,8 +447,8 @@ public void TestBreaksAtStartOfBeatmapAreRemoved() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 10000 }, - new HitCircle { StartTime = 11000 }, + new Note { StartTime = 10000 }, + new Note { StartTime = 11000 }, }, Breaks = { @@ -474,8 +474,8 @@ public void TestManualBreaksAtStartOfBeatmapAreRemoved() BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, HitObjects = { - new HitCircle { StartTime = 10000 }, - new HitCircle { StartTime = 11000 }, + new Note { StartTime = 10000 }, + new Note { StartTime = 11000 }, }, Breaks = { @@ -489,5 +489,55 @@ public void TestManualBreaksAtStartOfBeatmapAreRemoved() Assert.That(beatmap.Breaks, Is.Empty); } + + [Test] + public void TestTimePreemptIsRespected() + { + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + var beatmap = new EditorBeatmap(new Beatmap + { + ControlPointInfo = controlPoints, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, + Difficulty = + { + ApproachRate = 10, + }, + HitObjects = + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 5000 }, + } + }); + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset()); + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(1)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MIN)); + }); + + beatmap.Difficulty.ApproachRate = 0; + + foreach (var ho in beatmap.HitObjects) + ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + beatmapProcessor.PreProcess(); + beatmapProcessor.PostProcess(); + + Assert.Multiple(() => + { + Assert.That(beatmap.Breaks, Has.Count.EqualTo(1)); + Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200)); + Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX)); + }); + } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasTimePreempt.cs b/osu.Game/Rulesets/Objects/Types/IHasTimePreempt.cs new file mode 100644 index 000000000000..e7239515f683 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasTimePreempt.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Objects.Types +{ + /// + /// A that appears on screen at a fixed time interval before its . + /// + public interface IHasTimePreempt + { + double TimePreempt { get; } + } +} diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 9b6d956a4ce1..4fe431498fcc 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Screens.Edit { @@ -44,7 +45,7 @@ public void PostProcess() private void autoGenerateBreaks() { - var objectDuration = Beatmap.HitObjects.Select(ho => (ho.StartTime, ho.GetEndTime())).ToHashSet(); + var objectDuration = Beatmap.HitObjects.Select(ho => (ho.StartTime - ((ho as IHasTimePreempt)?.TimePreempt ?? 0), ho.GetEndTime())).ToHashSet(); if (objectDuration.SetEquals(objectDurationCache)) return; @@ -67,19 +68,26 @@ private void autoGenerateBreaks() for (int i = 1; i < Beatmap.HitObjects.Count; ++i) { + var previousObject = Beatmap.HitObjects[i - 1]; + var nextObject = Beatmap.HitObjects[i]; + // Keep track of the maximum end time encountered thus far. // This handles cases like osu!mania's hold notes, which could have concurrent other objects after their start time. // Note that we're relying on the implicit assumption that objects are sorted by start time, // which is why similar tracking is not done for start time. - currentMaxEndTime = Math.Max(currentMaxEndTime, Beatmap.HitObjects[i - 1].GetEndTime()); - - double nextObjectStartTime = Beatmap.HitObjects[i].StartTime; + currentMaxEndTime = Math.Max(currentMaxEndTime, previousObject.GetEndTime()); - if (nextObjectStartTime - currentMaxEndTime < BreakPeriod.MIN_GAP_DURATION) + if (nextObject.StartTime - currentMaxEndTime < BreakPeriod.MIN_GAP_DURATION) continue; double breakStartTime = currentMaxEndTime + BreakPeriod.GAP_BEFORE_BREAK; - double breakEndTime = nextObjectStartTime - Math.Max(BreakPeriod.GAP_AFTER_BREAK, Beatmap.ControlPointInfo.TimingPointAt(nextObjectStartTime).BeatLength * 2); + + double breakEndTime = nextObject.StartTime; + + if (nextObject is IHasTimePreempt hasTimePreempt) + breakEndTime -= hasTimePreempt.TimePreempt; + else + breakEndTime -= Math.Max(BreakPeriod.GAP_AFTER_BREAK, Beatmap.ControlPointInfo.TimingPointAt(nextObject.StartTime).BeatLength * 2); if (breakEndTime - breakStartTime < BreakPeriod.MIN_BREAK_DURATION) continue;