1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:27:29 +08:00

Merge pull request #28528 from bdach/break-autogeneration

This commit is contained in:
Dean Herbert 2024-06-19 23:46:05 +09:00 committed by GitHub
commit 9eb6752033
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 523 additions and 94 deletions

View File

@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
StartTime = 5000, StartTime = 5000,
} }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(2000, 4000), new BreakPeriod(2000, 4000),
} }

View File

@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Autoplay = false, Autoplay = false,
Beatmap = new Beatmap Beatmap = new Beatmap
{ {
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(500, 2000), new BreakPeriod(500, 2000),
}, },

View File

@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
StartTime = 5000, StartTime = 5000,
} }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(2000, 4000), new BreakPeriod(2000, 4000),
} }

View File

@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Autoplay = false, Autoplay = false,
Beatmap = new Beatmap Beatmap = new Beatmap
{ {
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(500, 2000), new BreakPeriod(500, 2000),
}, },

View File

@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
Autoplay = false, Autoplay = false,
Beatmap = new Beatmap Beatmap = new Beatmap
{ {
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(100, 1600), new BreakPeriod(100, 1600),
}, },

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -29,7 +28,7 @@ namespace osu.Game.Tests.Editing.Checks
{ {
var beatmap = new Beatmap<HitObject> var beatmap = new Beatmap<HitObject>
{ {
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(0, 649) new BreakPeriod(0, 649)
} }
@ -52,7 +51,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_200 } new HitCircle { StartTime = 1_200 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(100, 751) new BreakPeriod(100, 751)
} }
@ -75,7 +74,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_298 } new HitCircle { StartTime = 1_298 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(200, 850) new BreakPeriod(200, 850)
} }
@ -98,7 +97,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1200 } new HitCircle { StartTime = 1200 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(1398, 2300) new BreakPeriod(1398, 2300)
} }
@ -121,7 +120,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 1100 }, new HitCircle { StartTime = 1100 },
new HitCircle { StartTime = 1500 } new HitCircle { StartTime = 1500 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(0, 652) new BreakPeriod(0, 652)
} }
@ -145,7 +144,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 1_297 }, new HitCircle { StartTime = 1_297 },
new HitCircle { StartTime = 1_298 } new HitCircle { StartTime = 1_298 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(200, 850) new BreakPeriod(200, 850)
} }
@ -168,7 +167,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_300 } new HitCircle { StartTime = 1_300 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(200, 850) new BreakPeriod(200, 850)
} }

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 }, new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 40_000 } new HitCircle { StartTime = 40_000 }
}, },
Breaks = new List<BreakPeriod> Breaks =
{ {
new BreakPeriod(10_000, 21_000) new BreakPeriod(10_000, 21_000)
} }

View File

@ -0,0 +1,300 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Editing
{
[TestFixture]
public class TestSceneEditorBeatmapProcessor
{
[Test]
public void TestEmptyBeatmap()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestSingleObjectBeatmap()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1000 },
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestTwoObjectsCloseTogether()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestTwoObjectsFarApart()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
}
};
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(4000));
});
}
[Test]
public void TestBreaksAreFused()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
},
Breaks =
{
new BreakPeriod(1200, 4000),
new BreakPeriod(5200, 8000),
}
};
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(8000));
});
}
[Test]
public void TestBreaksAreSplit()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new HitCircle { StartTime = 9000 },
},
Breaks =
{
new BreakPeriod(1200, 8000),
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(beatmap.Breaks, Has.Count.EqualTo(2));
Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000));
Assert.That(beatmap.Breaks[1].StartTime, Is.EqualTo(5200));
Assert.That(beatmap.Breaks[1].EndTime, Is.EqualTo(8000));
});
}
[Test]
public void TestBreaksAreNudged()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1100 },
new HitCircle { StartTime = 9000 },
},
Breaks =
{
new BreakPeriod(1200, 8000),
}
};
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(1300));
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(8000));
});
}
[Test]
public void TestManualBreaksAreNotFused()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
},
Breaks =
{
new ManualBreakPeriod(1200, 4000),
new ManualBreakPeriod(5200, 8000),
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(beatmap.Breaks, Has.Count.EqualTo(2));
Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000));
Assert.That(beatmap.Breaks[1].StartTime, Is.EqualTo(5200));
Assert.That(beatmap.Breaks[1].EndTime, Is.EqualTo(8000));
});
}
[Test]
public void TestManualBreaksAreSplit()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new HitCircle { StartTime = 9000 },
},
Breaks =
{
new ManualBreakPeriod(1200, 8000),
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(beatmap.Breaks, Has.Count.EqualTo(2));
Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(4000));
Assert.That(beatmap.Breaks[1].StartTime, Is.EqualTo(5200));
Assert.That(beatmap.Breaks[1].EndTime, Is.EqualTo(8000));
});
}
[Test]
public void TestManualBreaksAreNotNudged()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
},
Breaks =
{
new ManualBreakPeriod(1200, 8800),
}
};
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(8800));
});
}
}
}

View File

@ -4,10 +4,12 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
@ -15,6 +17,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test] [Test]
public void TestSelectedObjects() public void TestSelectedObjects()
{ {

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.IO.Serialization.Converters; using osu.Game.IO.Serialization.Converters;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
@ -61,7 +62,7 @@ namespace osu.Game.Beatmaps
public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo();
public List<BreakPeriod> Breaks { get; set; } = new List<BreakPeriod>(); public BindableList<BreakPeriod> Breaks { get; set; } = new BindableList<BreakPeriod>();
public List<string> UnhandledEventLines { get; set; } = new List<string>(); public List<string> UnhandledEventLines { get; set; } = new List<string>();

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -40,7 +41,7 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// The breaks in this beatmap. /// The breaks in this beatmap.
/// </summary> /// </summary>
List<BreakPeriod> Breaks { get; } BindableList<BreakPeriod> Breaks { get; }
/// <summary> /// <summary>
/// All lines from the [Events] section which aren't handled in the encoding process yet. /// All lines from the [Events] section which aren't handled in the encoding process yet.

View File

@ -1,26 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
namespace osu.Game.Beatmaps.Timing namespace osu.Game.Beatmaps.Timing
{ {
public class BreakPeriod public class BreakPeriod : IEquatable<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> /// <summary>
/// The minimum duration required for a break to have any effect. /// The minimum duration required for a break to have any effect.
/// </summary> /// </summary>
public const double MIN_BREAK_DURATION = 650; 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> /// <summary>
/// The break start time. /// The break start time.
/// </summary> /// </summary>
public double StartTime; public double StartTime { get; }
/// <summary> /// <summary>
/// The break end time. /// The break end time.
/// </summary> /// </summary>
public double EndTime; public double EndTime { get; }
/// <summary> /// <summary>
/// The break duration. /// The break duration.
@ -49,5 +67,14 @@ namespace osu.Game.Beatmaps.Timing
/// <param name="time">The time to check in milliseconds.</param> /// <param name="time">The time to check in milliseconds.</param>
/// <returns>Whether the time falls within this <see cref="BreakPeriod"/>.</returns> /// <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 Contains(double time) => time >= StartTime && time <= EndTime - BreakOverlay.BREAK_FADE_DURATION;
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

@ -9,6 +9,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -329,7 +330,7 @@ namespace osu.Game.Rulesets.Difficulty
set => baseBeatmap.Difficulty = value; set => baseBeatmap.Difficulty = value;
} }
public List<BreakPeriod> Breaks => baseBeatmap.Breaks; public BindableList<BreakPeriod> Breaks => baseBeatmap.Breaks;
public List<string> UnhandledEventLines => baseBeatmap.UnhandledEventLines; public List<string> UnhandledEventLines => baseBeatmap.UnhandledEventLines;
public double TotalBreakTime => baseBeatmap.TotalBreakTime; public double TotalBreakTime => baseBeatmap.TotalBreakTime;

View File

@ -13,13 +13,7 @@ namespace osu.Game.Rulesets.Edit.Checks
{ {
// Breaks may be off by 1 ms. // Breaks may be off by 1 ms.
private const int leniency_threshold = 1; 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 CheckMetadata Metadata => new CheckMetadata(CheckCategory.Events, "Breaks not achievable using the editor");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[] public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
@ -45,8 +39,8 @@ namespace osu.Game.Rulesets.Edit.Checks
if (previousObjectEndTimeIndex >= 0) if (previousObjectEndTimeIndex >= 0)
{ {
double gapBeforeBreak = breakPeriod.StartTime - endTimes[previousObjectEndTimeIndex]; double gapBeforeBreak = breakPeriod.StartTime - endTimes[previousObjectEndTimeIndex];
if (gapBeforeBreak < minimum_gap_before_break - leniency_threshold) if (gapBeforeBreak < BreakPeriod.GAP_BEFORE_BREAK - leniency_threshold)
yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, minimum_gap_before_break - gapBeforeBreak); yield return new IssueTemplateEarlyStart(this).Create(breakPeriod.StartTime, BreakPeriod.GAP_BEFORE_BREAK - gapBeforeBreak);
} }
int nextObjectStartTimeIndex = startTimes.BinarySearch(breakPeriod.EndTime); int nextObjectStartTimeIndex = startTimes.BinarySearch(breakPeriod.EndTime);
@ -55,8 +49,8 @@ namespace osu.Game.Rulesets.Edit.Checks
if (nextObjectStartTimeIndex < startTimes.Count) if (nextObjectStartTimeIndex < startTimes.Count)
{ {
double gapAfterBreak = startTimes[nextObjectStartTimeIndex] - breakPeriod.EndTime; double gapAfterBreak = startTimes[nextObjectStartTimeIndex] - breakPeriod.EndTime;
if (gapAfterBreak < min_end_threshold - leniency_threshold) if (gapAfterBreak < BreakPeriod.GAP_AFTER_BREAK - leniency_threshold)
yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, min_end_threshold - gapAfterBreak); yield return new IssueTemplateLateEnd(this).Create(breakPeriod.StartTime, BreakPeriod.GAP_AFTER_BREAK - gapAfterBreak);
} }
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
@ -14,35 +15,33 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// </summary> /// </summary>
public partial class BreakPart : TimelinePart public partial class BreakPart : TimelinePart
{ {
private readonly BindableList<BreakPeriod> breaks = new BindableList<BreakPeriod>();
protected override void LoadBeatmap(EditorBeatmap beatmap) protected override void LoadBeatmap(EditorBeatmap beatmap)
{ {
base.LoadBeatmap(beatmap); base.LoadBeatmap(beatmap);
breaks.UnbindAll();
breaks.BindTo(beatmap.Breaks);
breaks.BindCollectionChanged((_, _) =>
{
foreach (var breakPeriod in beatmap.Breaks) foreach (var breakPeriod in beatmap.Breaks)
Add(new BreakVisualisation(breakPeriod)); Add(new BreakVisualisation(breakPeriod));
}, true);
} }
private partial class BreakVisualisation : Circle private partial class BreakVisualisation : Circle
{ {
private readonly BreakPeriod breakPeriod;
public BreakVisualisation(BreakPeriod breakPeriod) public BreakVisualisation(BreakPeriod breakPeriod)
{ {
this.breakPeriod = breakPeriod;
RelativePositionAxes = Axes.X; RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
}
protected override void Update()
{
base.Update();
X = (float)breakPeriod.StartTime; X = (float)breakPeriod.StartTime;
Width = (float)breakPeriod.Duration; Width = (float)breakPeriod.Duration;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.GreyCarmineLight; private void load(OsuColour colours) => Colour = colours.Gray7;
} }
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -20,11 +21,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public partial class TimelineBreak : CompositeDrawable public partial class TimelineBreak : CompositeDrawable
{ {
public BreakPeriod Break { get; } public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>();
public TimelineBreak(BreakPeriod b) public TimelineBreak(BreakPeriod b)
{ {
Break = b; Break.Value = b;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -44,44 +45,50 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Child = new Box Child = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colours.PurpleLight, Colour = colours.Gray5,
Alpha = 0.4f, Alpha = 0.7f,
}, },
}, },
new DragHandle(Break, isStartHandle: true) new DragHandle(isStartHandle: true)
{ {
Break = { BindTarget = Break },
Anchor = Anchor.TopLeft, Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft, Origin = Anchor.TopLeft,
Action = (time, breakPeriod) => breakPeriod.StartTime = time, Action = (time, breakPeriod) => new ManualBreakPeriod(time, breakPeriod.EndTime),
}, },
new DragHandle(Break, isStartHandle: false) new DragHandle(isStartHandle: false)
{ {
Break = { BindTarget = Break },
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Action = (time, breakPeriod) => breakPeriod.EndTime = time, Action = (time, breakPeriod) => new ManualBreakPeriod(breakPeriod.StartTime, time),
}, },
}; };
} }
protected override void Update() protected override void LoadComplete()
{ {
base.Update(); base.LoadComplete();
X = (float)Break.StartTime; Break.BindValueChanged(_ =>
Width = (float)Break.Duration; {
X = (float)Break.Value.StartTime;
Width = (float)Break.Value.Duration;
}, true);
} }
private partial class DragHandle : FillFlowContainer private partial class DragHandle : FillFlowContainer
{ {
public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>();
public new Anchor Anchor public new Anchor Anchor
{ {
get => base.Anchor; get => base.Anchor;
init => base.Anchor = value; init => base.Anchor = value;
} }
public Action<double, BreakPeriod>? Action { get; init; } public Func<double, BreakPeriod, BreakPeriod>? Action { get; init; }
private readonly BreakPeriod breakPeriod;
private readonly bool isStartHandle; private readonly bool isStartHandle;
private Container handle = null!; private Container handle = null!;
@ -99,9 +106,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private OsuColour colours { get; set; } = null!; private OsuColour colours { get; set; } = null!;
public DragHandle(BreakPeriod breakPeriod, bool isStartHandle) public DragHandle(bool isStartHandle)
{ {
this.breakPeriod = breakPeriod;
this.isStartHandle = isStartHandle; this.isStartHandle = isStartHandle;
} }
@ -164,13 +170,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
changeHandler?.BeginChange(); changeHandler?.BeginChange();
updateState(); updateState();
double min = beatmap.HitObjects.Last(ho => ho.GetEndTime() <= breakPeriod.StartTime).GetEndTime(); double min = beatmap.HitObjects.Last(ho => ho.GetEndTime() <= Break.Value.StartTime).GetEndTime();
double max = beatmap.HitObjects.First(ho => ho.StartTime >= breakPeriod.EndTime).StartTime; double max = beatmap.HitObjects.First(ho => ho.StartTime >= Break.Value.EndTime).StartTime;
if (isStartHandle) if (isStartHandle)
max = Math.Min(max, breakPeriod.EndTime - BreakPeriod.MIN_BREAK_DURATION); max = Math.Min(max, Break.Value.EndTime - BreakPeriod.MIN_BREAK_DURATION);
else else
min = Math.Max(min, breakPeriod.StartTime + BreakPeriod.MIN_BREAK_DURATION); min = Math.Max(min, Break.Value.StartTime + BreakPeriod.MIN_BREAK_DURATION);
allowedDragRange = (min, max); allowedDragRange = (min, max);
@ -183,11 +189,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Debug.Assert(allowedDragRange != null); Debug.Assert(allowedDragRange != null);
if (timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition).Time is double time if (Action != null
&& timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition).Time is double time
&& time > allowedDragRange.Value.min && time > allowedDragRange.Value.min
&& time < allowedDragRange.Value.max) && time < allowedDragRange.Value.max)
{ {
Action?.Invoke(time, breakPeriod); int index = beatmap.Breaks.IndexOf(Break.Value);
beatmap.Breaks[index] = Break.Value = Action.Invoke(time, Break.Value);
} }
updateState(); updateState();
@ -204,7 +212,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
bool active = IsHovered || IsDragged; bool active = IsHovered || IsDragged;
var colour = colours.PurpleLighter; var colour = colours.Gray8;
if (active) if (active)
colour = colour.Lighten(0.3f); colour = colour.Lighten(0.3f);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
@ -27,8 +28,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
base.LoadBeatmap(beatmap); base.LoadBeatmap(beatmap);
// TODO: this will have to be mutable soon enough breaks.UnbindAll();
breaks.AddRange(beatmap.Breaks); breaks.BindTo(beatmap.Breaks);
breaks.BindCollectionChanged((_, e) =>
{
if (e.Action != NotifyCollectionChangedAction.Replace)
breakCache.Invalidate();
});
} }
protected override void Update() protected override void Update()
@ -56,14 +62,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void recreateBreaks() private void recreateBreaks()
{ {
// Remove groups outside the visible range Clear();
foreach (TimelineBreak drawableBreak in this)
{
if (!shouldBeVisible(drawableBreak.Break))
drawableBreak.Expire();
}
// Add remaining ones
for (int i = 0; i < breaks.Count; i++) for (int i = 0; i < breaks.Count; i++)
{ {
var breakPeriod = breaks[i]; var breakPeriod = breaks[i];
@ -71,20 +71,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (!shouldBeVisible(breakPeriod)) if (!shouldBeVisible(breakPeriod))
continue; continue;
bool alreadyVisible = false;
foreach (var b in this)
{
if (ReferenceEquals(b.Break, breakPeriod))
{
alreadyVisible = true;
break;
}
}
if (alreadyVisible)
continue;
Add(new TimelineBreak(breakPeriod)); Add(new TimelineBreak(breakPeriod));
} }
} }

View File

@ -105,7 +105,7 @@ namespace osu.Game.Screens.Edit
BeatmapSkin.BeatmapSkinChanged += SaveState; BeatmapSkin.BeatmapSkinChanged += SaveState;
} }
beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapProcessor(this); beatmapProcessor = new EditorBeatmapProcessor(this, playableBeatmap.BeatmapInfo.Ruleset.CreateInstance());
foreach (var obj in HitObjects) foreach (var obj in HitObjects)
trackStartTime(obj); trackStartTime(obj);
@ -172,7 +172,7 @@ namespace osu.Game.Screens.Edit
set => PlayableBeatmap.ControlPointInfo = value; set => PlayableBeatmap.ControlPointInfo = value;
} }
public List<BreakPeriod> Breaks => PlayableBeatmap.Breaks; public BindableList<BreakPeriod> Breaks => PlayableBeatmap.Breaks;
public List<string> UnhandledEventLines => PlayableBeatmap.UnhandledEventLines; public List<string> UnhandledEventLines => PlayableBeatmap.UnhandledEventLines;
@ -349,13 +349,13 @@ namespace osu.Game.Screens.Edit
if (batchPendingUpdates.Count == 0 && batchPendingDeletes.Count == 0 && batchPendingInserts.Count == 0) if (batchPendingUpdates.Count == 0 && batchPendingDeletes.Count == 0 && batchPendingInserts.Count == 0)
return; return;
beatmapProcessor?.PreProcess(); beatmapProcessor.PreProcess();
foreach (var h in batchPendingDeletes) processHitObject(h); foreach (var h in batchPendingDeletes) processHitObject(h);
foreach (var h in batchPendingInserts) processHitObject(h); foreach (var h in batchPendingInserts) processHitObject(h);
foreach (var h in batchPendingUpdates) processHitObject(h); foreach (var h in batchPendingUpdates) processHitObject(h);
beatmapProcessor?.PostProcess(); beatmapProcessor.PostProcess();
BeatmapReprocessed?.Invoke(); BeatmapReprocessed?.Invoke();

View File

@ -0,0 +1,70 @@
// 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);
foreach (var manualBreak in Beatmap.Breaks.ToList())
{
if (Beatmap.HitObjects.Any(ho => ho.StartTime <= manualBreak.EndTime && ho.GetEndTime() >= manualBreak.StartTime))
Beatmap.Breaks.Remove(manualBreak);
}
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

@ -45,6 +45,7 @@ namespace osu.Game.Screens.Edit
editorBeatmap.BeginChange(); editorBeatmap.BeginChange();
processHitObjects(result, () => newBeatmap ??= readBeatmap(newState)); processHitObjects(result, () => newBeatmap ??= readBeatmap(newState));
processTimingPoints(() => newBeatmap ??= readBeatmap(newState)); processTimingPoints(() => newBeatmap ??= readBeatmap(newState));
processBreaks(() => newBeatmap ??= readBeatmap(newState));
processHitObjectLocalData(() => newBeatmap ??= readBeatmap(newState)); processHitObjectLocalData(() => newBeatmap ??= readBeatmap(newState));
editorBeatmap.EndChange(); editorBeatmap.EndChange();
} }
@ -75,6 +76,27 @@ namespace osu.Game.Screens.Edit
} }
} }
private void processBreaks(Func<IBeatmap> getNewBeatmap)
{
var newBreaks = getNewBeatmap().Breaks.ToArray();
foreach (var oldBreak in editorBeatmap.Breaks.ToArray())
{
if (newBreaks.Any(b => b.Equals(oldBreak)))
continue;
editorBeatmap.Breaks.Remove(oldBreak);
}
foreach (var newBreak in newBreaks)
{
if (editorBeatmap.Breaks.Any(b => b.Equals(newBreak)))
continue;
editorBeatmap.Breaks.Add(newBreak);
}
}
private void processHitObjects(DiffResult result, Func<IBeatmap> getNewBeatmap) private void processHitObjects(DiffResult result, Func<IBeatmap> getNewBeatmap)
{ {
findChangedIndices(result, LegacyDecoder<Beatmap>.Section.HitObjects, out var removedIndices, out var addedIndices); findChangedIndices(result, LegacyDecoder<Beatmap>.Section.HitObjects, out var removedIndices, out var addedIndices);

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 class ManualBreakPeriod : BreakPeriod
{
public ManualBreakPeriod(double startTime, double endTime)
: base(startTime, endTime)
{
}
}
}

View File

@ -26,11 +26,13 @@ namespace osu.Game.Tests.Beatmaps
BeatmapInfo = baseBeatmap.BeatmapInfo; BeatmapInfo = baseBeatmap.BeatmapInfo;
ControlPointInfo = baseBeatmap.ControlPointInfo; ControlPointInfo = baseBeatmap.ControlPointInfo;
Breaks = baseBeatmap.Breaks;
UnhandledEventLines = baseBeatmap.UnhandledEventLines; UnhandledEventLines = baseBeatmap.UnhandledEventLines;
if (withHitObjects) if (withHitObjects)
{
HitObjects = baseBeatmap.HitObjects; HitObjects = baseBeatmap.HitObjects;
Breaks = baseBeatmap.Breaks;
}
BeatmapInfo.Ruleset = ruleset; BeatmapInfo.Ruleset = ruleset;
BeatmapInfo.Length = 75000; BeatmapInfo.Length = 75000;