1226 lines
42 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|