mirror of
https://github.com/ppy/osu.git
synced 2024-11-15 09:47:24 +08:00
Merge pull request #28382 from Hecatia-Lapislazuli/move-already-placed-objects-when-adjusting-offset-bpm
Implemented ability to adjust already-placed objects when changing timing offsets
This commit is contained in:
commit
8605639e67
161
osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs
Normal file
161
osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs
Normal file
@ -0,0 +1,161 @@
|
||||
// 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;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
|
||||
namespace osu.Game.Tests.Editing
|
||||
{
|
||||
[TestFixture]
|
||||
public class TimingSectionAdjustmentsTest
|
||||
{
|
||||
[Test]
|
||||
public void TestOffsetAdjustment()
|
||||
{
|
||||
var controlPoints = new ControlPointInfo();
|
||||
|
||||
controlPoints.Add(100, new TimingControlPoint { BeatLength = 100 });
|
||||
controlPoints.Add(50_000, new TimingControlPoint { BeatLength = 200 });
|
||||
controlPoints.Add(100_000, new TimingControlPoint { BeatLength = 50 });
|
||||
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
ControlPointInfo = controlPoints,
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0 },
|
||||
new HitCircle { StartTime = 200 },
|
||||
new HitCircle { StartTime = 49_900 },
|
||||
new HitCircle { StartTime = 50_000 },
|
||||
new HitCircle { StartTime = 50_200 },
|
||||
new HitCircle { StartTime = 99_800 },
|
||||
new HitCircle { StartTime = 100_000 },
|
||||
new HitCircle { StartTime = 100_050 },
|
||||
new HitCircle { StartTime = 100_550 },
|
||||
}
|
||||
};
|
||||
|
||||
moveTimingPoint(beatmap, 100, -50);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(beatmap.HitObjects[0].StartTime, Is.EqualTo(-50));
|
||||
Assert.That(beatmap.HitObjects[1].StartTime, Is.EqualTo(150));
|
||||
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(49_850));
|
||||
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(50_000));
|
||||
});
|
||||
|
||||
moveTimingPoint(beatmap, 50_000, 1_000);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(49_850));
|
||||
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(51_000));
|
||||
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(51_200));
|
||||
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(100_800));
|
||||
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(100_000));
|
||||
});
|
||||
|
||||
moveTimingPoint(beatmap, 100_000, 10_000);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(51_200));
|
||||
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(110_800));
|
||||
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(110_000));
|
||||
Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(110_050));
|
||||
Assert.That(beatmap.HitObjects[8].StartTime, Is.EqualTo(110_550));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBPMAdjustment()
|
||||
{
|
||||
var controlPoints = new ControlPointInfo();
|
||||
|
||||
controlPoints.Add(100, new TimingControlPoint { BeatLength = 100 });
|
||||
controlPoints.Add(50_000, new TimingControlPoint { BeatLength = 200 });
|
||||
controlPoints.Add(100_000, new TimingControlPoint { BeatLength = 50 });
|
||||
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
ControlPointInfo = controlPoints,
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0 },
|
||||
new HitCircle { StartTime = 200 },
|
||||
new Spinner { StartTime = 500, EndTime = 1000 },
|
||||
new HitCircle { StartTime = 49_900 },
|
||||
new HitCircle { StartTime = 50_000 },
|
||||
new HitCircle { StartTime = 50_200 },
|
||||
new HitCircle { StartTime = 99_800 },
|
||||
new HitCircle { StartTime = 100_000 },
|
||||
new HitCircle { StartTime = 100_050 },
|
||||
new HitCircle { StartTime = 100_550 },
|
||||
}
|
||||
};
|
||||
|
||||
adjustBeatLength(beatmap, 100, 50);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(beatmap.HitObjects[0].StartTime, Is.EqualTo(50));
|
||||
Assert.That(beatmap.HitObjects[1].StartTime, Is.EqualTo(150));
|
||||
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(300));
|
||||
Assert.That(beatmap.HitObjects[2].GetEndTime(), Is.EqualTo(550));
|
||||
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(25_000));
|
||||
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(50_000));
|
||||
});
|
||||
|
||||
adjustBeatLength(beatmap, 50_000, 400);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(300));
|
||||
Assert.That(beatmap.HitObjects[2].GetEndTime(), Is.EqualTo(550));
|
||||
Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(25_000));
|
||||
Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(50_000));
|
||||
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(50_400));
|
||||
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(149_600));
|
||||
Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(100_000));
|
||||
});
|
||||
|
||||
adjustBeatLength(beatmap, 100_000, 100);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(50_400));
|
||||
Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(199_200));
|
||||
Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(100_000));
|
||||
Assert.That(beatmap.HitObjects[8].StartTime, Is.EqualTo(100_100));
|
||||
Assert.That(beatmap.HitObjects[9].StartTime, Is.EqualTo(101_100));
|
||||
});
|
||||
}
|
||||
|
||||
private static void moveTimingPoint(IBeatmap beatmap, double originalTime, double adjustment)
|
||||
{
|
||||
var controlPoints = beatmap.ControlPointInfo;
|
||||
var controlPointGroup = controlPoints.GroupAt(originalTime);
|
||||
var timingPoint = controlPointGroup.ControlPoints.OfType<TimingControlPoint>().Single();
|
||||
controlPoints.RemoveGroup(controlPointGroup);
|
||||
TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, timingPoint, adjustment);
|
||||
controlPoints.Add(originalTime - adjustment, timingPoint);
|
||||
}
|
||||
|
||||
private static void adjustBeatLength(IBeatmap beatmap, double groupTime, double newBeatLength)
|
||||
{
|
||||
var controlPoints = beatmap.ControlPointInfo;
|
||||
var controlPointGroup = controlPoints.GroupAt(groupTime);
|
||||
var timingPoint = controlPointGroup.ControlPoints.OfType<TimingControlPoint>().Single();
|
||||
double oldBeatLength = timingPoint.BeatLength;
|
||||
timingPoint.BeatLength = newBeatLength;
|
||||
TimingSectionAdjustments.SetHitObjectBPM(beatmap, timingPoint, oldBeatLength);
|
||||
}
|
||||
}
|
||||
}
|
@ -196,6 +196,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.EditorShowSpeedChanges, false);
|
||||
SetDefault(OsuSetting.EditorScaleOrigin, EditorOrigin.GridCentre);
|
||||
SetDefault(OsuSetting.EditorRotationOrigin, EditorOrigin.GridCentre);
|
||||
SetDefault(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges, true);
|
||||
|
||||
SetDefault(OsuSetting.HideCountryFlags, false);
|
||||
|
||||
@ -442,5 +443,6 @@ namespace osu.Game.Configuration
|
||||
EditorScaleOrigin,
|
||||
EditorRotationOrigin,
|
||||
EditorTimelineShowBreaks,
|
||||
EditorAdjustExistingObjectsOnTimingChanges,
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString SetPreviewPointToCurrent => new TranslatableString(getKey(@"set_preview_point_to_current"), @"Set preview point to current time");
|
||||
|
||||
/// <summary>
|
||||
/// "Move already placed objects when changing timing"
|
||||
/// </summary>
|
||||
public static LocalisableString AdjustExistingObjectsOnTimingChanges => new TranslatableString(getKey(@"adjust_existing_objects_on_timing_changes"), @"Move already placed objects when changing timing");
|
||||
|
||||
/// <summary>
|
||||
/// "For editing (.olz)"
|
||||
/// </summary>
|
||||
|
@ -421,7 +421,7 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
Items = new MenuItem[]
|
||||
{
|
||||
new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime)
|
||||
new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osuTK;
|
||||
@ -25,6 +26,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
protected EditorBeatmap Beatmap { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; } = null!;
|
||||
|
||||
@ -110,7 +114,16 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value);
|
||||
|
||||
foreach (var cp in currentGroupItems)
|
||||
{
|
||||
// Only adjust hit object offsets if the group contains a timing control point
|
||||
if (cp is TimingControlPoint tp && configManager.Get<bool>(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges))
|
||||
{
|
||||
TimingSectionAdjustments.AdjustHitObjectOffset(Beatmap, tp, time - SelectedGroup.Value.Time);
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
}
|
||||
|
||||
Beatmap.ControlPointInfo.Add(time, cp);
|
||||
}
|
||||
|
||||
// the control point might not necessarily exist yet, if currentGroupItems was empty.
|
||||
SelectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(time, true);
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
@ -26,6 +27,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<ControlPointGroup> selectedGroup { get; set; } = null!;
|
||||
|
||||
@ -202,15 +206,25 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
// VERY TEMPORARY
|
||||
var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray();
|
||||
|
||||
beatmap.BeginChange();
|
||||
beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
|
||||
|
||||
double newOffset = selectedGroup.Value.Time + adjust;
|
||||
|
||||
foreach (var cp in currentGroupItems)
|
||||
{
|
||||
if (cp is TimingControlPoint tp)
|
||||
{
|
||||
TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, tp, adjust);
|
||||
beatmap.UpdateAllHitObjects();
|
||||
}
|
||||
|
||||
beatmap.ControlPointInfo.Add(newOffset, cp);
|
||||
}
|
||||
|
||||
// the control point might not necessarily exist yet, if currentGroupItems was empty.
|
||||
selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true);
|
||||
beatmap.EndChange();
|
||||
|
||||
if (!editorClock.IsRunning && wasAtStart)
|
||||
editorClock.Seek(newOffset);
|
||||
@ -223,7 +237,16 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (timing == null)
|
||||
return;
|
||||
|
||||
double oldBeatLength = timing.BeatLength;
|
||||
timing.BeatLength = 60000 / (timing.BPM + adjust);
|
||||
|
||||
if (configManager.Get<bool>(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges))
|
||||
{
|
||||
beatmap.BeginChange();
|
||||
TimingSectionAdjustments.SetHitObjectBPM(beatmap, timing, oldBeatLength);
|
||||
beatmap.UpdateAllHitObjects();
|
||||
beatmap.EndChange();
|
||||
}
|
||||
}
|
||||
|
||||
private partial class InlineButton : OsuButton
|
||||
|
@ -1,11 +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.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
@ -15,11 +18,20 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
private LabelledSwitchButton omitBarLine = null!;
|
||||
private BPMTextBox bpmTextEntry = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Flow.AddRange(new Drawable[]
|
||||
{
|
||||
new LabelledSwitchButton
|
||||
{
|
||||
Label = EditorStrings.AdjustExistingObjectsOnTimingChanges,
|
||||
FixedLabelWidth = 220,
|
||||
Current = configManager.GetBindable<bool>(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges),
|
||||
},
|
||||
new TapTimingControl(),
|
||||
bpmTextEntry = new BPMTextBox(),
|
||||
timeSignature = new LabelledTimeSignature
|
||||
@ -42,6 +54,17 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
if (!isRebinding) ChangeHandler?.SaveState();
|
||||
}
|
||||
|
||||
bpmTextEntry.OnCommit = (oldBeatLength, _) =>
|
||||
{
|
||||
if (!configManager.Get<bool>(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges) || ControlPoint.Value == null)
|
||||
return;
|
||||
|
||||
Beatmap.BeginChange();
|
||||
TimingSectionAdjustments.SetHitObjectBPM(Beatmap, ControlPoint.Value, oldBeatLength);
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
Beatmap.EndChange();
|
||||
};
|
||||
}
|
||||
|
||||
private bool isRebinding;
|
||||
@ -74,6 +97,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
private partial class BPMTextBox : LabelledTextBox
|
||||
{
|
||||
public new Action<double, double>? OnCommit { get; set; }
|
||||
|
||||
private readonly BindableNumber<double> beatLengthBindable = new TimingControlPoint().BeatLengthBindable;
|
||||
|
||||
public BPMTextBox()
|
||||
@ -81,10 +106,12 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Label = "BPM";
|
||||
SelectAllOnFocus = true;
|
||||
|
||||
OnCommit += (_, isNew) =>
|
||||
base.OnCommit += (_, isNew) =>
|
||||
{
|
||||
if (!isNew) return;
|
||||
|
||||
double oldBeatLength = beatLengthBindable.Value;
|
||||
|
||||
try
|
||||
{
|
||||
if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0)
|
||||
@ -98,6 +125,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
// This is run regardless of parsing success as the parsed number may not actually trigger a change
|
||||
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
|
||||
beatLengthBindable.TriggerChange();
|
||||
OnCommit?.Invoke(oldBeatLength, beatLengthBindable.Value);
|
||||
};
|
||||
|
||||
beatLengthBindable.BindValueChanged(val =>
|
||||
|
55
osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs
Normal file
55
osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// 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 osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
public static class TimingSectionAdjustments
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all objects from <paramref name="beatmap"/> which are affected by the supplied <paramref name="timingControlPoint"/>.
|
||||
/// </summary>
|
||||
public static List<HitObject> HitObjectsInTimingRange(IBeatmap beatmap, TimingControlPoint timingControlPoint)
|
||||
{
|
||||
// If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects
|
||||
double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < timingControlPoint.Time) ? timingControlPoint.Time : double.MinValue;
|
||||
double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > timingControlPoint.Time)?.Time ?? double.MaxValue;
|
||||
|
||||
return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves all relevant objects after <paramref name="timingControlPoint"/>'s offset has been changed by <paramref name="adjustment"/>.
|
||||
/// </summary>
|
||||
public static void AdjustHitObjectOffset(IBeatmap beatmap, TimingControlPoint timingControlPoint, double adjustment)
|
||||
{
|
||||
foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint))
|
||||
{
|
||||
hitObject.StartTime += adjustment;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures all relevant objects are still snapped to the same beats after <paramref name="timingControlPoint"/>'s beat length / BPM has been changed.
|
||||
/// </summary>
|
||||
public static void SetHitObjectBPM(IBeatmap beatmap, TimingControlPoint timingControlPoint, double oldBeatLength)
|
||||
{
|
||||
foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint))
|
||||
{
|
||||
double beat = (hitObject.StartTime - timingControlPoint.Time) / oldBeatLength;
|
||||
|
||||
hitObject.StartTime = (beat * timingControlPoint.BeatLength) + timingControlPoint.Time;
|
||||
|
||||
if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration)
|
||||
hitObjectWithDuration.Duration *= timingControlPoint.BeatLength / oldBeatLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user