2023-02-03 09:53:22 +00: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.
|
|
|
|
|
2023-04-29 17:52:22 +00:00
|
|
|
using System;
|
2023-02-03 09:53:22 +00:00
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.IO;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Text;
|
|
|
|
using Newtonsoft.Json;
|
2023-02-07 07:23:24 +00:00
|
|
|
using osu.Framework.Bindables;
|
2023-02-03 09:53:22 +00:00
|
|
|
using osu.Framework.Graphics;
|
|
|
|
using osu.Framework.Testing;
|
|
|
|
using osu.Game.Screens.Edit;
|
|
|
|
using osu.Game.Skinning;
|
|
|
|
|
|
|
|
namespace osu.Game.Overlays.SkinEditor
|
|
|
|
{
|
|
|
|
public partial class SkinEditorChangeHandler : EditorChangeHandler
|
|
|
|
{
|
2023-02-15 07:28:42 +00:00
|
|
|
private readonly ISerialisableDrawableContainer? firstTarget;
|
2023-02-03 09:53:22 +00:00
|
|
|
|
2023-02-07 07:23:24 +00:00
|
|
|
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
|
2023-02-15 07:01:26 +00:00
|
|
|
private readonly BindableList<ISerialisableDrawable>? components;
|
2023-02-03 09:53:22 +00:00
|
|
|
|
|
|
|
public SkinEditorChangeHandler(Drawable targetScreen)
|
|
|
|
{
|
|
|
|
// To keep things simple, we are currently only handling the current target screen for undo / redo.
|
|
|
|
// In the future we'll want this to cover all changes, even to skin's `InstantiationInfo`.
|
|
|
|
// We'll also need to consider cases where multiple targets are on screen at the same time.
|
|
|
|
|
2023-02-15 07:28:42 +00:00
|
|
|
firstTarget = targetScreen.ChildrenOfType<ISerialisableDrawableContainer>().FirstOrDefault();
|
2023-02-03 09:53:22 +00:00
|
|
|
|
2023-02-07 07:23:24 +00:00
|
|
|
if (firstTarget == null)
|
|
|
|
return;
|
|
|
|
|
2023-02-15 07:01:26 +00:00
|
|
|
components = new BindableList<ISerialisableDrawable> { BindTarget = firstTarget.Components };
|
2023-02-07 07:23:24 +00:00
|
|
|
components.BindCollectionChanged((_, _) => SaveState());
|
2023-02-03 09:53:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
protected override void WriteCurrentStateToStream(MemoryStream stream)
|
|
|
|
{
|
|
|
|
if (firstTarget == null)
|
|
|
|
return;
|
|
|
|
|
2023-02-15 06:47:41 +00:00
|
|
|
var skinnableInfos = firstTarget.CreateSerialisedInfo().ToArray();
|
2023-02-03 09:53:22 +00:00
|
|
|
string json = JsonConvert.SerializeObject(skinnableInfos, new JsonSerializerSettings { Formatting = Formatting.Indented });
|
|
|
|
stream.Write(Encoding.UTF8.GetBytes(json));
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void ApplyStateChange(byte[] previousState, byte[] newState)
|
|
|
|
{
|
|
|
|
if (firstTarget == null)
|
|
|
|
return;
|
|
|
|
|
2023-02-15 06:47:41 +00:00
|
|
|
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(Encoding.UTF8.GetString(newState));
|
2023-02-03 09:53:22 +00:00
|
|
|
|
|
|
|
if (deserializedContent == null)
|
|
|
|
return;
|
|
|
|
|
2023-04-29 17:52:22 +00:00
|
|
|
SerialisedDrawableInfo[] skinnableInfos = deserializedContent.ToArray();
|
|
|
|
ISerialisableDrawable[] targetComponents = firstTarget.Components.ToArray();
|
2023-02-03 09:53:22 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
// Store components based on type for later lookup
|
|
|
|
var typedComponents = new Dictionary<Type, List<ISerialisableDrawable>>();
|
2023-04-29 17:52:22 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
foreach (var component in targetComponents)
|
2023-04-29 17:52:22 +00:00
|
|
|
{
|
2023-04-30 10:05:55 +00:00
|
|
|
Type lookup = component.GetType();
|
2023-04-29 17:52:22 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
if (!typedComponents.TryGetValue(lookup, out List<ISerialisableDrawable>? typeComponents))
|
|
|
|
typedComponents.Add(lookup, typeComponents = new List<ISerialisableDrawable>());
|
2023-04-29 17:52:22 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
typeComponents.Add(component);
|
2023-04-29 17:52:22 +00:00
|
|
|
}
|
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
// Remove all components
|
|
|
|
for (int i = targetComponents.Length - 1; i >= 0; i--)
|
|
|
|
firstTarget.Remove(targetComponents[i], false);
|
2023-04-30 00:01:18 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
// Keeps count of how many components for each type were already revived
|
|
|
|
Dictionary<Type, int> typedComponentCounter = typedComponents.Keys.ToDictionary(t => t, _ => 0);
|
2023-04-30 00:01:18 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
foreach (var skinnableInfo in skinnableInfos)
|
2023-02-03 09:53:22 +00:00
|
|
|
{
|
2023-04-30 10:05:55 +00:00
|
|
|
Type lookup = skinnableInfo.Type;
|
2023-04-30 00:01:18 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
if (!typedComponents.TryGetValue(lookup, out List<ISerialisableDrawable>? typeComponents))
|
|
|
|
{
|
|
|
|
firstTarget.Add((ISerialisableDrawable)skinnableInfo.CreateInstance());
|
|
|
|
continue;
|
|
|
|
}
|
2023-04-30 00:01:18 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
int typeComponentsUsed = typedComponentCounter[lookup]++;
|
2023-04-30 00:01:18 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
ISerialisableDrawable component;
|
2023-04-29 17:52:22 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
if (typeComponentsUsed < typeComponents.Count)
|
|
|
|
// Re-use unused component
|
|
|
|
((Drawable)(component = typeComponents[typeComponentsUsed])).ApplySerialisedInfo(skinnableInfo);
|
|
|
|
else
|
|
|
|
// Create new one
|
|
|
|
component = (ISerialisableDrawable)skinnableInfo.CreateInstance();
|
2023-02-03 09:53:22 +00:00
|
|
|
|
2023-04-30 10:05:55 +00:00
|
|
|
firstTarget.Add(component);
|
2023-02-03 09:53:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|