Skip to content

Commit

Permalink
Merge pull request #28382 from Hecatia-Lapislazuli/move-already-place…
Browse files Browse the repository at this point in the history
…d-objects-when-adjusting-offset-bpm

Implemented ability to adjust already-placed objects when changing timing offsets
  • Loading branch information
peppy authored Nov 11, 2024
2 parents f8ac54d + c37e487 commit 8605639
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 2 deletions.
161 changes: 161 additions & 0 deletions osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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);
}
}
}
2 changes: 2 additions & 0 deletions osu.Game/Configuration/OsuConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ protected override void InitialiseDefaults()
SetDefault(OsuSetting.EditorShowSpeedChanges, false);
SetDefault(OsuSetting.EditorScaleOrigin, EditorOrigin.GridCentre);
SetDefault(OsuSetting.EditorRotationOrigin, EditorOrigin.GridCentre);
SetDefault(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges, true);

SetDefault(OsuSetting.HideCountryFlags, false);

Expand Down Expand Up @@ -442,5 +443,6 @@ public enum OsuSetting
EditorScaleOrigin,
EditorRotationOrigin,
EditorTimelineShowBreaks,
EditorAdjustExistingObjectsOnTimingChanges,
}
}
5 changes: 5 additions & 0 deletions osu.Game/Localisation/EditorStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public static class EditorStrings
/// </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>
Expand Down
2 changes: 1 addition & 1 deletion osu.Game/Screens/Edit/Editor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ private void load(OsuConfigManager config)
{
Items = new MenuItem[]
{
new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime)
new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime),
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions osu.Game/Screens/Edit/Timing/GroupSection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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;
Expand All @@ -25,6 +26,9 @@ internal partial class GroupSection : CompositeDrawable
[Resolved]
protected EditorBeatmap Beatmap { get; private set; } = null!;

[Resolved]
private OsuConfigManager configManager { get; set; } = null!;

[Resolved]
private EditorClock clock { get; set; } = null!;

Expand Down Expand Up @@ -110,7 +114,16 @@ private void changeSelectedGroupTime(in double time)
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);
Expand Down
23 changes: 23 additions & 0 deletions osu.Game/Screens/Edit/Timing/TapTimingControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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;
Expand All @@ -26,6 +27,9 @@ public partial class TapTimingControl : CompositeDrawable
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;

[Resolved]
private OsuConfigManager configManager { get; set; } = null!;

[Resolved]
private Bindable<ControlPointGroup> selectedGroup { get; set; } = null!;

Expand Down Expand Up @@ -202,15 +206,25 @@ private void adjustOffset(double adjust)
// 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);
Expand All @@ -223,7 +237,16 @@ private void adjustBpm(double adjust)
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
Expand Down
30 changes: 29 additions & 1 deletion osu.Game/Screens/Edit/Timing/TimingSection.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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
{
Expand All @@ -15,11 +18,20 @@ internal partial class TimingSection : Section<TimingControlPoint>
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
Expand All @@ -42,6 +54,17 @@ void saveChanges()
{
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;
Expand Down Expand Up @@ -74,17 +97,21 @@ protected override TimingControlPoint CreatePoint()

private partial class BPMTextBox : LabelledTextBox
{
public new Action<double, double>? OnCommit { get; set; }

private readonly BindableNumber<double> beatLengthBindable = new TimingControlPoint().BeatLengthBindable;

public BPMTextBox()
{
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)
Expand All @@ -98,6 +125,7 @@ public BPMTextBox()
// 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 =>
Expand Down
Loading

0 comments on commit 8605639

Please sign in to comment.