1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-22 22:17:46 +08:00

Merge pull request #18668 from smoogipoo/editor-controlpoint-undo-redo

This commit is contained in:
Dean Herbert 2022-06-23 04:02:24 +09:00 committed by GitHub
commit 97fcf8cec9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 266 additions and 109 deletions

View File

@ -374,7 +374,7 @@ namespace osu.Game.Rulesets.Osu.Mods
int i = 0;
double currentTime = timingPoint.Time;
while (!definitelyBigger(currentTime, mapEndTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint)
while (!definitelyBigger(currentTime, mapEndTime) && ReferenceEquals(controlPointInfo.TimingPointAt(currentTime), timingPoint))
{
beats.Add(Math.Floor(currentTime));
i++;

View File

@ -117,8 +117,8 @@ namespace osu.Game.Tests.Visual.Editing
// After placement these must be non-default as defaults are read-only.
AddAssert("Placed object has non-default control points", () =>
EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT &&
EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT);
!ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) &&
!ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
ReloadEditorToSameBeatmap();
@ -126,8 +126,8 @@ namespace osu.Game.Tests.Visual.Editing
// After placement these must be non-default as defaults are read-only.
AddAssert("Placed object still has non-default control points", () =>
EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT &&
EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT);
!ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) &&
!ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
}
[Test]

View File

@ -269,7 +269,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private TimingControlPoint getNextTimingPoint(TimingControlPoint current)
{
if (timingPoints[^1] == current)
if (ReferenceEquals(timingPoints[^1], current))
return current;
int index = timingPoints.IndexOf(current); // -1 means that this is a "default beat"
@ -281,7 +281,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
if (timingPoints.Count == 0) return 0;
if (timingPoints[^1] == current)
if (ReferenceEquals(timingPoints[^1], current))
{
Debug.Assert(BeatSyncSource.Clock != null);

View File

@ -1,8 +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.
#nullable disable
using System;
using Newtonsoft.Json;
using osu.Game.Graphics;
@ -11,7 +9,7 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
public abstract class ControlPoint : IComparable<ControlPoint>, IDeepCloneable<ControlPoint>
public abstract class ControlPoint : IComparable<ControlPoint>, IDeepCloneable<ControlPoint>, IEquatable<ControlPoint>
{
/// <summary>
/// The time at which the control point takes effect.
@ -30,7 +28,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
/// <param name="existing">An existing control point to compare with.</param>
/// <returns>Whether this <see cref="ControlPoint"/> is redundant when placed alongside <paramref name="existing"/>.</returns>
public abstract bool IsRedundant(ControlPoint existing);
public abstract bool IsRedundant(ControlPoint? existing);
/// <summary>
/// Create an unbound copy of this control point.
@ -48,5 +46,20 @@ namespace osu.Game.Beatmaps.ControlPoints
{
Time = other.Time;
}
public sealed override bool Equals(object? obj)
=> obj is ControlPoint otherControlPoint
&& Equals(otherControlPoint);
public virtual bool Equals(ControlPoint? other)
{
if (ReferenceEquals(other, null)) return false;
if (ReferenceEquals(other, this)) return true;
return Time == other.Time;
}
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => Time.GetHashCode();
}
}

View File

@ -1,18 +1,16 @@
// 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.
#nullable disable
using System;
using System.Linq;
using osu.Framework.Bindables;
namespace osu.Game.Beatmaps.ControlPoints
{
public class ControlPointGroup : IComparable<ControlPointGroup>
public class ControlPointGroup : IComparable<ControlPointGroup>, IEquatable<ControlPointGroup>
{
public event Action<ControlPoint> ItemAdded;
public event Action<ControlPoint> ItemRemoved;
public event Action<ControlPoint>? ItemAdded;
public event Action<ControlPoint>? ItemRemoved;
/// <summary>
/// The time at which the control point takes effect.
@ -48,5 +46,23 @@ namespace osu.Game.Beatmaps.ControlPoints
controlPoints.Remove(point);
ItemRemoved?.Invoke(point);
}
public sealed override bool Equals(object? obj)
=> obj is ControlPointGroup otherGroup
&& Equals(otherGroup);
public virtual bool Equals(ControlPointGroup? other)
=> other != null
&& Time == other.Time
&& ControlPoints.SequenceEqual(other.ControlPoints);
public override int GetHashCode()
{
HashCode hashCode = new HashCode();
hashCode.Add(Time);
foreach (var point in controlPoints)
hashCode.Add(point);
return hashCode.ToHashCode();
}
}
}

View File

@ -1,8 +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.
#nullable disable
using System;
using osu.Framework.Bindables;
using osu.Game.Graphics;
using osuTK.Graphics;
@ -12,7 +11,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <remarks>
/// Note that going forward, this control point type should always be assigned directly to HitObjects.
/// </remarks>
public class DifficultyControlPoint : ControlPoint
public class DifficultyControlPoint : ControlPoint, IEquatable<DifficultyControlPoint>
{
public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint
{
@ -41,7 +40,7 @@ namespace osu.Game.Beatmaps.ControlPoints
set => SliderVelocityBindable.Value = value;
}
public override bool IsRedundant(ControlPoint existing)
public override bool IsRedundant(ControlPoint? existing)
=> existing is DifficultyControlPoint existingDifficulty
&& SliderVelocity == existingDifficulty.SliderVelocity;
@ -51,5 +50,15 @@ namespace osu.Game.Beatmaps.ControlPoints
base.CopyFrom(other);
}
public override bool Equals(ControlPoint? other)
=> other is DifficultyControlPoint otherDifficultyControlPoint
&& Equals(otherDifficultyControlPoint);
public bool Equals(DifficultyControlPoint? other)
=> base.Equals(other)
&& SliderVelocity == other.SliderVelocity;
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity);
}
}

View File

@ -1,15 +1,14 @@
// 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.
#nullable disable
using System;
using osu.Framework.Bindables;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
public class EffectControlPoint : ControlPoint
public class EffectControlPoint : ControlPoint, IEquatable<EffectControlPoint>
{
public static readonly EffectControlPoint DEFAULT = new EffectControlPoint
{
@ -68,7 +67,7 @@ namespace osu.Game.Beatmaps.ControlPoints
set => KiaiModeBindable.Value = value;
}
public override bool IsRedundant(ControlPoint existing)
public override bool IsRedundant(ControlPoint? existing)
=> !OmitFirstBarLine
&& existing is EffectControlPoint existingEffect
&& KiaiMode == existingEffect.KiaiMode
@ -83,5 +82,17 @@ namespace osu.Game.Beatmaps.ControlPoints
base.CopyFrom(other);
}
public override bool Equals(ControlPoint? other)
=> other is EffectControlPoint otherEffectControlPoint
&& Equals(otherEffectControlPoint);
public bool Equals(EffectControlPoint? other)
=> base.Equals(other)
&& OmitFirstBarLine == other.OmitFirstBarLine
&& ScrollSpeed == other.ScrollSpeed
&& KiaiMode == other.KiaiMode;
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), OmitFirstBarLine, ScrollSpeed, KiaiMode);
}
}

View File

@ -1,8 +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.
#nullable disable
using System;
using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Graphics;
@ -13,7 +12,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <remarks>
/// Note that going forward, this control point type should always be assigned directly to HitObjects.
/// </remarks>
public class SampleControlPoint : ControlPoint
public class SampleControlPoint : ControlPoint, IEquatable<SampleControlPoint>
{
public const string DEFAULT_BANK = "normal";
@ -73,7 +72,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public virtual HitSampleInfo ApplyTo(HitSampleInfo hitSampleInfo)
=> hitSampleInfo.With(newBank: hitSampleInfo.Bank ?? SampleBank, newVolume: hitSampleInfo.Volume > 0 ? hitSampleInfo.Volume : SampleVolume);
public override bool IsRedundant(ControlPoint existing)
public override bool IsRedundant(ControlPoint? existing)
=> existing is SampleControlPoint existingSample
&& SampleBank == existingSample.SampleBank
&& SampleVolume == existingSample.SampleVolume;
@ -85,5 +84,16 @@ namespace osu.Game.Beatmaps.ControlPoints
base.CopyFrom(other);
}
public override bool Equals(ControlPoint? other)
=> other is SampleControlPoint otherSampleControlPoint
&& Equals(otherSampleControlPoint);
public bool Equals(SampleControlPoint? other)
=> base.Equals(other)
&& SampleBank == other.SampleBank
&& SampleVolume == other.SampleVolume;
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SampleBank, SampleVolume);
}
}

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;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics;
@ -8,7 +9,7 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
public class TimingControlPoint : ControlPoint
public class TimingControlPoint : ControlPoint, IEquatable<TimingControlPoint>
{
/// <summary>
/// The time signature at this control point.
@ -68,7 +69,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public double BPM => 60000 / BeatLength;
// Timing points are never redundant as they can change the time signature.
public override bool IsRedundant(ControlPoint existing) => false;
public override bool IsRedundant(ControlPoint? existing) => false;
public override void CopyFrom(ControlPoint other)
{
@ -77,5 +78,16 @@ namespace osu.Game.Beatmaps.ControlPoints
base.CopyFrom(other);
}
public override bool Equals(ControlPoint? other)
=> other is TimingControlPoint otherTimingControlPoint
&& Equals(otherTimingControlPoint);
public bool Equals(TimingControlPoint? other)
=> base.Equals(other)
&& TimeSignature.Equals(other.TimeSignature)
&& BeatLength.Equals(other.BeatLength);
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), TimeSignature, BeatLength);
}
}

View File

@ -1,8 +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.
#nullable disable
using System;
using System.Collections.Generic;
using osu.Framework.Extensions;
@ -145,7 +143,7 @@ namespace osu.Game.Beatmaps.Formats
protected string CleanFilename(string path) => path.Trim('"').ToStandardisedPath();
protected enum Section
public enum Section
{
General,
Editor,
@ -162,7 +160,7 @@ namespace osu.Game.Beatmaps.Formats
}
[Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")]
public class LegacyDifficultyControlPoint : DifficultyControlPoint
public class LegacyDifficultyControlPoint : DifficultyControlPoint, IEquatable<LegacyDifficultyControlPoint>
{
/// <summary>
/// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
@ -188,9 +186,20 @@ namespace osu.Game.Beatmaps.Formats
BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier;
}
public override bool Equals(ControlPoint? other)
=> other is LegacyDifficultyControlPoint otherLegacyDifficultyControlPoint
&& Equals(otherLegacyDifficultyControlPoint);
public bool Equals(LegacyDifficultyControlPoint? other)
=> base.Equals(other)
&& BpmMultiplier == other.BpmMultiplier;
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier);
}
internal class LegacySampleControlPoint : SampleControlPoint
internal class LegacySampleControlPoint : SampleControlPoint, IEquatable<LegacySampleControlPoint>
{
public int CustomSampleBank;
@ -204,7 +213,7 @@ namespace osu.Game.Beatmaps.Formats
return baseInfo;
}
public override bool IsRedundant(ControlPoint existing)
public override bool IsRedundant(ControlPoint? existing)
=> base.IsRedundant(existing)
&& existing is LegacySampleControlPoint existingSample
&& CustomSampleBank == existingSample.CustomSampleBank;
@ -215,6 +224,17 @@ namespace osu.Game.Beatmaps.Formats
CustomSampleBank = ((LegacySampleControlPoint)other).CustomSampleBank;
}
public override bool Equals(ControlPoint? other)
=> other is LegacySampleControlPoint otherLegacySampleControlPoint
&& Equals(otherLegacySampleControlPoint);
public bool Equals(LegacySampleControlPoint? other)
=> base.Equals(other)
&& CustomSampleBank == other.CustomSampleBank;
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank);
}
}
}

View File

@ -127,7 +127,7 @@ namespace osu.Game.Graphics.Containers
TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
if (timingPoint == lastTimingPoint && beatIndex == lastBeat)
if (ReferenceEquals(timingPoint, lastTimingPoint) && beatIndex == lastBeat)
return;
// as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat.

View File

@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Objects
if (legacyInfo != null)
DifficultyControlPoint = (DifficultyControlPoint)legacyInfo.DifficultyPointAt(StartTime).DeepClone();
else if (DifficultyControlPoint == DifficultyControlPoint.DEFAULT)
else if (ReferenceEquals(DifficultyControlPoint, DifficultyControlPoint.DEFAULT))
DifficultyControlPoint = new DifficultyControlPoint();
DifficultyControlPoint.Time = StartTime;
@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Objects
// This is done here after ApplyDefaultsToSelf as we may require custom defaults to be applied to have an accurate end time.
if (legacyInfo != null)
SampleControlPoint = (SampleControlPoint)legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency).DeepClone();
else if (SampleControlPoint == SampleControlPoint.DEFAULT)
else if (ReferenceEquals(SampleControlPoint, SampleControlPoint.DEFAULT))
SampleControlPoint = new SampleControlPoint();
SampleControlPoint.Time = this.GetEndTime() + control_point_leniency;

View File

@ -49,7 +49,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
case NotifyCollectionChangedAction.Remove:
foreach (var group in args.OldItems.OfType<ControlPointGroup>())
{
var matching = Children.SingleOrDefault(gv => gv.Group == group);
var matching = Children.SingleOrDefault(gv => ReferenceEquals(gv.Group, group));
if (matching != null)
matching.Expire();

View File

@ -40,7 +40,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
case NotifyCollectionChangedAction.Remove:
foreach (var group in args.OldItems.OfType<ControlPointGroup>())
{
var matching = Children.SingleOrDefault(gv => gv.Group == group);
var matching = Children.SingleOrDefault(gv => ReferenceEquals(gv.Group, group));
matching?.Expire();
}

View File

@ -205,7 +205,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
updateRepeats(repeats);
}
if (difficultyControlPoint != Item.DifficultyControlPoint)
if (!ReferenceEquals(difficultyControlPoint, Item.DifficultyControlPoint))
{
difficultyControlPoint = Item.DifficultyControlPoint;
difficultyOverrideDisplay?.Expire();
@ -220,7 +220,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
if (sampleControlPoint != Item.SampleControlPoint)
if (!ReferenceEquals(sampleControlPoint, Item.SampleControlPoint))
{
sampleControlPoint = Item.SampleControlPoint;
sampleOverrideDisplay?.Expire();
@ -393,7 +393,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (e.CurrentState.Keyboard.ShiftPressed)
{
if (hitObject.DifficultyControlPoint == DifficultyControlPoint.DEFAULT)
if (ReferenceEquals(hitObject.DifficultyControlPoint, DifficultyControlPoint.DEFAULT))
hitObject.DifficultyControlPoint = new DifficultyControlPoint();
double newVelocity = hitObject.DifficultyControlPoint.SliderVelocity * (repeatHitObject.Duration / proposedDuration);

View File

@ -73,31 +73,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;
@ -110,6 +86,39 @@ namespace osu.Game.Screens.Edit
trackStartTime(obj);
}
/// <summary>
/// Converts a <see cref="ControlPointInfo"/> such that the resultant <see cref="ControlPointInfo"/> is non-legacy.
/// </summary>
/// <param name="incoming">The <see cref="ControlPointInfo"/> to convert.</param>
/// <returns>The non-legacy <see cref="ControlPointInfo"/>. <paramref name="incoming"/> is returned if already non-legacy.</returns>
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;

View File

@ -141,7 +141,7 @@ namespace osu.Game.Screens.Edit
seekTime = timingPoint.Time + closestBeat * seekAmount;
}
if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First())
if (seekTime < timingPoint.Time && !ReferenceEquals(timingPoint, ControlPointInfo.TimingPoints.First()))
seekTime = timingPoint.Time;
SeekSmoothlyTo(seekTime);

View File

@ -97,7 +97,7 @@ namespace osu.Game.Screens.Edit
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
hoveredBackground.Colour = colourHover = colours.Background1;
colourHover = colours.Background1;
colourSelected = colours.Colour3;
}
@ -105,8 +105,7 @@ namespace osu.Game.Screens.Edit
{
base.LoadComplete();
// Reduce flicker of rows when offset is being changed rapidly.
// Probably need to reconsider this.
updateState();
FinishTransforms(true);
}

View File

@ -5,13 +5,16 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
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.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Skinning;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
@ -34,61 +37,107 @@ namespace osu.Game.Screens.Edit
{
// Diff the beatmaps
var result = new Differ().CreateLineDiffs(readString(currentState), readString(newState), true, false);
IBeatmap newBeatmap = null;
// 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]");
editorBeatmap.BeginChange();
processHitObjects(result, () => newBeatmap ??= readBeatmap(newState));
processTimingPoints(() => newBeatmap ??= readBeatmap(newState));
editorBeatmap.EndChange();
}
Debug.Assert(oldHitObjectsIndex >= 0);
Debug.Assert(newHitObjectsIndex >= 0);
private void processTimingPoints(Func<IBeatmap> getNewBeatmap)
{
ControlPointInfo newControlPoints = EditorBeatmap.ConvertControlPoints(getNewBeatmap().ControlPointInfo);
var toRemove = new List<int>();
var toAdd = new List<int>();
// Remove all groups from the current beatmap which don't have a corresponding equal group in the new beatmap.
foreach (var oldGroup in editorBeatmap.ControlPointInfo.Groups.ToArray())
{
var newGroup = newControlPoints.GroupAt(oldGroup.Time);
if (!oldGroup.Equals(newGroup))
editorBeatmap.ControlPointInfo.RemoveGroup(oldGroup);
}
// Add all groups from the new beatmap which don't have a corresponding equal group in the old beatmap.
foreach (var newGroup in newControlPoints.Groups)
{
var oldGroup = editorBeatmap.ControlPointInfo.GroupAt(newGroup.Time);
if (!newGroup.Equals(oldGroup))
{
foreach (var point in newGroup.ControlPoints)
editorBeatmap.ControlPointInfo.Add(newGroup.Time, point);
}
}
}
private void processHitObjects(DiffResult result, Func<IBeatmap> getNewBeatmap)
{
findChangedIndices(result, LegacyDecoder<Beatmap>.Section.HitObjects, out var removedIndices, out var addedIndices);
for (int i = removedIndices.Count - 1; i >= 0; i--)
editorBeatmap.RemoveAt(removedIndices[i]);
if (addedIndices.Count > 0)
{
var newBeatmap = getNewBeatmap();
foreach (int i in addedIndices)
editorBeatmap.Insert(i, newBeatmap.HitObjects[i]);
}
}
private void findChangedIndices(DiffResult result, LegacyDecoder<Beatmap>.Section section, out List<int> removedIndices, out List<int> addedIndices)
{
removedIndices = new List<int>();
addedIndices = new List<int>();
// Find the start and end indices of the relevant section headers in both the old and the new beatmap file. Lines changed outside of the modified ranges are ignored.
int oldSectionStartIndex = Array.IndexOf(result.PiecesOld, $"[{section}]");
if (oldSectionStartIndex == -1)
return;
int oldSectionEndIndex = Array.FindIndex(result.PiecesOld, oldSectionStartIndex + 1, s => s.StartsWith('['));
if (oldSectionEndIndex == -1)
oldSectionEndIndex = result.PiecesOld.Length;
int newSectionStartIndex = Array.IndexOf(result.PiecesNew, $"[{section}]");
if (newSectionStartIndex == -1)
return;
int newSectionEndIndex = Array.FindIndex(result.PiecesNew, newSectionStartIndex + 1, s => s.StartsWith('['));
if (newSectionEndIndex == -1)
newSectionEndIndex = result.PiecesNew.Length;
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();
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.Sort();
addedIndices.Sort();
}
private string readString(byte[] state) => Encoding.UTF8.GetString(state);

View File

@ -54,6 +54,8 @@ namespace osu.Game.Screens.Edit.Timing
Columns = createHeaders();
Content = value.Select(createContent).ToArray().ToRectangular();
updateSelectedGroup();
}
}
@ -64,10 +66,17 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.BindValueChanged(group =>
{
// TODO: This should scroll the selected row into view.
foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
updateSelectedGroup();
}, true);
}
private void updateSelectedGroup()
{
// TODO: This should scroll the selected row into view.
foreach (var b in BackgroundFlow)
b.Selected = ReferenceEquals(b.Item, selectedGroup?.Value);
}
private TableColumn[] createHeaders()
{
var columns = new List<TableColumn>

View File

@ -222,7 +222,7 @@ namespace osu.Game.Screens.Edit.Timing
// Try and create matching types from the currently selected control point.
var selected = selectedGroup.Value;
if (selected != null && selected != group)
if (selected != null && !ReferenceEquals(selected, group))
{
foreach (var controlPoint in selected.ControlPoints)
{

View File

@ -128,7 +128,7 @@ namespace osu.Game.Screens.Edit.Timing
double? offsetChange = newStartTime - selectedGroupStartTime;
var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints
.SkipWhile(g => g != tcp)
.SkipWhile(g => !ReferenceEquals(g, tcp))
.Skip(1)
.FirstOrDefault();