Files
barotrauma-localmods/Quick Interactions/CSharp/Client/CrabUI/Components/CUIComponent/CUIComponent.Serialization.cs

468 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.IO;
using Barotrauma;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Xml;
using System.Xml.Linq;
using HarmonyLib;
namespace QICrabUI
{
public partial class CUIComponent
{
public record CompareResult(bool equal, string firstMismatch = "")
{
public static implicit operator bool(CompareResult r) => r.equal;
}
public static bool DeepCompareVerbose(CUIComponent a, CUIComponent b)
{
CompareResult result = DeepCompare(a, b);
if (result.equal) CUI.Log($"{a} == {b}");
else CUI.Log($"{result.firstMismatch}");
return result.equal;
}
public static CompareResult DeepCompare(CUIComponent a, CUIComponent b)
{
if (a.GetType() != b.GetType()) return new CompareResult(false, $"type mismatch: {a} | {b}");
Type T = a.GetType();
CUITypeMetaData meta = CUITypeMetaData.Get(T);
foreach (var (key, pi) in meta.Serializable)
{
if (!object.Equals(pi.GetValue(a), pi.GetValue(b)))
{
return new CompareResult(false, $"{pi}: {a}{pi.GetValue(a)} | {b}{pi.GetValue(b)}");
}
}
if (a.Children.Count != b.Children.Count)
{
return new CompareResult(false, $"child count mismatch: {a}{CUI.ArrayToString(a.Children)} | {b}{CUI.ArrayToString(b.Children)}");
}
for (int i = 0; i < a.Children.Count; i++)
{
CompareResult sub = DeepCompare(a.Children[i], b.Children[i]);
if (!sub.equal) return sub;
}
return new CompareResult(true);
}
#region State --------------------------------------------------------
/// <summary>
/// State is just a clone component with copies of all props
/// </summary>
public Dictionary<string, CUIComponent> States { get; set; } = new();
// TODO why all clones are unreal? this is sneaky, and i don't remember what's it for
public CUIComponent Clone()
{
CUIComponent clone = new CUIComponent()
{
Unreal = true,
};
clone.ApplyState(this);
return clone;
}
public void SaveStateAs(string name) => States[name] = this.Clone();
public void LoadState(string name) => ApplyState(States.GetValueOrDefault(name));
public void ForgetState(string name) => States.Remove(name);
//TODO think about edge cases (PassPropsToChild)
public void ApplyState(CUIComponent state)
{
Stopwatch sw = Stopwatch.StartNew();
if (state == null) return;
//TODO why not closest relative?
Type targetType = state.GetType() == GetType() ? GetType() : typeof(CUIComponent);
CUITypeMetaData meta = CUITypeMetaData.Get(targetType);
//TODO Megacringe, fix it
foreach (PropertyInfo pi in meta.Serializable.Values)
{
if (pi.PropertyType.IsValueType || pi.PropertyType == typeof(string))
{
pi.SetValue(this, pi.GetValue(state));
}
else
{
object value = pi.GetValue(state);
if (value == null)
{
pi.SetValue(this, null);
continue;
}
if (pi.PropertyType.IsAssignableTo(typeof(ICloneable)))
{
ICloneable cloneable = (ICloneable)pi.GetValue(state);
object clone = cloneable.Clone();
pi.SetValue(this, clone);
}
else
{
CUI.Info($"Ekhem, can't copy {pi} prop from {state} to {this} because it's not cloneable");
}
}
}
//TODO Megacringe, fix it
foreach (PropertyInfo pi in meta.Serializable.Values)
{
if (pi.PropertyType.IsValueType && !object.Equals(pi.GetValue(state), pi.GetValue(this)))
{
pi.SetValue(this, pi.GetValue(state));
}
}
}
#endregion
#region XML --------------------------------------------------------
public static bool ForceSaveAllProps { get; set; } = false;
public static bool SaveAfterLoad { get; set; } = true;
public string SavePath { get; set; }
public virtual XElement ToXML(CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
try
{
if (Unserializable) return null;
Type type = GetType();
XElement e = new XElement(type.Name);
PackProps(e, propAttribute);
foreach (CUIComponent child in Children)
{
if (!this.BreakSerialization)
{
e.Add(child.ToXML(propAttribute));
}
}
return e;
}
catch (Exception e)
{
CUI.Warning(e);
return new XElement("Error", e.Message);
}
}
public virtual void FromXML(XElement element, string baseFolder = null)
{
foreach (XElement childElement in element.Elements())
{
Type childType = CUIReflection.GetComponentTypeByName(childElement.Name.ToString());
if (childType == null) continue;
CUIComponent child = (CUIComponent)Activator.CreateInstance(childType);
child.FromXML(childElement, baseFolder);
//CUI.Log($"{this}[{child.AKA}] = {child} ");
this.Append(child, child.AKA);
}
ExtractProps(element, baseFolder);
}
protected void ExtractProps(XElement element, string baseFolder = null)
{
Type type = GetType();
CUITypeMetaData meta = CUITypeMetaData.Get(type);
foreach (XAttribute attribute in element.Attributes())
{
if (!meta.Serializable.ContainsKey(attribute.Name.ToString()))
{
CUIDebug.Error($"Can't parse prop {attribute.Name} in {type.Name} because type metadata doesn't contain that prop (is it a property? fields aren't supported yet)");
continue;
}
PropertyInfo prop = meta.Serializable[attribute.Name.ToString()];
MethodInfo parse = null;
if (CUIExtensions.Parse.ContainsKey(prop.PropertyType))
{
parse = CUIExtensions.Parse[prop.PropertyType];
}
parse ??= prop.PropertyType.GetMethod(
"Parse",
BindingFlags.Public | BindingFlags.Static,
new Type[] { typeof(string) }
);
Func<string, object> ParseWithContext = null;
//HACK
if (prop.PropertyType == typeof(CUISprite) && baseFolder != null)
{
ParseWithContext = (raw) => CUISprite.ParseWithContext(raw, baseFolder);
}
if (parse == null)
{
if (prop.PropertyType.IsEnum)
{
try
{
prop.SetValue(this, Enum.Parse(prop.PropertyType, attribute.Value));
}
catch (Exception e)
{
CUIDebug.Error($"Can't parse {attribute.Value} into {prop.PropertyType.Name}\n{e}");
}
}
else
{
CUIDebug.Error($"Can't parse prop {prop.Name} in {type.Name} because it's type {prop.PropertyType.Name} is missing Parse method");
}
}
else
{
try
{
object result = null;
if (ParseWithContext != null)
{
result = ParseWithContext(attribute.Value);
}
else
{
result = parse.Invoke(null, new object[] { attribute.Value });
}
prop.SetValue(this, result);
}
catch (Exception e)
{
CUIDebug.Error($"Can't parse {attribute.Value} into {prop.PropertyType.Name}\n{e}");
}
}
}
}
protected void PackProps(XElement element, CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
Type type = GetType();
CUITypeMetaData meta = CUITypeMetaData.Get(type);
SortedDictionary<string, PropertyInfo> props = propAttribute switch
{
CUIAttribute.CUISerializable => meta.Serializable,
CUIAttribute.Calculated => meta.Calculated,
_ => meta.Serializable,
};
foreach (string key in props.Keys)
{
try
{
object value = props[key].GetValue(this);
// it's default value for this prop
if (!ForceSaveAllProps && meta.Default != null && Object.Equals(value, CUIReflection.GetNestedValue(meta.Default, key)))
{
continue;
}
MethodInfo customToString = CUIExtensions.CustomToString.GetValueOrDefault(props[key].PropertyType);
if (customToString != null)
{
element?.SetAttributeValue(key, customToString.Invoke(null, new object[] { value }));
}
else
{
element?.SetAttributeValue(key, value);
}
}
catch (Exception e)
{
CUI.Warning($"Failed to serialize prop: {e.Message}");
CUI.Warning($"{key} in {this}");
}
}
}
public string Serialize(CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
try
{
XElement e = this.ToXML(propAttribute);
return e.ToString();
}
catch (Exception e)
{
CUI.Error(e);
return e.Message;
}
}
public static CUIComponent Deserialize(string raw, string baseFolder = null)
{
return Deserialize(XElement.Parse(raw));
}
public static CUIComponent Deserialize(XElement e, string baseFolder = null)
{
try
{
Type type = CUIReflection.GetComponentTypeByName(e.Name.ToString());
if (type == null) return null;
CUIComponent c = (CUIComponent)Activator.CreateInstance(type);
// c.RemoveAllChildren();
c.FromXML(e, baseFolder);
CUIComponent.RunRecursiveOn(c, (component) => component.Hydrate());
return c;
}
catch (Exception ex)
{
CUIDebug.Error(ex);
return null;
}
}
public void LoadSelfFromFile(string path, bool searchForSpritesInTheSameFolder = true, bool saveAfterLoad = false)
{
try
{
XDocument xdoc = XDocument.Load(path);
RemoveAllChildren();
if (searchForSpritesInTheSameFolder) FromXML(xdoc.Root, Path.GetDirectoryName(path));
else FromXML(xdoc.Root);
CUIComponent.RunRecursiveOn(this, (component) => component.Hydrate());
SavePath = path;
if (SaveAfterLoad && saveAfterLoad) SaveToTheSamePath();
}
catch (Exception ex)
{
CUI.Warning(ex);
}
}
public static CUIComponent LoadFromFile(string path, bool searchForSpritesInTheSameFolder = true, bool saveAfterLoad = false)
{
try
{
XDocument xdoc = XDocument.Load(path);
CUIComponent result;
if (searchForSpritesInTheSameFolder)
{
result = Deserialize(xdoc.Root, Path.GetDirectoryName(path));
}
else result = Deserialize(xdoc.Root);
result.SavePath = path;
if (SaveAfterLoad && saveAfterLoad) result.SaveToTheSamePath();
return result;
}
catch (Exception ex)
{
CUIDebug.Error(ex);
return null;
}
}
public static T LoadFromFile<T>(string path, bool searchForSpritesInTheSameFolder = true, bool saveAfterLoad = false) where T : CUIComponent
{
try
{
XDocument xdoc = XDocument.Load(path);
T result;
if (searchForSpritesInTheSameFolder)
{
result = (T)Deserialize(xdoc.Root, Path.GetDirectoryName(path));
}
else result = (T)Deserialize(xdoc.Root);
result.SavePath = path;
if (SaveAfterLoad && saveAfterLoad) result.SaveToTheSamePath();
return result;
}
catch (Exception ex)
{
CUIDebug.Error(ex);
return null;
}
}
public void LoadFromTheSameFile()
{
if (SavePath == null)
{
CUI.Warning($"Can't load {this} from The Same Path, SavePath is null");
return;
}
LoadSelfFromFile(SavePath);
}
public void SaveToTheSamePath()
{
if (SavePath == null)
{
CUI.Warning($"Can't save {this} To The Same Path, SavePath is null");
return;
}
SaveToFile(SavePath);
}
public void SaveToFile(string path, CUIAttribute propAttribute = CUIAttribute.CUISerializable)
{
try
{
XDocument xdoc = new XDocument();
xdoc.Add(this.ToXML(propAttribute));
xdoc.Save(path);
SavePath = path;
}
catch (Exception e)
{
CUI.Warning(e);
}
}
/// <summary>
/// Experimental method
/// Here you can add data/ callbacks/ save stuff to variables
/// after loading a xml skeletom
/// </summary>
public virtual void Hydrate()
{
}
#endregion
}
}