2020-04-09 19:48:59 +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
2020-04-09 19:48:59 +08:00
using System ;
using System.Collections.Generic ;
2023-05-23 04:33:41 +08:00
using System.Diagnostics ;
2020-04-09 19:48:59 +08:00
using System.IO ;
2022-06-20 14:28:36 +08:00
using System.Linq ;
2020-04-13 16:18:50 +08:00
using System.Text ;
2020-04-09 19:48:59 +08:00
using DiffPlex ;
2022-06-13 14:40:11 +08:00
using DiffPlex.Model ;
2020-04-09 19:48:59 +08:00
using osu.Framework.Audio.Track ;
using osu.Framework.Graphics.Textures ;
using osu.Game.Beatmaps ;
2022-06-20 14:28:36 +08:00
using osu.Game.Beatmaps.ControlPoints ;
2022-06-13 14:40:11 +08:00
using osu.Game.Beatmaps.Formats ;
2023-05-23 04:33:41 +08:00
using osu.Game.Extensions ;
2020-04-09 19:48:59 +08:00
using osu.Game.IO ;
2023-05-23 04:19:10 +08:00
using osu.Game.Rulesets.Objects.Types ;
2021-05-22 01:21:00 +08:00
using osu.Game.Skinning ;
2020-04-13 16:18:50 +08:00
using Decoder = osu . Game . Beatmaps . Formats . Decoder ;
2020-04-09 19:48:59 +08:00
namespace osu.Game.Screens.Edit
{
2020-04-13 16:20:01 +08:00
/// <summary>
/// Patches an <see cref="EditorBeatmap"/> based on the difference between two legacy (.osu) states.
/// </summary>
public class LegacyEditorBeatmapPatcher
2020-04-09 19:48:59 +08:00
{
private readonly EditorBeatmap editorBeatmap ;
2020-04-13 16:20:01 +08:00
public LegacyEditorBeatmapPatcher ( EditorBeatmap editorBeatmap )
2020-04-09 19:48:59 +08:00
{
this . editorBeatmap = editorBeatmap ;
}
2020-04-13 16:18:50 +08:00
public void Patch ( byte [ ] currentState , byte [ ] newState )
2020-04-09 19:48:59 +08:00
{
// Diff the beatmaps
var result = new Differ ( ) . CreateLineDiffs ( readString ( currentState ) , readString ( newState ) , true , false ) ;
2022-06-13 14:40:11 +08:00
IBeatmap newBeatmap = null ;
editorBeatmap . BeginChange ( ) ;
processHitObjects ( result , ( ) = > newBeatmap ? ? = readBeatmap ( newState ) ) ;
2022-06-21 11:11:44 +08:00
processTimingPoints ( ( ) = > newBeatmap ? ? = readBeatmap ( newState ) ) ;
2023-05-23 04:33:41 +08:00
processHitObjectLocalData ( ( ) = > newBeatmap ? ? = readBeatmap ( newState ) ) ;
2022-06-13 14:40:11 +08:00
editorBeatmap . EndChange ( ) ;
}
2022-06-21 11:11:44 +08:00
private void processTimingPoints ( Func < IBeatmap > getNewBeatmap )
2022-06-13 14:40:11 +08:00
{
2022-06-20 14:28:36 +08:00
ControlPointInfo newControlPoints = EditorBeatmap . ConvertControlPoints ( getNewBeatmap ( ) . ControlPointInfo ) ;
2022-06-13 14:40:11 +08:00
2022-06-20 14:28:36 +08:00
// 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 ) ;
2022-06-13 14:40:11 +08:00
2022-06-20 14:28:36 +08:00
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 ) ;
}
}
2022-06-13 14:40:11 +08:00
}
private void processHitObjects ( DiffResult result , Func < IBeatmap > getNewBeatmap )
{
findChangedIndices ( result , LegacyDecoder < Beatmap > . Section . HitObjects , out var removedIndices , out var addedIndices ) ;
2022-06-13 16:36:32 +08:00
for ( int i = removedIndices . Count - 1 ; i > = 0 ; i - - )
editorBeatmap . RemoveAt ( removedIndices [ i ] ) ;
2022-06-13 14:40:11 +08:00
if ( addedIndices . Count > 0 )
{
var newBeatmap = getNewBeatmap ( ) ;
foreach ( int i in addedIndices )
editorBeatmap . Insert ( i , newBeatmap . HitObjects [ i ] ) ;
}
}
2023-05-23 04:33:41 +08:00
private void processHitObjectLocalData ( Func < IBeatmap > getNewBeatmap )
{
// This method handles data that are stored in control points in the legacy format,
// but were moved to the hitobjects themselves in lazer.
// Specifically, the data being referred to here consists of: slider velocity and sample information.
// For simplicity, this implementation relies on the editor beatmap already having the same hitobjects in sequence as the new beatmap.
// To guarantee that, `processHitObjects()` must be ran prior to this method for correct operation.
// This is done to avoid the necessity of reimplementing/reusing parts of LegacyBeatmapDecoder that already treat this data correctly.
var oldObjects = editorBeatmap . HitObjects ;
var newObjects = getNewBeatmap ( ) . HitObjects ;
Debug . Assert ( oldObjects . Count = = newObjects . Count ) ;
foreach ( var ( oldObject , newObject ) in oldObjects . Zip ( newObjects ) )
{
2023-05-23 04:45:39 +08:00
// if `oldObject` and `newObject` are the same, it means that `oldObject` was inserted into `editorBeatmap` by `processHitObjects()`.
// in that case, there is nothing to do (and some of the subsequent changes may even prove destructive).
if ( ReferenceEquals ( oldObject , newObject ) )
continue ;
2023-05-23 04:33:41 +08:00
if ( oldObject is IHasSliderVelocity oldWithVelocity & & newObject is IHasSliderVelocity newWithVelocity )
oldWithVelocity . SliderVelocity = newWithVelocity . SliderVelocity ;
oldObject . Samples = newObject . Samples ;
if ( oldObject is IHasRepeats oldWithRepeats & & newObject is IHasRepeats newWithRepeats )
{
oldWithRepeats . NodeSamples . Clear ( ) ;
oldWithRepeats . NodeSamples . AddRange ( newWithRepeats . NodeSamples ) ;
}
}
}
2022-06-13 14:40:11 +08:00
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 > ( ) ;
2020-04-09 19:48:59 +08:00
2022-06-13 16:36:32 +08:00
// 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.
2022-06-13 14:40:11 +08:00
int oldSectionStartIndex = Array . IndexOf ( result . PiecesOld , $"[{section}]" ) ;
2022-06-13 15:56:08 +08:00
if ( oldSectionStartIndex = = - 1 )
return ;
2022-06-13 14:40:11 +08:00
2022-06-13 15:56:08 +08:00
int oldSectionEndIndex = Array . FindIndex ( result . PiecesOld , oldSectionStartIndex + 1 , s = > s . StartsWith ( '[' ) ) ;
2022-06-13 14:40:11 +08:00
if ( oldSectionEndIndex = = - 1 )
oldSectionEndIndex = result . PiecesOld . Length ;
int newSectionStartIndex = Array . IndexOf ( result . PiecesNew , $"[{section}]" ) ;
2022-06-13 15:56:08 +08:00
if ( newSectionStartIndex = = - 1 )
return ;
2020-04-09 19:48:59 +08:00
2022-06-13 15:56:08 +08:00
int newSectionEndIndex = Array . FindIndex ( result . PiecesNew , newSectionStartIndex + 1 , s = > s . StartsWith ( '[' ) ) ;
2022-06-13 14:40:11 +08:00
if ( newSectionEndIndex = = - 1 )
2022-06-13 15:55:46 +08:00
newSectionEndIndex = result . PiecesNew . Length ;
2020-11-07 23:18:25 +08:00
2020-04-09 19:48:59 +08:00
foreach ( var block in result . DiffBlocks )
{
2022-06-13 14:40:11 +08:00
// Removed indices
2020-04-09 19:48:59 +08:00
for ( int i = 0 ; i < block . DeleteCountA ; i + + )
{
2022-06-13 14:40:11 +08:00
int objectIndex = block . DeleteStartA + i ;
2020-04-09 19:48:59 +08:00
2022-06-13 14:40:11 +08:00
if ( objectIndex < = oldSectionStartIndex | | objectIndex > = oldSectionEndIndex )
2020-04-09 19:48:59 +08:00
continue ;
2022-06-13 14:40:11 +08:00
removedIndices . Add ( objectIndex - oldSectionStartIndex - 1 ) ;
2020-04-09 19:48:59 +08:00
}
2022-06-13 14:40:11 +08:00
// Added indices
2020-04-09 19:48:59 +08:00
for ( int i = 0 ; i < block . InsertCountB ; i + + )
{
2022-06-13 14:40:11 +08:00
int objectIndex = block . InsertStartB + i ;
2020-04-09 19:48:59 +08:00
2022-06-13 14:40:11 +08:00
if ( objectIndex < = newSectionStartIndex | | objectIndex > = newSectionEndIndex )
2020-04-09 19:48:59 +08:00
continue ;
2022-06-13 14:40:11 +08:00
addedIndices . Add ( objectIndex - newSectionStartIndex - 1 ) ;
2020-04-09 19:48:59 +08:00
}
}
2020-04-30 19:39:41 +08:00
// 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.
2022-06-13 14:40:11 +08:00
removedIndices . Sort ( ) ;
addedIndices . Sort ( ) ;
2020-04-09 19:48:59 +08:00
}
2020-04-13 16:18:50 +08:00
private string readString ( byte [ ] state ) = > Encoding . UTF8 . GetString ( state ) ;
2020-04-09 19:48:59 +08:00
2020-04-13 16:18:50 +08:00
private IBeatmap readBeatmap ( byte [ ] state )
2020-04-09 19:48:59 +08:00
{
2020-04-13 16:18:50 +08:00
using ( var stream = new MemoryStream ( state ) )
2020-04-09 19:48:59 +08:00
using ( var reader = new LineBufferedReader ( stream , true ) )
2020-04-30 19:03:46 +08:00
{
var decoded = Decoder . GetDecoder < Beatmap > ( reader ) . Decode ( reader ) ;
decoded . BeatmapInfo . Ruleset = editorBeatmap . BeatmapInfo . Ruleset ;
return new PassThroughWorkingBeatmap ( decoded ) . GetPlayableBeatmap ( editorBeatmap . BeatmapInfo . Ruleset ) ;
}
2020-04-09 19:48:59 +08:00
}
private class PassThroughWorkingBeatmap : WorkingBeatmap
{
private readonly IBeatmap beatmap ;
public PassThroughWorkingBeatmap ( IBeatmap beatmap )
: base ( beatmap . BeatmapInfo , null )
{
this . beatmap = beatmap ;
}
protected override IBeatmap GetBeatmap ( ) = > beatmap ;
2023-06-08 15:17:44 +08:00
public override Texture GetBackground ( ) = > throw new NotImplementedException ( ) ;
2020-04-09 19:48:59 +08:00
2020-08-07 21:31:41 +08:00
protected override Track GetBeatmapTrack ( ) = > throw new NotImplementedException ( ) ;
2021-04-17 23:47:13 +08:00
2021-08-16 00:38:01 +08:00
protected internal override ISkin GetSkin ( ) = > throw new NotImplementedException ( ) ;
2021-05-22 01:21:00 +08:00
2021-04-17 23:47:13 +08:00
public override Stream GetStream ( string storagePath ) = > throw new NotImplementedException ( ) ;
2020-04-09 19:48:59 +08:00
}
}
}