1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 13:27:25 +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,
}
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(2000, 4000),
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
@ -29,7 +28,7 @@ namespace osu.Game.Tests.Editing.Checks
{
var beatmap = new Beatmap<HitObject>
{
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(0, 649)
}
@ -52,7 +51,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_200 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(100, 751)
}
@ -75,7 +74,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_298 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(200, 850)
}
@ -98,7 +97,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1200 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(1398, 2300)
}
@ -121,7 +120,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 1100 },
new HitCircle { StartTime = 1500 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(0, 652)
}
@ -145,7 +144,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 1_297 },
new HitCircle { StartTime = 1_298 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(200, 850)
}
@ -168,7 +167,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 1_300 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
new BreakPeriod(200, 850)
}

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 40_000 }
},
Breaks = new List<BreakPeriod>
Breaks =
{
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
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Editing
{
@ -15,6 +17,8 @@ namespace osu.Game.Tests.Visual.Editing
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestSelectedObjects()
{

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.IO.Serialization.Converters;
namespace osu.Game.Beatmaps
@ -61,7 +62,7 @@ namespace osu.Game.Beatmaps
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>();

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
@ -40,7 +41,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// The breaks in this beatmap.
/// </summary>
List<BreakPeriod> Breaks { get; }
BindableList<BreakPeriod> Breaks { get; }
/// <summary>
/// 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.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Screens.Play;
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>
/// 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;
public double StartTime { get; }
/// <summary>
/// The break end time.
/// </summary>
public double EndTime;
public double EndTime { get; }
/// <summary>
/// The break duration.
@ -49,5 +67,14 @@ namespace osu.Game.Beatmaps.Timing
/// <param name="time">The time to check in milliseconds.</param>
/// <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 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 JetBrains.Annotations;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -329,7 +330,7 @@ namespace osu.Game.Rulesets.Difficulty
set => baseBeatmap.Difficulty = value;
}
public List<BreakPeriod> Breaks => baseBeatmap.Breaks;
public BindableList<BreakPeriod> Breaks => baseBeatmap.Breaks;
public List<string> UnhandledEventLines => baseBeatmap.UnhandledEventLines;
public double TotalBreakTime => baseBeatmap.TotalBreakTime;

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

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.Timing;
@ -14,35 +15,33 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// </summary>
public partial class BreakPart : TimelinePart
{
private readonly BindableList<BreakPeriod> breaks = new BindableList<BreakPeriod>();
protected override void LoadBeatmap(EditorBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
foreach (var breakPeriod in beatmap.Breaks)
Add(new BreakVisualisation(breakPeriod));
breaks.UnbindAll();
breaks.BindTo(beatmap.Breaks);
breaks.BindCollectionChanged((_, _) =>
{
foreach (var breakPeriod in beatmap.Breaks)
Add(new BreakVisualisation(breakPeriod));
}, true);
}
private partial class BreakVisualisation : Circle
{
private readonly BreakPeriod breakPeriod;
public BreakVisualisation(BreakPeriod breakPeriod)
{
this.breakPeriod = breakPeriod;
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Both;
}
protected override void Update()
{
base.Update();
X = (float)breakPeriod.StartTime;
Width = (float)breakPeriod.Duration;
}
[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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -20,11 +21,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public partial class TimelineBreak : CompositeDrawable
{
public BreakPeriod Break { get; }
public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>();
public TimelineBreak(BreakPeriod b)
{
Break = b;
Break.Value = b;
}
[BackgroundDependencyLoader]
@ -44,44 +45,50 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.PurpleLight,
Alpha = 0.4f,
Colour = colours.Gray5,
Alpha = 0.7f,
},
},
new DragHandle(Break, isStartHandle: true)
new DragHandle(isStartHandle: true)
{
Break = { BindTarget = Break },
Anchor = 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,
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;
Width = (float)Break.Duration;
Break.BindValueChanged(_ =>
{
X = (float)Break.Value.StartTime;
Width = (float)Break.Value.Duration;
}, true);
}
private partial class DragHandle : FillFlowContainer
{
public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>();
public new Anchor Anchor
{
get => base.Anchor;
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 Container handle = null!;
@ -99,9 +106,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private OsuColour colours { get; set; } = null!;
public DragHandle(BreakPeriod breakPeriod, bool isStartHandle)
public DragHandle(bool isStartHandle)
{
this.breakPeriod = breakPeriod;
this.isStartHandle = isStartHandle;
}
@ -164,13 +170,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
changeHandler?.BeginChange();
updateState();
double min = beatmap.HitObjects.Last(ho => ho.GetEndTime() <= breakPeriod.StartTime).GetEndTime();
double max = beatmap.HitObjects.First(ho => ho.StartTime >= breakPeriod.EndTime).StartTime;
double min = beatmap.HitObjects.Last(ho => ho.GetEndTime() <= Break.Value.StartTime).GetEndTime();
double max = beatmap.HitObjects.First(ho => ho.StartTime >= Break.Value.EndTime).StartTime;
if (isStartHandle)
max = Math.Min(max, breakPeriod.EndTime - BreakPeriod.MIN_BREAK_DURATION);
max = Math.Min(max, Break.Value.EndTime - BreakPeriod.MIN_BREAK_DURATION);
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);
@ -183,11 +189,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
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.max)
{
Action?.Invoke(time, breakPeriod);
int index = beatmap.Breaks.IndexOf(Break.Value);
beatmap.Breaks[index] = Break.Value = Action.Invoke(time, Break.Value);
}
updateState();
@ -204,7 +212,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
bool active = IsHovered || IsDragged;
var colour = colours.PurpleLighter;
var colour = colours.Gray8;
if (active)
colour = colour.Lighten(0.3f);

View File

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

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);
@ -172,7 +172,7 @@ namespace osu.Game.Screens.Edit
set => PlayableBeatmap.ControlPointInfo = value;
}
public List<BreakPeriod> Breaks => PlayableBeatmap.Breaks;
public BindableList<BreakPeriod> Breaks => PlayableBeatmap.Breaks;
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)
return;
beatmapProcessor?.PreProcess();
beatmapProcessor.PreProcess();
foreach (var h in batchPendingDeletes) processHitObject(h);
foreach (var h in batchPendingInserts) processHitObject(h);
foreach (var h in batchPendingUpdates) processHitObject(h);
beatmapProcessor?.PostProcess();
beatmapProcessor.PostProcess();
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();
processHitObjects(result, () => newBeatmap ??= readBeatmap(newState));
processTimingPoints(() => newBeatmap ??= readBeatmap(newState));
processBreaks(() => newBeatmap ??= readBeatmap(newState));
processHitObjectLocalData(() => newBeatmap ??= readBeatmap(newState));
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)
{
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;
ControlPointInfo = baseBeatmap.ControlPointInfo;
Breaks = baseBeatmap.Breaks;
UnhandledEventLines = baseBeatmap.UnhandledEventLines;
if (withHitObjects)
{
HitObjects = baseBeatmap.HitObjects;
Breaks = baseBeatmap.Breaks;
}
BeatmapInfo.Ruleset = ruleset;
BeatmapInfo.Length = 75000;