JD2022-TU1/main/tools/framework/JD.Serialization/Serializers/Lua/LuaSerializer.cs

1226 lines
42 KiB
C#

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<string> _tablePaths = new List<string>();
private readonly List<SpecialCustomTypeDescriptor> _tableObjects = new List<SpecialCustomTypeDescriptor>();
private readonly Set<SpecialCustomTypeDescriptor.AdditionalInstruction> _capturedAdditionalInstructions = new Set<SpecialCustomTypeDescriptor.AdditionalInstruction>();
private readonly Lazy<bool> _helpersLoaded;
#endregion
#region Properties
private static string HelpersIluFile { get; }
private static Dictionary<string, ActorComponent_Template> ComponentsCache { get; } = new Dictionary<string, ActorComponent_Template>();
public string LastError { get; set; }
public bool CommentDefaultValues { get; set; }
public bool IgnoreDefaultValues { get; set; }
public Set<string> AdditionalInstructions { get; }
/// <summary>
/// The Serialize will resolve c# types based on their names. This is the Assembly list in which it will look.
/// </summary>
public static HashSet<Assembly> AssembliesToSearchForType { get; }
#endregion
#region Constructor
static LuaSerializer()
{
AssembliesToSearchForType = new HashSet<Assembly> { 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<bool>(LoadHelpersIlu);
Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;
CommentDefaultValues = false;
IgnoreDefaultValues = false;
IncludeSpecialFields = true;
AdditionalInstructions = new Set<string>();
}
#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
/// <summary>
/// Creates a new Serializer and initializes it with the engine declarations from the data.
/// </summary>
public static LuaSerializer Create()
{
var serializer = new LuaSerializer { RethrowExceptions = true };
serializer.InitializeEngineTypes();
return serializer;
}
/// <summary>
/// Returns the T component from the specified document.
/// </summary>
public T ReadComponent<T>(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<T>().First();
ComponentsCache[cacheKey] = component ?? throw new Exception($"Could not find a component of type {typeof(T).Name} in the file \"{filePath}\"");
return component;
}
/// <summary>
/// Returns the component of type TComponent from the specified document.
/// </summary>
public TComponent ReadActorComponent<TComponent>(string fileName)
where TComponent : ActorComponent
{
var actor = (Actor)LoadObjectFromFile(fileName);
var component = actor.COMPONENTS.OfType<TComponent>().First();
if (component == null)
{
throw new Exception($"Could not find a component of type {typeof(TComponent).Name} in the file {fileName}");
}
return component;
}
/// <summary>
/// Fills the provided object with the lua content.
/// </summary>
/// <param name="luaContent">The lua to deserialize.</param>
/// <param name="toFill">Object in which to deserialize.</param>
/// <param name="propertyName">When specified, this is the opening property of the document.</param>
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);
}
}
}
/// <summary>
/// Saves the content of the object and returns it as a lua string.
/// </summary>
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);
}
/// <summary>
/// Loads main engine types from helpers.
/// </summary>
public void InitializeEngineTypes()
{
DoFile(System.IO.Path.Combine(Root, "EngineData/Helpers/helpers.ilu"));
DoFile(System.IO.Path.Combine(Root, "EngineData/Helpers/gameplay_types.ilu"));
}
/// <summary>
/// Defines autonomous objects from helpers.
/// </summary>
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<object, object> 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<LuaTable>().Select(table => CreateObjectFromLuaTable(table)).ToArray();
return ToTypedList(result);
}
else
{
return CreateObjectFromLuaTable(luaTable);
}
}
/// <summary>
/// Returns an UAFList with the most specific type possible.
/// </summary>
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<object>();
}
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<LuaTable>().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<LuaTable>().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<PropertyGlobalVariable>();
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<string>().FirstOrDefault(x => x.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
if (keyFound != null)
{
return table[keyFound];
}
return null;
}
#endregion // Read
#region Utilities
/// <summary>
/// Tries to find the value of the given field. Returns wether or not the uint value was successfully found.
/// </summary>
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;
}
/// <summary>
/// Tries to find the value of the given field. Returns wether or not the int value was successfully found.
/// </summary>
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;
}
/// <summary>
/// Tries to find the value of the given field. Returns wether or not the bool value was successfully found.
/// </summary>
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;
}
/// <summary>
/// Tries to find the value of the given field. Returns wether or not the string was successfully found.
/// </summary>
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*(?<fieldValue>" + 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);
}
}
/// <summary>
/// Saves all child files of the given object.
/// </summary>
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
/// <summary>
/// Configures serializer.
/// </summary>
/// <param name="filepath">Path to the file</param>
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
/// <summary>
/// Retrieves additional instructions for current Lua file.
/// </summary>
/// <param name="luaString">Content of Lua.</param>
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;
}
}
}
}
/// <summary>
/// Writes additional instructions to current Lua saving file.
/// </summary>
/// <param name="writer">Instance of the stream.</param>
/// <param name="obj">Written object.</param>
/// <param name="instructionPosition"></param>
private void WriteAdditionalInstructions(LuaWriter writer, object obj, SpecialCustomTypeDescriptor.InstructionPosition instructionPosition)
{
var customType = obj as SpecialCustomTypeDescriptor;
if (customType == null && obj is IEnumerable<object> 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);
}
}
/// <summary>
/// Attaches additional instructions to serialization object.
/// </summary>
/// <param name="obj">Instance of the object.</param>
private void AttachAdditionalInstructions(SpecialCustomTypeDescriptor obj)
{
obj.__AdditionalInstructions.AddRange(_capturedAdditionalInstructions);
_capturedAdditionalInstructions.Clear();
}
#endregion
protected IEnumerable<PropertyInfo> SerializableFieldsSorted(Type type)
{
return SerializableFields(type).OrderByDescending(x => IsCollection(x.PropertyType) ? 0 : 1);
}
public void Dispose()
{
_lua?.Dispose();
}
}
}