diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index c90557b5f1..416ad0a590 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -13,8 +13,28 @@ namespace osu.Game.Beatmaps.ControlPoints [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable, IControlPoint { + /// + /// Invoked when any of this 's properties have changed. + /// + public event Action? Changed; + + protected void RaiseChanged() => Changed?.Invoke(this); + + private double time; + [JsonIgnore] - public double Time { get; set; } + public double Time + { + get => time; + set + { + if (time == value) + return; + + time = value; + RaiseChanged(); + } + } public void AttachGroup(ControlPointGroup pointGroup) => Time = pointGroup.Time; diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs index 1f34f3777d..c9c87dc85d 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs @@ -10,8 +10,11 @@ namespace osu.Game.Beatmaps.ControlPoints public class ControlPointGroup : IComparable, IEquatable { public event Action? ItemAdded; + public event Action? ItemChanged; public event Action? ItemRemoved; + private void raiseItemChanged(ControlPoint controlPoint) => ItemChanged?.Invoke(controlPoint); + /// /// The time at which the control point takes effect. /// @@ -39,12 +42,14 @@ namespace osu.Game.Beatmaps.ControlPoints controlPoints.Add(point); ItemAdded?.Invoke(point); + point.Changed += raiseItemChanged; } public void Remove(ControlPoint point) { controlPoints.Remove(point); ItemRemoved?.Invoke(point); + point.Changed -= raiseItemChanged; } public sealed override bool Equals(object? obj) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index c695112990..f8e72a1e34 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -20,6 +20,14 @@ namespace osu.Game.Beatmaps.ControlPoints [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] public class ControlPointInfo : IDeepCloneable { + /// + /// Invoked on any change to the set of control points. + /// + [CanBeNull] + public event Action ControlPointsChanged; + + private void raiseControlPointsChanged([CanBeNull] ControlPoint _ = null) => ControlPointsChanged?.Invoke(); + /// /// All control points grouped by time. /// @@ -117,6 +125,7 @@ namespace osu.Game.Beatmaps.ControlPoints if (addIfNotExisting) { newGroup.ItemAdded += GroupItemAdded; + newGroup.ItemChanged += raiseControlPointsChanged; newGroup.ItemRemoved += GroupItemRemoved; groups.Insert(~i, newGroup); @@ -132,6 +141,7 @@ namespace osu.Game.Beatmaps.ControlPoints group.Remove(item); group.ItemAdded -= GroupItemAdded; + group.ItemChanged -= raiseControlPointsChanged; group.ItemRemoved -= GroupItemRemoved; groups.Remove(group); @@ -288,6 +298,8 @@ namespace osu.Game.Beatmaps.ControlPoints default: throw new ArgumentException($"A control point of unexpected type {controlPoint.GetType()} was added to this {nameof(ControlPointInfo)}"); } + + raiseControlPointsChanged(); } protected virtual void GroupItemRemoved(ControlPoint controlPoint) @@ -302,6 +314,8 @@ namespace osu.Game.Beatmaps.ControlPoints effectPoints.Remove(typed); break; } + + raiseControlPointsChanged(); } public ControlPointInfo DeepClone() diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 05230c85f4..9f8ed1b396 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -44,6 +44,11 @@ namespace osu.Game.Beatmaps.ControlPoints set => SliderVelocityBindable.Value = value; } + public DifficultyControlPoint() + { + SliderVelocityBindable.BindValueChanged(_ => RaiseChanged()); + } + public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty && GenerateTicks == existingDifficulty.GenerateTicks diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 0138ac7569..d48ed957ee 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -50,6 +50,12 @@ namespace osu.Game.Beatmaps.ControlPoints set => KiaiModeBindable.Value = value; } + public EffectControlPoint() + { + KiaiModeBindable.BindValueChanged(_ => RaiseChanged()); + ScrollSpeedBindable.BindValueChanged(_ => RaiseChanged()); + } + public override bool IsRedundant(ControlPoint? existing) => existing is EffectControlPoint existingEffect && KiaiMode == existingEffect.KiaiMode diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index ae4bdafd6f..800d9f9abc 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -56,6 +56,12 @@ namespace osu.Game.Beatmaps.ControlPoints set => SampleVolumeBindable.Value = value; } + public SampleControlPoint() + { + SampleBankBindable.BindValueChanged(_ => RaiseChanged()); + SampleVolumeBindable.BindValueChanged(_ => RaiseChanged()); + } + /// /// Create a SampleInfo based on the sample settings in this control point. /// diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 4e69486e2d..9ac361cffe 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -82,6 +82,13 @@ namespace osu.Game.Beatmaps.ControlPoints /// public double BPM => 60000 / BeatLength; + public TimingControlPoint() + { + TimeSignatureBindable.BindValueChanged(_ => RaiseChanged()); + OmitFirstBarLineBindable.BindValueChanged(_ => RaiseChanged()); + BeatLengthBindable.BindValueChanged(_ => RaiseChanged()); + } + // Timing points are never redundant as they can change the time signature. public override bool IsRedundant(ControlPoint? existing) => false; diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs index eb73cef01a..223b770b48 100644 --- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -12,6 +13,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -21,6 +23,9 @@ namespace osu.Game.Rulesets.Edit public abstract partial class ScrollingHitObjectComposer : HitObjectComposer where TObject : HitObject { + [Resolved] + private Editor? editor { get; set; } + private readonly Bindable showSpeedChanges = new Bindable(); private Bindable configShowSpeedChanges = null!; @@ -72,6 +77,8 @@ namespace osu.Game.Rulesets.Edit if (beatSnapGrid != null) AddInternal(beatSnapGrid); + + EditorBeatmap.ControlPointInfo.ControlPointsChanged += expireComposeScreenOnControlPointChange; } protected override void UpdateAfterChildren() @@ -104,5 +111,15 @@ namespace osu.Game.Rulesets.Edit beatSnapGrid.SelectionTimeRange = null; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (EditorBeatmap.IsNotNull()) + EditorBeatmap.ControlPointInfo.ControlPointsChanged -= expireComposeScreenOnControlPointChange; + } + + private void expireComposeScreenOnControlPointChange() => editor?.ReloadComposeScreen(); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 02dcad46f7..e91777eab2 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1019,6 +1019,15 @@ namespace osu.Game.Screens.Edit } } + /// + /// Forces a reload of the compose screen after significant configuration changes. + /// + /// + /// This can be necessary for scrolling rulesets, as they do not easily support control points changing under them. + /// The reason that this works is that will re-instantiate the screen whenever it is requested next. + /// + public void ReloadComposeScreen() => screenContainer.SingleOrDefault(s => s.Type == EditorScreenMode.Compose)?.RemoveAndDisposeImmediately(); + [CanBeNull] private ScheduledDelegate playbackDisabledDebounce;