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 -------------------------------------------------------- /// /// State is just a clone component with copies of all props /// public Dictionary 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 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 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(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); } } /// /// Experimental method /// Here you can add data/ callbacks/ save stuff to variables /// after loading a xml skeletom /// public virtual void Hydrate() { } #endregion } }