// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Newtonsoft.Json; using osu.Framework.Bindables; 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 { private readonly ISerialisableDrawableContainer? firstTarget; // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable private readonly BindableList? components; 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. firstTarget = targetScreen.ChildrenOfType().FirstOrDefault(); if (firstTarget == null) return; components = new BindableList { BindTarget = firstTarget.Components }; components.BindCollectionChanged((_, _) => SaveState()); } protected override void WriteCurrentStateToStream(MemoryStream stream) { if (firstTarget == null) return; var skinnableInfos = firstTarget.CreateSerialisedInfo().ToArray(); 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; var deserializedContent = JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(newState)); if (deserializedContent == null) return; SerialisedDrawableInfo[] skinnableInfos = deserializedContent.ToArray(); ISerialisableDrawable[] targetComponents = firstTarget.Components.ToArray(); // Store indexes based on type for later lookup var skinnableInfoIndexes = new Dictionary>(); var targetComponentsIndexes = new Dictionary>(); for (int i = 0; i < skinnableInfos.Length; i++) { Type lookup = skinnableInfos[i].Type; if (!skinnableInfoIndexes.TryGetValue(lookup, out List? infoIndexes)) skinnableInfoIndexes.Add(lookup, infoIndexes = new List()); infoIndexes.Add(i); } for (int i = 0; i < targetComponents.Length; i++) { Type lookup = targetComponents[i].GetType(); if (!targetComponentsIndexes.TryGetValue(lookup, out List? componentIndexes)) targetComponentsIndexes.Add(lookup, componentIndexes = new List()); componentIndexes.Add(i); } foreach ((Type lookup, List infoIndexes) in skinnableInfoIndexes) { if (!targetComponentsIndexes.TryGetValue(lookup, out List? componentIndexes)) componentIndexes = new List(0); int j = 0; for (int i = 0; i < infoIndexes.Count; i++) { if (i >= componentIndexes.Count) // Add new component firstTarget.Add((ISerialisableDrawable)skinnableInfos[infoIndexes[i]].CreateInstance()); else // Modify existing component ((Drawable)targetComponents[componentIndexes[j++]]).ApplySerialisedInfo(skinnableInfos[infoIndexes[i]]); } // Remove extra components for (; j < componentIndexes.Count; j++) firstTarget.Remove(targetComponents[componentIndexes[j]], false); } foreach ((Type lookup, List componentIndexes) in targetComponentsIndexes) { if (skinnableInfoIndexes.ContainsKey(lookup)) continue; // Remove extra components that weren't removed above for (int i = 0; i < componentIndexes.Count; i++) firstTarget.Remove(targetComponents[componentIndexes[i]], false); } } } }