Merge pull request #8696 from smoogipoo/editor-undo-redo

Add undo/redo support to the Editor
This commit is contained in:
Dean Herbert 2020-04-13 19:12:47 +09:00 committed by GitHub
commit ef5e88dd82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 856 additions and 5 deletions

View File

@ -0,0 +1,342 @@
// 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.IO;
using System.Text;
using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
namespace osu.Game.Tests.Editor
{
[TestFixture]
public class LegacyEditorBeatmapPatcherTest
{
private LegacyEditorBeatmapPatcher patcher;
private EditorBeatmap current;
[SetUp]
public void Setup()
{
patcher = new LegacyEditorBeatmapPatcher(current = new EditorBeatmap(new OsuBeatmap
{
BeatmapInfo =
{
Ruleset = new OsuRuleset().RulesetInfo
}
}));
}
[Test]
public void TestAddHitObject()
{
var patch = new OsuBeatmap
{
HitObjects =
{
new HitCircle { StartTime = 1000 }
}
};
runTest(patch);
}
[Test]
public void TestInsertHitObject()
{
current.AddRange(new[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 3000 },
});
var patch = new OsuBeatmap
{
HitObjects =
{
(OsuHitObject)current.HitObjects[0],
new HitCircle { StartTime = 2000 },
(OsuHitObject)current.HitObjects[1],
}
};
runTest(patch);
}
[Test]
public void TestDeleteHitObject()
{
current.AddRange(new[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 3000 },
});
var patch = new OsuBeatmap
{
HitObjects =
{
(OsuHitObject)current.HitObjects[0],
(OsuHitObject)current.HitObjects[2],
}
};
runTest(patch);
}
[Test]
public void TestChangeStartTime()
{
current.AddRange(new[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 3000 },
});
var patch = new OsuBeatmap
{
HitObjects =
{
new HitCircle { StartTime = 500 },
(OsuHitObject)current.HitObjects[1],
(OsuHitObject)current.HitObjects[2],
}
};
runTest(patch);
}
[Test]
public void TestChangeSample()
{
current.AddRange(new[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 3000 },
});
var patch = new OsuBeatmap
{
HitObjects =
{
(OsuHitObject)current.HitObjects[0],
new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } },
(OsuHitObject)current.HitObjects[2],
}
};
runTest(patch);
}
[Test]
public void TestChangeSliderPath()
{
current.AddRange(new OsuHitObject[]
{
new HitCircle { StartTime = 1000 },
new Slider
{
StartTime = 2000,
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero),
new PathControlPoint(Vector2.One),
new PathControlPoint(new Vector2(2), PathType.Bezier),
new PathControlPoint(new Vector2(3)),
}, 50)
},
new HitCircle { StartTime = 3000 },
});
var patch = new OsuBeatmap
{
HitObjects =
{
(OsuHitObject)current.HitObjects[0],
new Slider
{
StartTime = 2000,
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.Bezier),
new PathControlPoint(new Vector2(4)),
new PathControlPoint(new Vector2(5)),
}, 100)
},
(OsuHitObject)current.HitObjects[2],
}
};
runTest(patch);
}
[Test]
public void TestAddMultipleHitObjects()
{
current.AddRange(new[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 3000 },
});
var patch = new OsuBeatmap
{
HitObjects =
{
new HitCircle { StartTime = 500 },
(OsuHitObject)current.HitObjects[0],
new HitCircle { StartTime = 1500 },
(OsuHitObject)current.HitObjects[1],
new HitCircle { StartTime = 2250 },
new HitCircle { StartTime = 2500 },
(OsuHitObject)current.HitObjects[2],
new HitCircle { StartTime = 3500 },
}
};
runTest(patch);
}
[Test]
public void TestDeleteMultipleHitObjects()
{
current.AddRange(new[]
{
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1500 },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 2250 },
new HitCircle { StartTime = 2500 },
new HitCircle { StartTime = 3000 },
new HitCircle { StartTime = 3500 },
});
var patch = new OsuBeatmap
{
HitObjects =
{
(OsuHitObject)current.HitObjects[1],
(OsuHitObject)current.HitObjects[3],
(OsuHitObject)current.HitObjects[6],
}
};
runTest(patch);
}
[Test]
public void TestChangeSamplesOfMultipleHitObjects()
{
current.AddRange(new[]
{
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1500 },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 2250 },
new HitCircle { StartTime = 2500 },
new HitCircle { StartTime = 3000 },
new HitCircle { StartTime = 3500 },
});
var patch = new OsuBeatmap
{
HitObjects =
{
(OsuHitObject)current.HitObjects[0],
new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } },
(OsuHitObject)current.HitObjects[2],
(OsuHitObject)current.HitObjects[3],
new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE } } },
(OsuHitObject)current.HitObjects[5],
new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP } } },
(OsuHitObject)current.HitObjects[7],
}
};
runTest(patch);
}
[Test]
public void TestAddAndDeleteHitObjects()
{
current.AddRange(new[]
{
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1500 },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 2250 },
new HitCircle { StartTime = 2500 },
new HitCircle { StartTime = 3000 },
new HitCircle { StartTime = 3500 },
});
var patch = new OsuBeatmap
{
HitObjects =
{
new HitCircle { StartTime = 750 },
(OsuHitObject)current.HitObjects[1],
(OsuHitObject)current.HitObjects[4],
(OsuHitObject)current.HitObjects[5],
new HitCircle { StartTime = 2650 },
new HitCircle { StartTime = 2750 },
new HitCircle { StartTime = 4000 },
}
};
runTest(patch);
}
private void runTest(IBeatmap patch)
{
// Due to the method of testing, "patch" comes in without having been decoded via a beatmap decoder.
// This causes issues because the decoder adds various default properties (e.g. new combo on first object, default samples).
// To resolve "patch" into a sane state it is encoded and then re-decoded.
patch = decode(encode(patch));
// Apply the patch.
patcher.Patch(encode(current), encode(patch));
// Convert beatmaps to strings for assertion purposes.
string currentStr = Encoding.ASCII.GetString(encode(current));
string patchStr = Encoding.ASCII.GetString(encode(patch));
Assert.That(currentStr, Is.EqualTo(patchStr));
}
private byte[] encode(IBeatmap beatmap)
{
using (var encoded = new MemoryStream())
{
using (var sw = new StreamWriter(encoded))
new LegacyBeatmapEncoder(beatmap).Encode(sw);
return encoded.ToArray();
}
}
private IBeatmap decode(byte[] state)
{
using (var stream = new MemoryStream(state))
using (var reader = new LineBufferedReader(stream))
return Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
}
}
}

View File

@ -0,0 +1,195 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editor
{
public class TestSceneEditorChangeStates : ScreenTestScene
{
private EditorBeatmap editorBeatmap;
public override void SetUpSteps()
{
base.SetUpSteps();
Screens.Edit.Editor editor = null;
AddStep("load editor", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
LoadScreen(editor = new Screens.Edit.Editor());
});
AddUntilStep("wait for editor to load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true);
AddStep("get beatmap", () => editorBeatmap = editor.ChildrenOfType<EditorBeatmap>().Single());
}
[Test]
public void TestUndoFromInitialState()
{
int hitObjectCount = 0;
AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count);
addUndoSteps();
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
}
[Test]
public void TestRedoFromInitialState()
{
int hitObjectCount = 0;
AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count);
addRedoSteps();
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
}
[Test]
public void TestAddObjectAndUndo()
{
HitObject addedObject = null;
HitObject removedObject = null;
HitObject expectedObject = null;
AddStep("bind removal", () =>
{
editorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h;
});
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddAssert("hitobject added", () => addedObject == expectedObject);
addUndoSteps();
AddAssert("hitobject removed", () => removedObject == expectedObject);
}
[Test]
public void TestAddObjectThenUndoThenRedo()
{
HitObject addedObject = null;
HitObject removedObject = null;
HitObject expectedObject = null;
AddStep("bind removal", () =>
{
editorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h;
});
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
addUndoSteps();
AddStep("reset variables", () =>
{
addedObject = null;
removedObject = null;
});
addRedoSteps();
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
AddAssert("no hitobject removed", () => removedObject == null);
}
[Test]
public void TestRemoveObjectThenUndo()
{
HitObject addedObject = null;
HitObject removedObject = null;
HitObject expectedObject = null;
AddStep("bind removal", () =>
{
editorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h;
});
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddStep("remove object", () => editorBeatmap.Remove(expectedObject));
AddStep("reset variables", () =>
{
addedObject = null;
removedObject = null;
});
addUndoSteps();
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
AddAssert("no hitobject removed", () => removedObject == null);
}
[Test]
public void TestRemoveObjectThenUndoThenRedo()
{
HitObject addedObject = null;
HitObject removedObject = null;
HitObject expectedObject = null;
AddStep("bind removal", () =>
{
editorBeatmap.HitObjectAdded += h => addedObject = h;
editorBeatmap.HitObjectRemoved += h => removedObject = h;
});
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddStep("remove object", () => editorBeatmap.Remove(expectedObject));
addUndoSteps();
AddStep("reset variables", () =>
{
addedObject = null;
removedObject = null;
});
addRedoSteps();
AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo)
AddAssert("no hitobject added", () => addedObject == null);
}
private void addUndoSteps()
{
AddStep("press undo", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.PressKey(Key.Z);
});
AddStep("release keys", () =>
{
InputManager.ReleaseKey(Key.LControl);
InputManager.ReleaseKey(Key.Z);
});
}
private void addRedoSteps()
{
AddStep("press redo", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.PressKey(Key.LShift);
InputManager.PressKey(Key.Z);
});
AddStep("release keys", () =>
{
InputManager.ReleaseKey(Key.LControl);
InputManager.ReleaseKey(Key.LShift);
InputManager.ReleaseKey(Key.Z);
});
}
}
}

View File

@ -62,6 +62,7 @@ namespace osu.Game.Screens.Edit
private IBeatmap playableBeatmap;
private EditorBeatmap editorBeatmap;
private EditorChangeHandler changeHandler;
private DependencyContainer dependencies;
@ -100,9 +101,11 @@ namespace osu.Game.Screens.Edit
}
AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap));
dependencies.CacheAs(editorBeatmap);
changeHandler = new EditorChangeHandler(editorBeatmap);
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
EditorMenuBar menuBar;
var fileMenuItems = new List<MenuItem>
@ -147,6 +150,14 @@ namespace osu.Game.Screens.Edit
new MenuItem("File")
{
Items = fileMenuItems
},
new MenuItem("Edit")
{
Items = new[]
{
new EditorMenuItem("Undo", MenuItemType.Standard, undo),
new EditorMenuItem("Redo", MenuItemType.Standard, redo)
}
}
}
}
@ -233,6 +244,19 @@ namespace osu.Game.Screens.Edit
return true;
}
break;
case Key.Z:
if (e.ControlPressed)
{
if (e.ShiftPressed)
redo();
else
undo();
return true;
}
break;
}
@ -297,6 +321,10 @@ namespace osu.Game.Screens.Edit
return base.OnExiting(next);
}
private void undo() => changeHandler.RestoreState(-1);
private void redo() => changeHandler.RestoreState(1);
private void resetTrack(bool seekToStart = false)
{
Beatmap.Value.Track?.Stop();

View File

@ -120,6 +120,16 @@ namespace osu.Game.Screens.Edit
private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;
/// <summary>
/// Adds a collection of <see cref="HitObject"/>s to this <see cref="EditorBeatmap"/>.
/// </summary>
/// <param name="hitObjects">The <see cref="HitObject"/>s to add.</param>
public void AddRange(IEnumerable<HitObject> hitObjects)
{
foreach (var h in hitObjects)
Add(h);
}
/// <summary>
/// Adds a <see cref="HitObject"/> to this <see cref="EditorBeatmap"/>.
/// </summary>
@ -141,12 +151,34 @@ namespace osu.Game.Screens.Edit
/// Removes a <see cref="HitObject"/> from this <see cref="EditorBeatmap"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to add.</param>
public void Remove(HitObject hitObject)
/// <returns>True if the <see cref="HitObject"/> has been removed, false otherwise.</returns>
public bool Remove(HitObject hitObject)
{
if (!mutableHitObjects.Contains(hitObject))
return;
int index = FindIndex(hitObject);
mutableHitObjects.Remove(hitObject);
if (index == -1)
return false;
RemoveAt(index);
return true;
}
/// <summary>
/// Finds the index of a <see cref="HitObject"/> in this <see cref="EditorBeatmap"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to search for.</param>
/// <returns>The index of <paramref name="hitObject"/>.</returns>
public int FindIndex(HitObject hitObject) => mutableHitObjects.IndexOf(hitObject);
/// <summary>
/// Removes a <see cref="HitObject"/> at an index in this <see cref="EditorBeatmap"/>.
/// </summary>
/// <param name="index">The index of the <see cref="HitObject"/> to remove.</param>
public void RemoveAt(int index)
{
var hitObject = (HitObject)mutableHitObjects[index];
mutableHitObjects.RemoveAt(index);
var bindable = startTimeBindables[hitObject];
bindable.UnbindAll();

View File

@ -0,0 +1,112 @@
// 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.Collections.Generic;
using System.IO;
using System.Text;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// Tracks changes to the <see cref="Editor"/>.
/// </summary>
public class EditorChangeHandler : IEditorChangeHandler
{
private readonly LegacyEditorBeatmapPatcher patcher;
private readonly List<byte[]> savedStates = new List<byte[]>();
private int currentState = -1;
private readonly EditorBeatmap editorBeatmap;
private int bulkChangesStarted;
private bool isRestoring;
/// <summary>
/// Creates a new <see cref="EditorChangeHandler"/>.
/// </summary>
/// <param name="editorBeatmap">The <see cref="EditorBeatmap"/> to track the <see cref="HitObject"/>s of.</param>
public EditorChangeHandler(EditorBeatmap editorBeatmap)
{
this.editorBeatmap = editorBeatmap;
editorBeatmap.HitObjectAdded += hitObjectAdded;
editorBeatmap.HitObjectRemoved += hitObjectRemoved;
editorBeatmap.HitObjectUpdated += hitObjectUpdated;
patcher = new LegacyEditorBeatmapPatcher(editorBeatmap);
// Initial state.
SaveState();
}
private void hitObjectAdded(HitObject obj) => SaveState();
private void hitObjectRemoved(HitObject obj) => SaveState();
private void hitObjectUpdated(HitObject obj) => SaveState();
public void BeginChange() => bulkChangesStarted++;
public void EndChange()
{
if (bulkChangesStarted == 0)
throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}.");
if (--bulkChangesStarted == 0)
SaveState();
}
/// <summary>
/// Saves the current <see cref="Editor"/> state.
/// </summary>
public void SaveState()
{
if (bulkChangesStarted > 0)
return;
if (isRestoring)
return;
if (currentState < savedStates.Count - 1)
savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1);
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(editorBeatmap).Encode(sw);
savedStates.Add(stream.ToArray());
}
currentState = savedStates.Count - 1;
}
/// <summary>
/// Restores an older or newer state.
/// </summary>
/// <param name="direction">The direction to restore in. If less than 0, an older state will be used. If greater than 0, a newer state will be used.</param>
public void RestoreState(int direction)
{
if (bulkChangesStarted > 0)
return;
if (savedStates.Count == 0)
return;
int newState = Math.Clamp(currentState + direction, 0, savedStates.Count - 1);
if (currentState == newState)
return;
isRestoring = true;
patcher.Patch(savedStates[currentState], savedStates[newState]);
currentState = newState;
isRestoring = false;
}
}
}

View File

@ -0,0 +1,33 @@
// 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.Rulesets.Objects;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// Interface for a component that manages changes in the <see cref="Editor"/>.
/// </summary>
public interface IEditorChangeHandler
{
/// <summary>
/// Begins a bulk state change event. <see cref="EndChange"/> should be invoked soon after.
/// </summary>
/// <remarks>
/// This should be invoked when multiple changes to the <see cref="Editor"/> should be bundled together into one state change event.
/// When nested invocations are involved, a state change will not occur until an equal number of invocations of <see cref="EndChange"/> are received.
/// </remarks>
/// <example>
/// When a group of <see cref="HitObject"/>s are deleted, a single undo and redo state change should update the state of all <see cref="HitObject"/>.
/// </example>
void BeginChange();
/// <summary>
/// Ends a bulk state change event.
/// </summary>
/// <remarks>
/// This should be invoked as soon as possible after <see cref="BeginChange"/> to cause a state change.
/// </remarks>
void EndChange();
}
}

View File

@ -0,0 +1,107 @@
// 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.Collections.Generic;
using System.IO;
using System.Text;
using DiffPlex;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.IO;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// Patches an <see cref="EditorBeatmap"/> based on the difference between two legacy (.osu) states.
/// </summary>
public class LegacyEditorBeatmapPatcher
{
private readonly EditorBeatmap editorBeatmap;
public LegacyEditorBeatmapPatcher(EditorBeatmap editorBeatmap)
{
this.editorBeatmap = editorBeatmap;
}
public void Patch(byte[] currentState, byte[] newState)
{
// Diff the beatmaps
var result = new Differ().CreateLineDiffs(readString(currentState), readString(newState), true, false);
// 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]");
var toRemove = new List<int>();
var toAdd = new List<int>();
foreach (var block in result.DiffBlocks)
{
// Removed hitobjects
for (int i = 0; i < block.DeleteCountA; i++)
{
int hoIndex = block.DeleteStartA + i - oldHitObjectsIndex - 1;
if (hoIndex < 0)
continue;
toRemove.Add(hoIndex);
}
// Added hitobjects
for (int i = 0; i < block.InsertCountB; i++)
{
int hoIndex = block.InsertStartB + i - newHitObjectsIndex - 1;
if (hoIndex < 0)
continue;
toAdd.Add(hoIndex);
}
}
// Make the removal indices are sorted so that iteration order doesn't get messed up post-removal.
toRemove.Sort();
// 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 (var i in toAdd)
editorBeatmap.Add(newBeatmap.HitObjects[i]);
}
}
private string readString(byte[] state) => Encoding.UTF8.GetString(state);
private IBeatmap readBeatmap(byte[] state)
{
using (var stream = new MemoryStream(state))
using (var reader = new LineBufferedReader(stream, true))
return new PassThroughWorkingBeatmap(Decoder.GetDecoder<Beatmap>(reader).Decode(reader)).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset);
}
private class PassThroughWorkingBeatmap : WorkingBeatmap
{
private readonly IBeatmap beatmap;
public PassThroughWorkingBeatmap(IBeatmap beatmap)
: base(beatmap.BeatmapInfo, null)
{
this.beatmap = beatmap;
}
protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetTrack() => throw new NotImplementedException();
}
}
}

View File

@ -18,6 +18,7 @@
</None>
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="DiffPlex" Version="1.6.1" />
<PackageReference Include="Humanizer" Version="2.7.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />

View File

@ -75,6 +75,7 @@
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies">
<PackageReference Include="DiffPlex" Version="1.6.1" />
<PackageReference Include="Humanizer" Version="2.7.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />