2019-08-29 11:43:43 +08:00
// 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.
2022-06-17 15:37:17 +08:00
#nullable disable
2019-08-29 11:43:43 +08:00
using System ;
2019-12-27 18:39:30 +08:00
using System.Collections ;
2019-08-29 11:43:43 +08:00
using System.Collections.Generic ;
2020-05-08 17:46:37 +08:00
using System.Linq ;
2020-04-09 18:54:58 +08:00
using JetBrains.Annotations ;
2020-02-05 16:32:33 +08:00
using osu.Framework.Allocation ;
2019-10-03 13:23:48 +08:00
using osu.Framework.Bindables ;
2019-08-29 11:43:43 +08:00
using osu.Game.Beatmaps ;
using osu.Game.Beatmaps.ControlPoints ;
2021-09-14 17:26:10 +08:00
using osu.Game.Beatmaps.Legacy ;
2019-08-29 11:43:43 +08:00
using osu.Game.Beatmaps.Timing ;
2020-01-23 12:33:55 +08:00
using osu.Game.Rulesets.Edit ;
2019-08-29 11:43:43 +08:00
using osu.Game.Rulesets.Objects ;
2020-08-31 03:12:45 +08:00
using osu.Game.Skinning ;
2019-08-29 11:43:43 +08:00
namespace osu.Game.Screens.Edit
{
2020-10-08 16:18:20 +08:00
public class EditorBeatmap : TransactionalCommitComponent , IBeatmap , IBeatSnapProvider
2019-08-29 11:43:43 +08:00
{
2022-08-16 14:26:29 +08:00
/// <summary>
2022-08-17 13:04:55 +08:00
/// Will become <c>true</c> when a new update is queued, and <c>false</c> when all updates have been applied.
2022-08-16 14:26:29 +08:00
/// </summary>
/// <remarks>
/// This is intended to be used to avoid performing operations (like playback of samples)
/// while mutating hitobjects.
/// </remarks>
2022-08-17 13:04:55 +08:00
public IBindable < bool > UpdateInProgress = > updateInProgress ;
private readonly BindableBool updateInProgress = new BindableBool ( ) ;
2022-08-16 14:26:29 +08:00
2019-10-03 13:37:16 +08:00
/// <summary>
2019-12-27 18:39:30 +08:00
/// Invoked when a <see cref="HitObject"/> is added to this <see cref="EditorBeatmap"/>.
2019-10-03 13:37:16 +08:00
/// </summary>
2019-08-29 15:05:44 +08:00
public event Action < HitObject > HitObjectAdded ;
2019-10-03 13:37:16 +08:00
/// <summary>
2019-12-27 18:39:30 +08:00
/// Invoked when a <see cref="HitObject"/> is removed from this <see cref="EditorBeatmap"/>.
2019-10-03 13:37:16 +08:00
/// </summary>
2019-08-29 15:05:44 +08:00
public event Action < HitObject > HitObjectRemoved ;
2019-10-03 13:37:16 +08:00
/// <summary>
2020-04-09 18:54:58 +08:00
/// Invoked when a <see cref="HitObject"/> is updated.
2019-10-03 13:37:16 +08:00
/// </summary>
2020-04-09 18:54:58 +08:00
public event Action < HitObject > HitObjectUpdated ;
2019-08-29 11:43:43 +08:00
2022-09-13 08:20:52 +08:00
/// <summary>
/// Invoked after <see cref="HitObjects"/> is updated during <see cref="UpdateState"/> and blueprints need to be sorted immediately to prevent a crash.
/// </summary>
public event Action SelectionBlueprintsShouldBeSorted ;
2020-02-07 17:03:14 +08:00
/// <summary>
/// All currently selected <see cref="HitObject"/>s.
/// </summary>
public readonly BindableList < HitObject > SelectedHitObjects = new BindableList < HitObject > ( ) ;
/// <summary>
2020-02-08 10:35:27 +08:00
/// The current placement. Null if there's no active placement.
2020-02-07 17:03:14 +08:00
/// </summary>
public readonly Bindable < HitObject > PlacementObject = new Bindable < HitObject > ( ) ;
2020-01-21 19:46:39 +08:00
2021-10-13 13:34:31 +08:00
private readonly BeatmapInfo beatmapInfo ;
2019-12-27 18:46:33 +08:00
public readonly IBeatmap PlayableBeatmap ;
2021-07-17 01:30:00 +08:00
/// <summary>
/// Whether at least one timing control point is present and providing timing information.
/// </summary>
public IBindable < bool > HasTiming = > hasTiming ;
private readonly Bindable < bool > hasTiming = new Bindable < bool > ( ) ;
2021-04-19 02:29:09 +08:00
[CanBeNull]
2021-08-15 22:13:59 +08:00
public readonly EditorBeatmapSkin BeatmapSkin ;
2020-08-31 03:12:45 +08:00
2020-02-05 16:32:33 +08:00
[Resolved]
private BindableBeatDivisor beatDivisor { get ; set ; }
2020-01-23 12:33:55 +08:00
2021-07-17 01:30:00 +08:00
[Resolved]
private EditorClock editorClock { get ; set ; }
2020-02-05 16:16:15 +08:00
private readonly IBeatmapProcessor beatmapProcessor ;
2022-09-13 07:09:42 +08:00
private readonly Dictionary < HitObject , Bindable < double > > startTimeBindables = new Dictionary < HitObject , Bindable < double > > ( ) ;
2019-08-29 11:43:43 +08:00
2021-10-13 13:34:31 +08:00
public EditorBeatmap ( IBeatmap playableBeatmap , ISkin beatmapSkin = null , BeatmapInfo beatmapInfo = null )
2019-08-29 11:43:43 +08:00
{
2019-12-27 18:46:33 +08:00
PlayableBeatmap = playableBeatmap ;
2022-06-13 14:40:11 +08:00
PlayableBeatmap . ControlPointInfo = ConvertControlPoints ( PlayableBeatmap . ControlPointInfo ) ;
2021-09-14 17:26:10 +08:00
2021-10-13 13:34:31 +08:00
this . beatmapInfo = beatmapInfo ? ? playableBeatmap . BeatmapInfo ;
2021-08-15 22:13:59 +08:00
if ( beatmapSkin is Skin skin )
2022-08-02 15:35:15 +08:00
{
2021-08-15 22:13:59 +08:00
BeatmapSkin = new EditorBeatmapSkin ( skin ) ;
2022-08-02 15:35:15 +08:00
BeatmapSkin . BeatmapSkinChanged + = SaveState ;
}
2020-02-05 16:16:15 +08:00
2022-01-12 21:34:07 +08:00
beatmapProcessor = playableBeatmap . BeatmapInfo . Ruleset . CreateInstance ( ) . CreateBeatmapProcessor ( PlayableBeatmap ) ;
2020-02-05 16:16:15 +08:00
2019-10-03 13:23:48 +08:00
foreach ( var obj in HitObjects )
2022-09-13 07:09:42 +08:00
trackStartTime ( obj ) ;
2019-08-29 11:43:43 +08:00
}
2022-06-13 14:40:11 +08:00
/// <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 )
{
2022-06-24 20:25:23 +08:00
case DifficultyControlPoint :
case SampleControlPoint :
2022-06-13 14:40:11 +08:00
// skip legacy types.
continue ;
default :
newControlPoints . Add ( controlPoint . Time , controlPoint ) ;
break ;
}
}
return newControlPoints ;
}
2019-08-29 11:43:43 +08:00
public BeatmapInfo BeatmapInfo
{
2021-10-13 13:34:31 +08:00
get = > beatmapInfo ;
2022-06-28 00:34:24 +08:00
set = > throw new InvalidOperationException ( $"Can't set {nameof(BeatmapInfo)} on {nameof(EditorBeatmap)}" ) ;
2019-08-29 11:43:43 +08:00
}
2021-10-13 13:34:31 +08:00
public BeatmapMetadata Metadata = > beatmapInfo . Metadata ;
2019-08-29 11:43:43 +08:00
2021-10-02 11:34:29 +08:00
public BeatmapDifficulty Difficulty
{
get = > PlayableBeatmap . Difficulty ;
set = > PlayableBeatmap . Difficulty = value ;
}
2021-01-15 16:34:01 +08:00
public ControlPointInfo ControlPointInfo
{
get = > PlayableBeatmap . ControlPointInfo ;
set = > PlayableBeatmap . ControlPointInfo = value ;
}
2019-08-29 11:43:43 +08:00
2019-12-27 18:46:33 +08:00
public List < BreakPeriod > Breaks = > PlayableBeatmap . Breaks ;
2019-08-29 11:43:43 +08:00
2019-12-27 18:46:33 +08:00
public double TotalBreakTime = > PlayableBeatmap . TotalBreakTime ;
2019-08-29 11:43:43 +08:00
2019-12-27 18:46:33 +08:00
public IReadOnlyList < HitObject > HitObjects = > PlayableBeatmap . HitObjects ;
2019-08-29 11:43:43 +08:00
2019-12-27 18:46:33 +08:00
public IEnumerable < BeatmapStatistic > GetStatistics ( ) = > PlayableBeatmap . GetStatistics ( ) ;
2019-08-29 11:43:43 +08:00
2021-01-15 13:28:49 +08:00
public double GetMostCommonBeatLength ( ) = > PlayableBeatmap . GetMostCommonBeatLength ( ) ;
2019-12-27 18:39:30 +08:00
public IBeatmap Clone ( ) = > ( EditorBeatmap ) MemberwiseClone ( ) ;
2019-08-29 11:43:43 +08:00
2020-01-01 22:26:45 +08:00
private IList mutableHitObjects = > ( IList ) PlayableBeatmap . HitObjects ;
2020-01-01 20:24:00 +08:00
2020-10-08 17:06:46 +08:00
private readonly List < HitObject > batchPendingInserts = new List < HitObject > ( ) ;
private readonly List < HitObject > batchPendingDeletes = new List < HitObject > ( ) ;
private readonly HashSet < HitObject > batchPendingUpdates = new HashSet < HitObject > ( ) ;
2021-02-26 13:15:12 +08:00
/// <summary>
/// Perform the provided action on every selected hitobject.
/// Changes will be grouped as one history action.
/// </summary>
/// <param name="action">The action to perform.</param>
public void PerformOnSelection ( Action < HitObject > action )
{
if ( SelectedHitObjects . Count = = 0 )
return ;
BeginChange ( ) ;
foreach ( var h in SelectedHitObjects )
action ( h ) ;
EndChange ( ) ;
}
2020-04-09 19:48:59 +08:00
/// <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 )
{
2020-10-08 16:18:20 +08:00
BeginChange ( ) ;
foreach ( var h in hitObjects )
Add ( h ) ;
EndChange ( ) ;
2020-04-09 19:48:59 +08:00
}
2019-08-29 15:31:43 +08:00
/// <summary>
2019-12-27 18:39:30 +08:00
/// Adds a <see cref="HitObject"/> to this <see cref="EditorBeatmap"/>.
2019-08-29 15:31:43 +08:00
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to add.</param>
2019-12-27 18:39:30 +08:00
public void Add ( HitObject hitObject )
2019-08-29 11:43:43 +08:00
{
// Preserve existing sorting order in the beatmap
2021-10-27 12:04:41 +08:00
int insertionIndex = findInsertionIndex ( PlayableBeatmap . HitObjects , hitObject . StartTime ) ;
2020-04-30 19:39:41 +08:00
Insert ( insertionIndex + 1 , hitObject ) ;
}
2019-08-29 11:43:43 +08:00
2020-04-30 19:39:41 +08:00
/// <summary>
/// Inserts a <see cref="HitObject"/> into this <see cref="EditorBeatmap"/>.
/// </summary>
/// <remarks>
/// It is the invoker's responsibility to make sure that <see cref="HitObject"/> sorting order is maintained.
/// </remarks>
/// <param name="index">The index to insert the <see cref="HitObject"/> at.</param>
/// <param name="hitObject">The <see cref="HitObject"/> to insert.</param>
public void Insert ( int index , HitObject hitObject )
{
2022-09-13 07:09:42 +08:00
trackStartTime ( hitObject ) ;
2020-04-09 18:54:58 +08:00
2020-09-12 20:20:37 +08:00
mutableHitObjects . Insert ( index , hitObject ) ;
2020-09-14 13:45:49 +08:00
2020-10-08 17:42:53 +08:00
BeginChange ( ) ;
batchPendingInserts . Add ( hitObject ) ;
EndChange ( ) ;
2020-09-14 13:45:49 +08:00
}
/// <summary>
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
2020-10-08 17:06:46 +08:00
public void Update ( [ NotNull ] HitObject hitObject )
2020-09-14 13:45:49 +08:00
{
2020-10-08 16:18:20 +08:00
// updates are debounced regardless of whether a batch is active.
batchPendingUpdates . Add ( hitObject ) ;
2022-08-16 14:26:29 +08:00
2022-08-17 13:04:55 +08:00
updateInProgress . Value = true ;
2019-08-29 11:43:43 +08:00
}
2020-10-08 17:06:46 +08:00
/// <summary>
/// Update all hit objects with potentially changed difficulty or control point data.
/// </summary>
public void UpdateAllHitObjects ( )
{
foreach ( var h in HitObjects )
batchPendingUpdates . Add ( h ) ;
2022-08-16 14:26:29 +08:00
2022-08-17 13:04:55 +08:00
updateInProgress . Value = true ;
2020-10-08 17:06:46 +08:00
}
2019-08-29 15:31:43 +08:00
/// <summary>
2019-12-27 18:39:30 +08:00
/// Removes a <see cref="HitObject"/> from this <see cref="EditorBeatmap"/>.
2019-08-29 15:31:43 +08:00
/// </summary>
2020-09-14 16:55:41 +08:00
/// <param name="hitObject">The <see cref="HitObject"/> to remove.</param>
2020-04-09 19:48:59 +08:00
/// <returns>True if the <see cref="HitObject"/> has been removed, false otherwise.</returns>
2020-09-14 16:55:41 +08:00
public bool Remove ( HitObject hitObject )
2020-04-09 19:48:59 +08:00
{
2020-09-14 16:55:41 +08:00
int index = FindIndex ( hitObject ) ;
2020-04-09 19:48:59 +08:00
2020-09-14 16:55:41 +08:00
if ( index = = - 1 )
return false ;
2020-04-09 19:48:59 +08:00
2020-09-14 16:55:41 +08:00
RemoveAt ( index ) ;
return true ;
2020-04-09 19:48:59 +08:00
}
2020-10-06 21:09:48 +08:00
/// <summary>
/// Removes a collection of <see cref="HitObject"/>s to this <see cref="EditorBeatmap"/>.
/// </summary>
/// <param name="hitObjects">The <see cref="HitObject"/>s to remove.</param>
public void RemoveRange ( IEnumerable < HitObject > hitObjects )
{
2020-10-08 16:18:20 +08:00
BeginChange ( ) ;
foreach ( var h in hitObjects )
Remove ( h ) ;
EndChange ( ) ;
2020-10-06 21:09:48 +08:00
}
2020-04-09 19:48:59 +08:00
/// <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 )
2019-08-29 11:43:43 +08:00
{
2020-04-09 19:48:59 +08:00
var hitObject = ( HitObject ) mutableHitObjects [ index ] ;
2019-10-03 13:23:48 +08:00
2020-04-09 19:48:59 +08:00
mutableHitObjects . RemoveAt ( index ) ;
2019-12-27 18:39:30 +08:00
2022-09-13 07:09:42 +08:00
var bindable = startTimeBindables [ hitObject ] ;
bindable . UnbindAll ( ) ;
startTimeBindables . Remove ( hitObject ) ;
2020-04-09 18:54:58 +08:00
2020-10-08 17:42:53 +08:00
BeginChange ( ) ;
batchPendingDeletes . Add ( hitObject ) ;
EndChange ( ) ;
2020-10-06 20:21:09 +08:00
}
2020-10-08 16:18:20 +08:00
protected override void Update ( )
2020-10-06 20:21:09 +08:00
{
2020-10-08 16:18:20 +08:00
base . Update ( ) ;
2020-10-06 20:21:09 +08:00
2020-10-08 16:18:20 +08:00
if ( batchPendingUpdates . Count > 0 )
UpdateState ( ) ;
2021-07-17 01:30:00 +08:00
hasTiming . Value = ! ReferenceEquals ( ControlPointInfo . TimingPointAt ( editorClock . CurrentTime ) , TimingControlPoint . DEFAULT ) ;
2020-10-08 16:18:20 +08:00
}
2020-10-06 20:21:09 +08:00
2020-10-08 16:18:20 +08:00
protected override void UpdateState ( )
{
if ( batchPendingUpdates . Count = = 0 & & batchPendingDeletes . Count = = 0 & & batchPendingInserts . Count = = 0 )
return ;
2020-10-06 20:21:09 +08:00
2020-09-14 13:45:49 +08:00
beatmapProcessor ? . PreProcess ( ) ;
2020-10-06 20:21:09 +08:00
2020-10-07 10:09:45 +08:00
foreach ( var h in batchPendingDeletes ) processHitObject ( h ) ;
foreach ( var h in batchPendingInserts ) processHitObject ( h ) ;
2022-09-13 07:09:42 +08:00
foreach ( var h in batchPendingUpdates ) processHitObject ( h ) ;
2020-10-06 20:21:09 +08:00
2020-10-07 10:09:45 +08:00
beatmapProcessor ? . PostProcess ( ) ;
2020-10-06 20:21:09 +08:00
2022-09-13 08:20:52 +08:00
// Signal selection blueprint sorting because it is possible that the beatmap processor changed the order of the selection blueprints
SelectionBlueprintsShouldBeSorted ? . Invoke ( ) ;
2020-10-08 16:52:49 +08:00
// callbacks may modify the lists so let's be safe about it
var deletes = batchPendingDeletes . ToArray ( ) ;
2020-10-07 10:09:45 +08:00
batchPendingDeletes . Clear ( ) ;
2020-10-08 16:52:49 +08:00
var inserts = batchPendingInserts . ToArray ( ) ;
2020-10-06 20:21:09 +08:00
batchPendingInserts . Clear ( ) ;
2020-10-08 16:52:49 +08:00
2022-09-13 07:09:42 +08:00
var updates = batchPendingUpdates . ToArray ( ) ;
batchPendingUpdates . Clear ( ) ;
2020-10-08 16:52:49 +08:00
foreach ( var h in deletes ) HitObjectRemoved ? . Invoke ( h ) ;
foreach ( var h in inserts ) HitObjectAdded ? . Invoke ( h ) ;
2022-09-13 07:09:42 +08:00
foreach ( var h in updates ) HitObjectUpdated ? . Invoke ( h ) ;
2022-08-17 13:04:55 +08:00
updateInProgress . Value = false ;
2019-10-03 13:23:48 +08:00
}
2020-05-08 17:46:37 +08:00
/// <summary>
/// Clears all <see cref="HitObjects"/> from this <see cref="EditorBeatmap"/>.
/// </summary>
2020-10-06 21:09:48 +08:00
public void Clear ( ) = > RemoveRange ( HitObjects . ToArray ( ) ) ;
2020-05-08 17:46:37 +08:00
2021-10-02 11:34:29 +08:00
private void processHitObject ( HitObject hitObject ) = > hitObject . ApplyDefaults ( ControlPointInfo , PlayableBeatmap . Difficulty ) ;
2020-09-14 13:45:49 +08:00
2022-09-13 07:09:42 +08:00
private void trackStartTime ( HitObject hitObject )
2019-10-03 13:23:48 +08:00
{
2022-09-13 07:09:42 +08:00
startTimeBindables [ hitObject ] = hitObject . StartTimeBindable . GetBoundCopy ( ) ;
startTimeBindables [ hitObject ] . ValueChanged + = _ = >
2019-10-03 13:23:48 +08:00
{
// For now we'll remove and re-add the hitobject. This is not optimal and can be improved if required.
2020-01-01 20:24:00 +08:00
mutableHitObjects . Remove ( hitObject ) ;
2019-10-03 13:23:48 +08:00
2021-10-27 12:04:41 +08:00
int insertionIndex = findInsertionIndex ( PlayableBeatmap . HitObjects , hitObject . StartTime ) ;
2020-01-01 20:24:00 +08:00
mutableHitObjects . Insert ( insertionIndex + 1 , hitObject ) ;
2019-10-03 13:23:48 +08:00
2020-10-08 17:06:46 +08:00
Update ( hitObject ) ;
2019-10-03 13:23:48 +08:00
} ;
2019-08-29 11:43:43 +08:00
}
2019-12-27 18:39:30 +08:00
private int findInsertionIndex ( IReadOnlyList < HitObject > list , double startTime )
{
for ( int i = 0 ; i < list . Count ; i + + )
{
if ( list [ i ] . StartTime > startTime )
return i - 1 ;
}
2019-08-29 11:43:43 +08:00
2019-12-27 18:39:30 +08:00
return list . Count - 1 ;
}
2020-01-23 12:33:55 +08:00
2021-04-28 15:57:52 +08:00
public double SnapTime ( double time , double? referenceTime ) = > ControlPointInfo . GetClosestSnappedTime ( time , BeatDivisor , referenceTime ) ;
2021-04-26 11:07:24 +08:00
2020-01-23 14:31:56 +08:00
public double GetBeatLengthAtTime ( double referenceTime ) = > ControlPointInfo . TimingPointAt ( referenceTime ) . BeatLength / BeatDivisor ;
2020-01-23 12:33:55 +08:00
public int BeatDivisor = > beatDivisor ? . Value ? ? 1 ;
2019-08-29 11:43:43 +08:00
}
}