using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; using System.Text.RegularExpressions; using System.Threading; using JD.Core.ProjectManaging; using JetBrains.Annotations; using NLua; using NLua.Exceptions; using Ubisoft.Collections; namespace JD.Serialization { public class LuaSerializer : Serializer, IDisposable { #region Constants public const string TplTableName = "params"; public const string TblTableName = "TABLE"; public const string TblListTableName = "DESCRIPTOR"; public const string AnimTableName = "animPackage"; private const string TablePath = "__TablePath"; private const string TableListPath = "__TableListPath"; #endregion #region Fields private Lua _lua; private string _currentFile; private readonly List _tablePaths = new List(); private readonly List _tableObjects = new List(); private readonly Set _capturedAdditionalInstructions = new Set(); private readonly Lazy _helpersLoaded; #endregion #region Properties private static string HelpersIluFile { get; } private static Dictionary ComponentsCache { get; } = new Dictionary(); public string LastError { get; set; } public bool CommentDefaultValues { get; set; } public bool IgnoreDefaultValues { get; set; } public Set AdditionalInstructions { get; } /// /// The Serialize will resolve c# types based on their names. This is the Assembly list in which it will look. /// public static HashSet AssembliesToSearchForType { get; } #endregion #region Constructor static LuaSerializer() { AssembliesToSearchForType = new HashSet { typeof(LuaSerializer).Assembly }; if (ProjectManager.Instance.CurrentProject != null) { HelpersIluFile = DataPath.GetAbsolutePath("EngineData\\Helpers\\helpers.ilu"); } } public LuaSerializer() : this(Serialization.TypeMap.Instance) { CommentDefaultValues = false; IgnoreDefaultValues = false; } protected LuaSerializer(ITypeMap typeMap) : base(typeMap) { _helpersLoaded = new Lazy(LoadHelpersIlu); Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; CommentDefaultValues = false; IgnoreDefaultValues = false; IncludeSpecialFields = true; AdditionalInstructions = new Set(); } #endregion #region Read private static readonly object LuaLock = new object(); private Lua Lua { get { if (_lua == null) { // Lua Library is not thread safe and uses a static dictionary on its initialization. lock (LuaLock) { _lua = new Lua(); } AddDefaultFunctions(_lua); } return _lua; } } #region Interface /// /// Creates a new Serializer and initializes it with the engine declarations from the data. /// public static LuaSerializer Create() { var serializer = new LuaSerializer { RethrowExceptions = true }; serializer.InitializeEngineTypes(); return serializer; } /// /// Returns the T component from the specified document. /// public T ReadComponent(string filePath) where T : ActorComponent_Template { if (!File.Exists(filePath)) { throw new FileNotFoundException($"Unable to read component because the file \"{filePath}\" doesn't exist."); } string cacheKey; using (var md5 = MD5.Create()) { using (var stream = File.OpenRead(filePath)) { var hash = string.Join("", md5.ComputeHash(stream).Select(b => b.ToString())); cacheKey = $"{hash}_{typeof(T)}"; } } if (ComponentsCache.ContainsKey(cacheKey)) { return (T)ComponentsCache[cacheKey]; } var actor = LoadObjectFromFile(filePath) as Actor_Template; var component = actor?.COMPONENTS.OfType().First(); ComponentsCache[cacheKey] = component ?? throw new Exception($"Could not find a component of type {typeof(T).Name} in the file \"{filePath}\""); return component; } /// /// Returns the component of type TComponent from the specified document. /// public TComponent ReadActorComponent(string fileName) where TComponent : ActorComponent { var actor = (Actor)LoadObjectFromFile(fileName); var component = actor.COMPONENTS.OfType().First(); if (component == null) { throw new Exception($"Could not find a component of type {typeof(TComponent).Name} in the file {fileName}"); } return component; } /// /// Fills the provided object with the lua content. /// /// The lua to deserialize. /// Object in which to deserialize. /// When specified, this is the opening property of the document. public void Deserialize(string luaContent, object toFill, string propertyName = null) { InitializePropertyName(toFill, ref propertyName); var luaTable = GetLuaTableFromString(luaContent, propertyName); if (luaTable != null) { if (toFill is IUAFCollection collection) { FillCollectionFromLuaTable(collection, luaTable); } else { FillObjectFromLuaTable(toFill, luaTable); } } } /// /// Saves the content of the object and returns it as a lua string. /// public string Serialize(object toSerialize, string propertyName = null) { InitializePropertyName(toSerialize, ref propertyName); var stringBuilder = new StringWriter(); Serialize(new LuaWriter(stringBuilder) { IgnoreDefaultValues = IgnoreDefaultValues }, toSerialize, propertyName); return stringBuilder.ToString(); } private void Serialize(LuaWriter writer, object toSerialize, string propertyName) { // Null property means the type name is written on the begin of the file (like the engine does). // example : params = { NAME = "MyType" } WriteProperty(writer, toSerialize, propertyType: null, propertyName: propertyName); } /// /// Loads main engine types from helpers. /// public void InitializeEngineTypes() { DoFile(System.IO.Path.Combine(Root, "EngineData/Helpers/helpers.ilu")); DoFile(System.IO.Path.Combine(Root, "EngineData/Helpers/gameplay_types.ilu")); } /// /// Defines autonomous objects from helpers. /// public void RegisterAutonomous() { DoFile(System.IO.Path.Combine(Root, "EngineData/Helpers/autonomous.ilu")); } public string Root { get { var result = ProjectManager.Instance.CurrentProject.DataPath.Replace("\\", "/"); if (result.Length != 0 && result[result.Length - 1] != '/') result += '/'; return result; } } [UsedImplicitly] private void IncludeReference(string str) { if (string.IsNullOrEmpty(str)) { return; } var prevGlobalFile = (string)Lua["ITF_LocalFile"]; var prevGlobalPath = (string)Lua["ITF_LocalPath"]; var fullFile = System.IO.Path.Combine(Root, str).Replace("\\", "/"); Lua["ITF_LocalFile"] = fullFile; Lua["ITF_LocalPath"] = fullFile.Substring(0, fullFile.LastIndexOf('/')); DoFile(fullFile); Lua["ITF_LocalFile"] = prevGlobalFile; Lua["ITF_LocalPath"] = prevGlobalPath; } public object ObjectFromLuaString(string content, string propertyName = null) { return GetObjectFromLuaString(content, propertyName); } public object ObjectFromLuaStream(Stream stream, string propertyName = null) { return GetObjectFromLuaString(ToString(stream), propertyName); } public LuaTable LuaTableFromFile(string file, string propertyName = null) { if (string.IsNullOrEmpty(propertyName)) { Configure(file); propertyName = GetTableNameFromFileName(file); } using (var stream = File.OpenRead(file)) { return LuaTableFromStream(stream, propertyName); } } public LuaTable LuaTableFromStream(Stream stream, string propertyName) { return GetLuaTableFromString(ToString(stream), propertyName); } public object CreateObjectFromLuaTable(LuaTable table, Type objectType = null) { object result = null; if (objectType != null && BasicTypeList.IsBasicType(objectType)) { return ConvertLuaValueToBasicType(table["VAL"], objectType); } var expectedType = (string)table["NAME"]; if (objectType == null || (expectedType != null && expectedType != objectType.Name)) { objectType = ResolveTypeFromLuaTable(table); } // Lua tables for objects normally follow a classic description : a NAME attribute indicating the class name, and a table describing the class object. // But it is legal to only provide the NAME attribute. // If objectType is still null, the table may be missing in the object description. // In this case, use the NAME attribute to create a default object. if (objectType == null && expectedType != null && table.Keys.Count == 1) // 1 element in table => only the name. { objectType = ResolveTypeFromLuaString(expectedType, null); } if (objectType != null) { result = Activator.CreateInstance(TypeMap.ToRuntimeType(objectType)); if (result is SpecialCustomTypeDescriptor descriptor) AttachAdditionalInstructions(descriptor); FillObjectFromLuaTable(result, table); } return result; } private Type ResolveTypeFromLuaTable(LuaTable table) { // Receives this : { AlphaClip= { Id=12 } } // Finds the "AlphaClip" string. Type result = null; foreach (KeyValuePair property in table) { if (property.Value is LuaTable) { result = ResolveTypeFromLuaString((string)property.Key, null); break; } } return result; } #endregion #region Implementation private void DoFile(string path) { try { using (var stream = File.OpenRead(path)) { DoString(ToString(stream)); } } catch (Exception ex) { LastError = ex.Message; if (RethrowExceptions) throw; } } private void DoString(string content) { if (content == null) { throw new ArgumentNullException(nameof(content)); } try { Lua.DoString(content); } catch (Exception ex) { LastError = ex.Message; if (RethrowExceptions) throw; } } private void AddDefaultFunctions(Lua lua) { lua.RegisterFunction("vectorNew", new Vec3d(), typeof(Vec3d).GetMethod("Vec3dLua")); lua.RegisterFunction("vector2dNew", new Vec2d(), typeof(Vec2d).GetMethod("Vec2dLua")); lua.RegisterFunction("includeReference", this, GetType().GetMethod("IncludeReference", BindingFlags.Instance | BindingFlags.NonPublic)); } private object GetObjectFromLuaString(string luaString, string propertyName) { InitializePropertyName(null, ref propertyName); var luaTable = GetLuaTableFromString(luaString, propertyName); if (luaTable == null) return null; if (propertyName == TblListTableName) { var result = luaTable.Values.OfType().Select(table => CreateObjectFromLuaTable(table)).ToArray(); return ToTypedList(result); } else { return CreateObjectFromLuaTable(luaTable); } } /// /// Returns an UAFList with the most specific type possible. /// private IList ToTypedList(object[] array) { IList result; if (array.Length > 0 && array.All(item => item.GetType() == array[0].GetType())) { result = (IList)Activator.CreateInstance(typeof(UAFList<>).MakeGenericType(array[0].GetType())); } else { result = new UAFList(); } foreach (var item in array) { result.Add(item); } return result; } protected override object LoadObjectFromFileImplementation(string file, Type type) { Configure(file); _currentFile = file; try { var tableName = GetTableNameFromFileName(file); using (var stream = File.OpenRead(file)) { return GetObjectFromLuaString(ToString(stream), tableName); } } finally { _currentFile = string.Empty; } } private bool LoadHelpersIlu() { bool usingFile = File.Exists(HelpersIluFile); try { // Fallback : use a light version of the helpers.ilu, distributed with the serialization lib. DoString(usingFile ? File.ReadAllText(HelpersIluFile) : ToString(new MemoryStream(Properties.Resources.helpers))); } catch (Exception exception) { throw new LuaException(usingFile ? $"Error while loading helpers from file \"{HelpersIluFile}\"" : "Error while loading helpers", exception); } return true; } private LuaTable GetLuaTableFromString(string luaString, string propertyName) { if (!_helpersLoaded.Value) { throw new LuaException("Error while loading helpers."); } _tablePaths.Clear(); _tableObjects.Clear(); LastError = null; try { GetAdditionalInstructions(luaString); DoString(luaString); Lua.DoString("ExpandTable(\"" + propertyName + "\")"); } catch (Exception ex) { LastError = ex.Message; if (RethrowExceptions) throw new LuaException($"Error reading file {_currentFile}.", ex); return null; } return (LuaTable)Lua[propertyName]; } private void FillObjectFromLuaTable(object toFill, LuaTable table, bool skipTypeDeclaration = true) { LuaTable typeTable = table; // Skip the type declaration. if (skipTypeDeclaration) { var tablePath = table[TablePath]; table = table.Values.OfType().FirstOrDefault(); if (table != null && tablePath != null) { table[TablePath] = tablePath; } } // When the object is an empty table, just don't fill it. if (table == null) { return; } foreach (var property in SerializableFieldsSorted(toFill.GetType())) { var propertyName = FormatPropertyName(property); var luaValue = GetValueIgnoreCase(table, propertyName); if (luaValue == null) { // process Table not found TypeMap.ProcessEmptyField(toFill, property); continue; } var subTable = luaValue as LuaTable; if (subTable == null || BasicTypeList.IsBasicType(property.PropertyType)) { FillBaseObject(toFill, luaValue, property, -1); } else { if (IsCollection(property.PropertyType)) { // Reuse existing list instead of creating a new one so that we can safely listen to that list. var collection = (IUAFCollection)property.GetValue(toFill, null); if (collection == null) { collection = (IUAFCollection)Activator.CreateInstance(property.PropertyType); property.SetValue(toFill, collection, null); } FillCollectionFromLuaTable(collection, subTable); } else { property.SetValue(toFill, CreateObjectFromLuaTable(subTable, property.PropertyType), null); } } } FillSpecialProperties(toFill, typeTable); } private void FillCollectionFromLuaTable(IUAFCollection collection, LuaTable table) { collection.Clear(); if (collection is IUAFDictionary) { foreach (LuaTable val in table.Values) { if (val["VAL"] == null) { // Legacy syntax. Convert to new syntax. DoString("emptyTable = {}"); LuaTable valTable = (LuaTable)Lua["emptyTable"]; if (val["NAME"] != null) valTable["NAME"] = val["NAME"]; LuaTable subTable = val.Values.OfType().FirstOrDefault(); if (subTable == null) continue; foreach (string key in val.Keys) { if (subTable.Equals(val[key])) valTable[key] = subTable; } val["VAL"] = valTable; } var element = Activator.CreateInstance(collection.ElementType); FillObjectFromLuaTable(element, val, skipTypeDeclaration: false); collection.Add(element); } } else { foreach (var val in table.Values) { collection.Add(CreateObjectFromLuaTable((LuaTable)val, collection.ElementType)); } } } private void FillSpecialProperties(object toFill, LuaTable table) { if (table[TableListPath] != null) { var tablePath = (string)table[TableListPath]; tablePath = tablePath.Substring(0, tablePath.LastIndexOf('.')).ToLower(); ((SpecialCustomTypeDescriptor)toFill).__TableListPath = new Path(tablePath + ".tbll"); } else if (table[TablePath] != null) { var tablePath = (string)table[TablePath]; tablePath = tablePath.Substring(0, tablePath.LastIndexOf('.')).ToLower(); var tabIdx = _tablePaths.IndexOf(tablePath); var castedObj = toFill as SpecialCustomTypeDescriptor; if (tabIdx >= 0) { castedObj.__TableListPath = new Path(tablePath + ".tbll"); _tableObjects[tabIdx].__TableListPath = new Path(tablePath + ".tbll"); _tableObjects[tabIdx].__TablePath = null; } else { castedObj.__TablePath = new Path(tablePath + ".tbl"); _tablePaths.Add(tablePath); _tableObjects.Add(castedObj); } } } private void FillBaseObject(object parent, object value, PropertyInfo property, int index) { if (value is LuaTable subTable) // reference to global variable { value = subTable["VAL"]; if (parent is SpecialCustomTypeDescriptor descriptor) { AddGlobalVariable(property, index, descriptor, subTable); } } property.SetValue(parent, ConvertLuaValueToBasicType(value, property.PropertyType), null); } private static void AddGlobalVariable(PropertyInfo property, int index, SpecialCustomTypeDescriptor parentClass, LuaTable subTable) { if (parentClass.__GlobalVariables == null) parentClass.__GlobalVariables = new UAFList(); var globalVariable = new PropertyGlobalVariable { Name = (string)subTable["VAR"], PropertyInfoName = property.Name, }; if (subTable["REF"] != null) globalVariable.Path = new Path((string)subTable["REF"]); if (index >= 0) globalVariable.PropertyIndex = new object[] { index }; parentClass.__GlobalVariables.Add(globalVariable); } private object ConvertLuaValueToBasicType(object luaValue, Type targetType) { if (luaValue == null || luaValue.GetType() == targetType) { return luaValue; } if (targetType.IsEnum) { return Convert.ToInt32(luaValue); } if (targetType == typeof(Angle) && (luaValue is double || luaValue is float)) { return new Angle((float)((double)luaValue), true); } // prevent lua data reference if (targetType == typeof(Vec3d) && luaValue is Vec2d d) { return new Vec3d(d); } var conv = TypeDescriptor.GetConverter(targetType); if (conv.CanConvertFrom(luaValue.GetType())) { return conv.ConvertFrom(luaValue); } if (targetType == typeof(uint) && (luaValue is double || luaValue is float) && (double)luaValue < 0.0) { // force uint32 to -1 when float is negative. return 0xffffffff; } return Convert.ChangeType(luaValue, targetType); } private object GetValueIgnoreCase(LuaTable table, string propertyName) { var keyFound = table.Keys.OfType().FirstOrDefault(x => x.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); if (keyFound != null) { return table[keyFound]; } return null; } #endregion // Read #region Utilities /// /// Tries to find the value of the given field. Returns wether or not the uint value was successfully found. /// public static bool TryToFindUintValue(string filePath, string fieldName, out uint value) { if (TryToFindValue(filePath, fieldName, @"\d+", out var valueAsString)) { return uint.TryParse(valueAsString, out value); } value = 0; return false; } /// /// Tries to find the value of the given field. Returns wether or not the int value was successfully found. /// public static bool TryToFindIntValue(string filePath, string fieldName, out int value) { if (TryToFindValue(filePath, fieldName, @"-?\d+", out var valueAsString)) { return int.TryParse(valueAsString, out value); } value = 0; return false; } /// /// Tries to find the value of the given field. Returns wether or not the bool value was successfully found. /// public static bool TryToFindBoolValue(string filePath, string fieldName, out bool value) { if (TryToFindValue(filePath, fieldName, @"(?:true|false)", out var valueAsString)) { return bool.TryParse(valueAsString, out value); } value = false; return false; } /// /// Tries to find the value of the given field. Returns wether or not the string was successfully found. /// public static bool TryToFindStringValue(string filePath, string fieldName, out string value) { if (TryToFindValue(filePath, fieldName, "\".+?\"", out var matchedValue)) { value = matchedValue.Replace("\"", ""); return true; } value = string.Empty; return false; } private static bool TryToFindValue(string filePath, string fieldName, string valuePattern, out string matchedValue) { matchedValue = string.Empty; if (!File.Exists(filePath)) { return false; } var fileContent = File.ReadAllText(filePath); var regexString = fieldName + @"\s*=\s*(?" + valuePattern + ")"; // example: "Id = 0001" can be matched and "0001" is captured var regex = new Regex(regexString, RegexOptions.IgnoreCase); var match = regex.Match(fileContent); if (!match.Success) { return false; } matchedValue = match.Groups["fieldValue"].Value.Trim(' '); return true; } private static string ToString(Stream stream) { var reader = new StreamReader(stream); var content = reader.ReadToEnd(); reader.Dispose(); return content; } #endregion #endregion #region Write #region Interface protected override bool SaveObjectToFileImplementation(object obj, string file) { // Prevent reentry on TablePath; Path tablePath = null; var customTypeDescriptor = obj as SpecialCustomTypeDescriptor; if (customTypeDescriptor?.__TablePath != null) { tablePath = customTypeDescriptor.__TablePath; customTypeDescriptor.__TablePath = null; } SaveToFile(obj, file); SaveReferredFiles(obj); if (tablePath != null) customTypeDescriptor.__TablePath = tablePath; return true; } private void SaveToFile(object obj, string file) { Configure(file); var tableName = GetTableNameFromFileName(file); using (var writer = new LuaWriter(file)) { WriteAdditionalInstructions(writer, obj, SpecialCustomTypeDescriptor.InstructionPosition.Start); writer.IgnoreDefaultValues = IgnoreDefaultValues; Serialize(writer, obj, tableName); WriteAdditionalInstructions(writer, obj, SpecialCustomTypeDescriptor.InstructionPosition.End); } } /// /// Saves all child files of the given object. /// public void SaveReferredFiles(object instance) { if (instance != null) { foreach (var field in instance.GetType().SerializableFields()) { var propertyValue = field.GetValue(instance, null); if (propertyValue != null) { if (propertyValue is IUAFCollection collection) { foreach (var value in collection.SerializableElements) { SaveReferredFiles(value); } } else if (propertyValue is SpecialCustomTypeDescriptor customTypeDescriptor) { if (customTypeDescriptor.__TablePath != null) { SaveToFile(customTypeDescriptor, customTypeDescriptor.__TablePath.getAbsolute()); } SaveReferredFiles(customTypeDescriptor); } } } } } #endregion #region Private /// /// Configures serializer. /// /// Path to the file private void Configure(string filepath) { // Additional instructions that must be rewritten. AdditionalInstructions.Add("includeReference"); AdditionalInstructions.Add("appendTable"); if (!string.IsNullOrEmpty(filepath) && filepath.EndsWith(".ilu")) { RegisterAutonomous(); InitializeEngineTypes(); } } #endregion #region Utilities public static string GetTableNameFromFileName(string file) { string ext = file.Substring(file.LastIndexOf('.') + 1); switch (ext) { case "tbl": return TblTableName; case "tbll": return TblListTableName; case "ilu": // only valid table ilu are anims ! return TblListTableName; default: return TplTableName; } } private void InitializePropertyName(object value, ref string propertyName) { if (string.IsNullOrEmpty(propertyName)) { propertyName = value is IUAFCollection ? TblListTableName : TblTableName; } } private Type ResolveTypeFromLuaString(string name, Type baseType) { string fullName = "JD.Serialization." + name; var result = FindType(fullName); if (result == null && baseType != null && baseType.IsGenericType) { result = FindType("JD.Serialization." + name + "`1"); if (result != null) result = result.MakeGenericType(baseType.GetGenericArguments()[0]); } if (result == null) result = baseType; if (result == null) { throw new Exception("Cannot find a type matching name " + fullName); } return result; } private Type FindType(string name) { var result = Type.GetType(name); if (result == null) { foreach (var assembly in AssembliesToSearchForType) { result = assembly.GetType(name); if (result != null) { return result; } } } return result; } private string ConvertBaseTypeToLuaString(object obj) { if (obj.GetType().IsEnum) { return (Convert.ToInt32(obj)).ToString(); } if (obj is float f) { return ((double)f).ToString("0.000000"); } if (obj is double d) { return d.ToString("0.000000"); } switch (obj.GetType().Name) { case "string": case "String": case "String8": case "StringID": case "ObjectPath": case "Path": case "URL": case "Platform": return $"\"{(obj.ToString()).Replace("\"", "\\\"")}\""; case "Vec2d": { // Convert the values to double to get the same result when saving in engine and in tools. var vector = (Vec2d)obj; return $"vector2dNew({(double)vector.X:0.000000},{(double)vector.Y:0.000000})"; } case "Vec3d": { // Convert the values to double to get the same result when saving in engine and in tools. var vector = (Vec3d)obj; return $"vectorNew({(double)vector.X:0.000000},{(double)vector.Y:0.000000},{(double)vector.Z:0.000000})"; } case "Color": return $"\"0x{((Color)obj).ToArgb():X8}\""; case "Angle": return $"{((Angle)obj).ToDegree()}"; case "Boolean": if ((bool)obj) return "1"; else return "0"; } return obj.ToString(); } #endregion #region Implementation private void WriteProperty(LuaWriter writer, object propertyValue, Type propertyType, string propertyName = null) { if (propertyValue == null) return; var valueType = propertyValue.GetType(); if (propertyValue is SpecialCustomTypeDescriptor customType && customType.__TablePath != null) { writer.WriteLine(@"getTable(""{0}""),", customType.__TablePath.getRelativePath()); } else if (BasicTypeList.IsBasicType(valueType)) { // Adds comment for enumerations to describe the effective value. var comment = string.Empty; if (valueType.IsEnum) { comment = $" -- {propertyValue}"; } writer.WriteLine("{0} = {1},{2}", propertyName, ConvertBaseTypeToLuaString(propertyValue), comment); } else if (IsCollection(valueType)) { if (((IUAFCollection)propertyValue).Count > 0) { writer.WriteLine("{0} =", propertyName); WriteLuaList(writer, (IUAFCollection)propertyValue); } } else { writer.WriteLine("{0} =", propertyName); WriteLuaClass(writer, propertyValue, propertyType); } } private void WriteLuaList(LuaWriter writer, IUAFCollection collection) { using (writer.Indent()) { if (BasicTypeList.IsBasicType(collection.ElementType)) { foreach (var item in collection.SerializableElements) { using (writer.Indent()) { writer.WriteLine("VAL = {0},", ConvertBaseTypeToLuaString(item)); } } } else if (collection is IUAFDictionary) { foreach (var item in collection.SerializableElements) { WriteClassContent(writer, item); } } else { foreach (var item in collection.SerializableElements) { NonLuaSerializableAttribute isNonLuaSerializable = (NonLuaSerializableAttribute)Attribute.GetCustomAttribute(item.GetType(), typeof(NonLuaSerializableAttribute)); if (isNonLuaSerializable == null) { WriteLuaClass(writer, item, collection.ElementType); } } } } } private void WriteLuaClass(LuaWriter writer, object instance, Type propertyType) { if (instance == null) { return; } if (instance is SpecialCustomTypeDescriptor customTypeDescriptor && customTypeDescriptor.__TablePath != null) { writer.WriteLine("getTable(\"{0}\"),", customTypeDescriptor.__TablePath.ToString()); LuaSerializer tableSerializer = new LuaSerializer { CommentDefaultValues = true }; tableSerializer.SaveObjectToFile(instance, customTypeDescriptor.__TablePath.getAbsolute()); return; } using (writer.Indent()) { string typeName = FormatTypeName(instance.GetType()); if (instance.GetType() != propertyType) { writer.WriteLine("NAME = \"{0}\",", typeName); } writer.WriteLine("{0} = ", typeName); WriteClassContent(writer, instance); } } private void WriteClassContent(LuaWriter writer, object instance) { using (writer.Indent()) { foreach (var field in SerializableFields(instance.GetType())) { var isDefaultValue = instance.HasDefaultValue(field); if (isDefaultValue && writer.IgnoreDefaultValues) { continue; } WriteProperty(writer, field.GetValue(instance, null), field.PropertyType, FormatPropertyName(field)); } } } #endregion #endregion #region Additional Instructions /// /// Retrieves additional instructions for current Lua file. /// /// Content of Lua. private void GetAdditionalInstructions(string luaString) { var luaLines = luaString.Split('\n').Where(line => !string.IsNullOrEmpty(line)).ToArray(); var instructionPosition = SpecialCustomTypeDescriptor.InstructionPosition.Start; foreach (var line in luaLines) { var instructionFound = false; foreach (var instruction in AdditionalInstructions) { if (line.Trim().StartsWith(instruction.ToString())) { _capturedAdditionalInstructions.Add(new SpecialCustomTypeDescriptor.AdditionalInstruction(line, instructionPosition)); instructionFound = true; break; } } if (!instructionFound && instructionPosition == SpecialCustomTypeDescriptor.InstructionPosition.Start) { if (!line.Trim().StartsWith("--")) { instructionPosition = SpecialCustomTypeDescriptor.InstructionPosition.End; } } } } /// /// Writes additional instructions to current Lua saving file. /// /// Instance of the stream. /// Written object. /// private void WriteAdditionalInstructions(LuaWriter writer, object obj, SpecialCustomTypeDescriptor.InstructionPosition instructionPosition) { var customType = obj as SpecialCustomTypeDescriptor; if (customType == null && obj is IEnumerable array) customType = array.ToArray()[0] as SpecialCustomTypeDescriptor; if (customType != null) { foreach (var instruction in customType.__AdditionalInstructions.Where(i => i.Position == instructionPosition)) writer.WriteLine(instruction.Instruction); } } /// /// Attaches additional instructions to serialization object. /// /// Instance of the object. private void AttachAdditionalInstructions(SpecialCustomTypeDescriptor obj) { obj.__AdditionalInstructions.AddRange(_capturedAdditionalInstructions); _capturedAdditionalInstructions.Clear(); } #endregion protected IEnumerable SerializableFieldsSorted(Type type) { return SerializableFields(type).OrderByDescending(x => IsCollection(x.PropertyType) ? 0 : 1); } public void Dispose() { _lua?.Dispose(); } } }