diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index ff13e61360..25551d1ef6 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -143,7 +143,7 @@ namespace osu.Game.Beatmaps.Formats
protected string CleanFilename(string path) => path.Trim('"').ToStandardisedPath();
- protected enum Section
+ public enum Section
{
General,
Editor,
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index c9449f3259..577a8291ad 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -71,31 +71,7 @@ namespace osu.Game.Screens.Edit
public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null, BeatmapInfo beatmapInfo = null)
{
PlayableBeatmap = playableBeatmap;
-
- // ensure we are not working with legacy control points.
- // if we leave the legacy points around they will be applied over any local changes on
- // ApplyDefaults calls. this should eventually be removed once the default logic is moved to the decoder/converter.
- if (PlayableBeatmap.ControlPointInfo is LegacyControlPointInfo)
- {
- var newControlPoints = new ControlPointInfo();
-
- foreach (var controlPoint in PlayableBeatmap.ControlPointInfo.AllControlPoints)
- {
- switch (controlPoint)
- {
- case DifficultyControlPoint _:
- case SampleControlPoint _:
- // skip legacy types.
- continue;
-
- default:
- newControlPoints.Add(controlPoint.Time, controlPoint);
- break;
- }
- }
-
- playableBeatmap.ControlPointInfo = newControlPoints;
- }
+ PlayableBeatmap.ControlPointInfo = ConvertControlPoints(PlayableBeatmap.ControlPointInfo);
this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo;
@@ -108,6 +84,39 @@ namespace osu.Game.Screens.Edit
trackStartTime(obj);
}
+ ///
+ /// Converts a such that the resultant is non-legacy.
+ ///
+ /// The to convert.
+ /// The non-legacy . is returned if already non-legacy.
+ public static ControlPointInfo ConvertControlPoints(ControlPointInfo incoming)
+ {
+ // ensure we are not working with legacy control points.
+ // if we leave the legacy points around they will be applied over any local changes on
+ // ApplyDefaults calls. this should eventually be removed once the default logic is moved to the decoder/converter.
+ if (!(incoming is LegacyControlPointInfo))
+ return incoming;
+
+ var newControlPoints = new ControlPointInfo();
+
+ foreach (var controlPoint in incoming.AllControlPoints)
+ {
+ switch (controlPoint)
+ {
+ case DifficultyControlPoint _:
+ case SampleControlPoint _:
+ // skip legacy types.
+ continue;
+
+ default:
+ newControlPoints.Add(controlPoint.Time, controlPoint);
+ break;
+ }
+ }
+
+ return newControlPoints;
+ }
+
public BeatmapInfo BeatmapInfo
{
get => beatmapInfo;
diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs
index 3ed2a7efe2..ba9a7ed1a3 100644
--- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs
+++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs
@@ -7,9 +7,11 @@ using System.Diagnostics;
using System.IO;
using System.Text;
using DiffPlex;
+using DiffPlex.Model;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Skinning;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
@@ -32,61 +34,99 @@ namespace osu.Game.Screens.Edit
{
// Diff the beatmaps
var result = new Differ().CreateLineDiffs(readString(currentState), readString(newState), true, false);
+ IBeatmap newBeatmap = null;
+
+ editorBeatmap.BeginChange();
+ processHitObjects(result, () => newBeatmap ??= readBeatmap(newState));
+ processTimingPoints(result, () => newBeatmap ??= readBeatmap(newState));
+ editorBeatmap.EndChange();
+ }
+
+ private void processTimingPoints(DiffResult result, Func getNewBeatmap)
+ {
+ findChangedIndices(result, LegacyDecoder.Section.TimingPoints, out var removedIndices, out var addedIndices);
+
+ if (removedIndices.Count == 0 && addedIndices.Count == 0)
+ return;
+
+ // Due to conversion from legacy to non-legacy control points, it becomes difficult to diff control points correctly.
+ // So instead _all_ control points are reloaded if _any_ control point is changed.
+
+ var newControlPoints = EditorBeatmap.ConvertControlPoints(getNewBeatmap().ControlPointInfo);
+
+ editorBeatmap.ControlPointInfo.Clear();
+ foreach (var point in newControlPoints.AllControlPoints)
+ editorBeatmap.ControlPointInfo.Add(point.Time, point);
+ }
+
+ private void processHitObjects(DiffResult result, Func getNewBeatmap)
+ {
+ findChangedIndices(result, LegacyDecoder.Section.HitObjects, out var removedIndices, out var addedIndices);
+
+ foreach (int removed in removedIndices)
+ editorBeatmap.RemoveAt(removed);
+
+ if (addedIndices.Count > 0)
+ {
+ var newBeatmap = getNewBeatmap();
+
+ foreach (int i in addedIndices)
+ editorBeatmap.Insert(i, newBeatmap.HitObjects[i]);
+ }
+ }
+
+ private void findChangedIndices(DiffResult result, LegacyDecoder.Section section, out List removedIndices, out List addedIndices)
+ {
+ removedIndices = new List();
+ addedIndices = new List();
// Find the index of [HitObject] sections. Lines changed prior to this index are ignored.
- int oldHitObjectsIndex = Array.IndexOf(result.PiecesOld, "[HitObjects]");
- int newHitObjectsIndex = Array.IndexOf(result.PiecesNew, "[HitObjects]");
+ int oldSectionStartIndex = Array.IndexOf(result.PiecesOld, $"[{section}]");
+ int oldSectionEndIndex = Array.FindIndex(result.PiecesOld, oldSectionStartIndex + 1, s => s.StartsWith(@"[", StringComparison.Ordinal));
- Debug.Assert(oldHitObjectsIndex >= 0);
- Debug.Assert(newHitObjectsIndex >= 0);
+ if (oldSectionEndIndex == -1)
+ oldSectionEndIndex = result.PiecesOld.Length;
- var toRemove = new List();
- var toAdd = new List();
+ int newSectionStartIndex = Array.IndexOf(result.PiecesNew, $"[{section}]");
+ int newSectionEndIndex = Array.FindIndex(result.PiecesNew, newSectionStartIndex + 1, s => s.StartsWith(@"[", StringComparison.Ordinal));
+
+ if (newSectionEndIndex == -1)
+ newSectionEndIndex = result.PiecesOld.Length;
+
+ Debug.Assert(oldSectionStartIndex >= 0);
+ Debug.Assert(newSectionStartIndex >= 0);
foreach (var block in result.DiffBlocks)
{
- // Removed hitobjects
+ // Removed indices
for (int i = 0; i < block.DeleteCountA; i++)
{
- int hoIndex = block.DeleteStartA + i - oldHitObjectsIndex - 1;
+ int objectIndex = block.DeleteStartA + i;
- if (hoIndex < 0)
+ if (objectIndex <= oldSectionStartIndex || objectIndex >= oldSectionEndIndex)
continue;
- toRemove.Add(hoIndex);
+ removedIndices.Add(objectIndex - oldSectionStartIndex - 1);
}
- // Added hitobjects
+ // Added indices
for (int i = 0; i < block.InsertCountB; i++)
{
- int hoIndex = block.InsertStartB + i - newHitObjectsIndex - 1;
+ int objectIndex = block.InsertStartB + i;
- if (hoIndex < 0)
+ if (objectIndex <= newSectionStartIndex || objectIndex >= newSectionEndIndex)
continue;
- toAdd.Add(hoIndex);
+ addedIndices.Add(objectIndex - newSectionStartIndex - 1);
}
}
// Sort the indices to ensure that removal + insertion indices don't get jumbled up post-removal or post-insertion.
// This isn't strictly required, but the differ makes no guarantees about order.
- toRemove.Sort();
- toAdd.Sort();
+ removedIndices.Sort();
+ addedIndices.Sort();
- editorBeatmap.BeginChange();
-
- // Apply the changes.
- for (int i = toRemove.Count - 1; i >= 0; i--)
- editorBeatmap.RemoveAt(toRemove[i]);
-
- if (toAdd.Count > 0)
- {
- IBeatmap newBeatmap = readBeatmap(newState);
- foreach (int i in toAdd)
- editorBeatmap.Insert(i, newBeatmap.HitObjects[i]);
- }
-
- editorBeatmap.EndChange();
+ removedIndices.Reverse();
}
private string readString(byte[] state) => Encoding.UTF8.GetString(state);