960 lines
45 KiB
C#
960 lines
45 KiB
C#
using FishNet.CodeGenerating.Extension;
|
|
using FishNet.CodeGenerating.Helping;
|
|
using FishNet.CodeGenerating.Helping.Extension;
|
|
using FishNet.Connection;
|
|
using FishNet.Object;
|
|
using FishNet.Object.Prediction;
|
|
using FishNet.Object.Prediction.Delegating;
|
|
using FishNet.Serializing;
|
|
using FishNet.Serializing.Helping;
|
|
using FishNet.Transporting;
|
|
using MonoFN.Cecil;
|
|
using MonoFN.Cecil.Cil;
|
|
using MonoFN.Cecil.Rocks;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using SR = System.Reflection;
|
|
|
|
namespace FishNet.CodeGenerating.Processing
|
|
{
|
|
internal class PredictionProcessor : CodegenBase
|
|
{
|
|
#region Types.
|
|
private enum InsertType
|
|
{
|
|
First,
|
|
Last,
|
|
Current
|
|
}
|
|
|
|
private class CreatedPredictionFields
|
|
{
|
|
/// <summary>
|
|
/// Delegate for calling replicate user logic.
|
|
/// </summary>
|
|
public readonly FieldReference ReplicateULDelegate;
|
|
/// <summary>
|
|
/// Delegate for calling replicate user logic.
|
|
/// </summary>
|
|
public readonly FieldReference ReconcileULDelegate;
|
|
/// <summary>
|
|
/// Replicate data buffered on the server.
|
|
/// </summary>
|
|
public readonly FieldReference ServerReplicateDatas;
|
|
/// <summary>
|
|
/// Replicate data buffered on the client.
|
|
/// </summary>
|
|
public readonly FieldReference ClientReplicateDatas;
|
|
/// <summary>
|
|
/// Last reconcile data received from the server.
|
|
/// </summary>
|
|
public readonly FieldReference ReconcileData;
|
|
/// <summary>
|
|
/// A buffer to read replicates into.
|
|
/// </summary>
|
|
public readonly FieldReference ServerReplicateReaderBuffer;
|
|
|
|
public CreatedPredictionFields(FieldReference replicateULDelegate, FieldReference reconcileULDelegate, FieldReference serverReplicateDatas, FieldReference clientReplicateDatas, FieldReference reconcileData,
|
|
FieldReference serverReplicateReaderBuffer)
|
|
{
|
|
ReplicateULDelegate = replicateULDelegate;
|
|
ReconcileULDelegate = reconcileULDelegate;
|
|
ServerReplicateDatas = serverReplicateDatas;
|
|
ClientReplicateDatas = clientReplicateDatas;
|
|
ReconcileData = reconcileData;
|
|
ServerReplicateReaderBuffer = serverReplicateReaderBuffer;
|
|
}
|
|
}
|
|
|
|
private class PredictionReaders
|
|
{
|
|
public MethodReference ReplicateReader;
|
|
public MethodReference ReconcileReader;
|
|
|
|
public PredictionReaders(MethodReference replicateReader, MethodReference reconcileReader)
|
|
{
|
|
ReplicateReader = replicateReader;
|
|
ReconcileReader = reconcileReader;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public.
|
|
public string IReplicateData_FullName = typeof(IReplicateData).FullName;
|
|
public string IReconcileData_FullName = typeof(IReconcileData).FullName;
|
|
public TypeReference ReplicateULDelegate_TypeRef;
|
|
public TypeReference ReconcileULDelegate_TypeRef;
|
|
public MethodReference IReplicateData_GetTick_MethodRef;
|
|
public MethodReference IReplicateData_SetTick_MethodRef;
|
|
public MethodReference IReconcileData_GetTick_MethodRef;
|
|
public MethodReference IReconcileData_SetTick_MethodRef;
|
|
public MethodReference Unity_GetGameObject_MethodRef;
|
|
#endregion
|
|
|
|
#region Const.
|
|
public const string REPLICATE_LOGIC_PREFIX = "Logic_Replicate___";
|
|
public const string REPLICATE_READER_PREFIX = "Reader_Replicate___";
|
|
public const string RECONCILE_LOGIC_PREFIX = "Logic_Reconcile___";
|
|
public const string RECONCILE_READER_PREFIX = "Reader_Reconcile___";
|
|
#endregion
|
|
|
|
public override bool ImportReferences()
|
|
{
|
|
System.Type locType;
|
|
SR.MethodInfo locMi;
|
|
|
|
ReplicateULDelegate_TypeRef = base.ImportReference(typeof(ReplicateUserLogicDelegate<>));
|
|
ReconcileULDelegate_TypeRef = base.ImportReference(typeof(ReconcileUserLogicDelegate<>));
|
|
|
|
//GetGameObject.
|
|
locMi = typeof(UnityEngine.Component).GetMethod("get_gameObject");
|
|
Unity_GetGameObject_MethodRef = base.ImportReference(locMi);
|
|
|
|
//Get/Set tick.
|
|
locType = typeof(IReplicateData);
|
|
foreach (SR.MethodInfo mi in locType.GetMethods())
|
|
{
|
|
if (mi.Name == nameof(IReplicateData.GetTick))
|
|
IReplicateData_GetTick_MethodRef = base.ImportReference(mi);
|
|
else if (mi.Name == nameof(IReplicateData.SetTick))
|
|
IReplicateData_SetTick_MethodRef = base.ImportReference(mi);
|
|
}
|
|
|
|
locType = typeof(IReconcileData);
|
|
foreach (SR.MethodInfo mi in locType.GetMethods())
|
|
{
|
|
if (mi.Name == nameof(IReconcileData.GetTick))
|
|
IReconcileData_GetTick_MethodRef = base.ImportReference(mi);
|
|
else if (mi.Name == nameof(IReconcileData.SetTick))
|
|
IReconcileData_SetTick_MethodRef = base.ImportReference(mi);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal bool Process(TypeDefinition typeDef, ref uint rpcCount)
|
|
{
|
|
bool modified = false;
|
|
modified |= ProcessLocal(typeDef, ref rpcCount);
|
|
|
|
return modified;
|
|
}
|
|
|
|
#region Setup and checks.
|
|
/// <summary>
|
|
/// Gets number of predictions by checking for prediction attributes. This does not perform error checking.
|
|
/// </summary>
|
|
/// <param name="typeDef"></param>
|
|
/// <returns></returns>
|
|
internal uint GetPredictionCount(TypeDefinition typeDef)
|
|
{
|
|
/* Currently only one prediction method is allowed per typeDef.
|
|
* Return 1 soon as a method is found. */
|
|
foreach (MethodDefinition methodDef in typeDef.Methods)
|
|
{
|
|
foreach (CustomAttribute customAttribute in methodDef.CustomAttributes)
|
|
{
|
|
if (customAttribute.Is(base.GetClass<AttributeHelper>().ReplicateAttribute_FullName))
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Ensures only one prediction and reconile method exist per typeDef, and outputs finding.
|
|
/// </summary>
|
|
/// <returns>True if there is only one set of prediction methods. False if none, or more than one set.</returns>
|
|
internal bool GetPredictionMethods(TypeDefinition typeDef, out MethodDefinition replicateMd, out MethodDefinition reconcileMd)
|
|
{
|
|
replicateMd = null;
|
|
reconcileMd = null;
|
|
|
|
bool error = false;
|
|
foreach (MethodDefinition methodDef in typeDef.Methods)
|
|
{
|
|
foreach (CustomAttribute customAttribute in methodDef.CustomAttributes)
|
|
{
|
|
if (customAttribute.Is(base.GetClass<AttributeHelper>().ReplicateAttribute_FullName))
|
|
{
|
|
if (!MethodIsPrivate(methodDef) || AlreadyFound(replicateMd))
|
|
error = true;
|
|
else
|
|
replicateMd = methodDef;
|
|
}
|
|
else if (customAttribute.Is(base.GetClass<AttributeHelper>().ReconcileAttribute_FullName))
|
|
{
|
|
if (!MethodIsPrivate(methodDef) || AlreadyFound(reconcileMd))
|
|
error = true;
|
|
else
|
|
reconcileMd = methodDef;
|
|
}
|
|
if (error)
|
|
break;
|
|
}
|
|
if (error)
|
|
break;
|
|
}
|
|
|
|
bool MethodIsPrivate(MethodDefinition md)
|
|
{
|
|
bool isPrivate = md.Attributes.HasFlag(MethodAttributes.Private);
|
|
if (!isPrivate)
|
|
base.LogError($"Method {md.Name} within {typeDef.Name} is a prediction method and must be private.");
|
|
return isPrivate;
|
|
}
|
|
|
|
bool AlreadyFound(MethodDefinition md)
|
|
{
|
|
bool alreadyFound = (md != null);
|
|
if (alreadyFound)
|
|
base.LogError($"{typeDef.Name} contains multiple prediction sets; currently only one set is allowed.");
|
|
|
|
return alreadyFound;
|
|
}
|
|
|
|
if (!error && ((replicateMd == null) != (reconcileMd == null)))
|
|
{
|
|
base.LogError($"{typeDef.Name} must contain both a [Replicate] and [Reconcile] method when using prediction.");
|
|
error = true;
|
|
}
|
|
|
|
if (error || (replicateMd == null) || (reconcileMd == null))
|
|
return false;
|
|
else
|
|
return true;
|
|
}
|
|
#endregion
|
|
|
|
private bool ProcessLocal(TypeDefinition typeDef, ref uint rpcCount)
|
|
{
|
|
MethodDefinition replicateMd;
|
|
MethodDefinition reconcileMd;
|
|
|
|
//Not using prediction methods.
|
|
if (!GetPredictionMethods(typeDef, out replicateMd, out reconcileMd))
|
|
return false;
|
|
|
|
//If replication methods found but this hierarchy already has max.
|
|
if (rpcCount >= NetworkBehaviourHelper.MAX_RPC_ALLOWANCE)
|
|
{
|
|
base.LogError($"{typeDef.FullName} and inherited types exceed {NetworkBehaviourHelper.MAX_RPC_ALLOWANCE} replicated methods. Only {NetworkBehaviourHelper.MAX_RPC_ALLOWANCE} replicated methods are supported per inheritance hierarchy.");
|
|
return false;
|
|
}
|
|
|
|
bool parameterError = false;
|
|
parameterError |= HasParameterError(replicateMd, typeDef, true);
|
|
parameterError |= HasParameterError(reconcileMd, typeDef, false);
|
|
if (parameterError)
|
|
return false;
|
|
|
|
TypeDefinition replicateDataTd = replicateMd.Parameters[0].ParameterType.CachedResolve(base.Session);
|
|
TypeDefinition reconcileDataTd = reconcileMd.Parameters[0].ParameterType.CachedResolve(base.Session);
|
|
//Ensure datas implement interfaces.
|
|
bool interfacesImplemented = true;
|
|
DataImplementInterfaces(replicateMd, true, ref interfacesImplemented);
|
|
DataImplementInterfaces(reconcileMd, false, ref interfacesImplemented);
|
|
if (!interfacesImplemented)
|
|
return false;
|
|
if (!TickFieldIsNonSerializable(replicateDataTd, true))
|
|
return false;
|
|
if (!TickFieldIsNonSerializable(reconcileDataTd, false))
|
|
return false;
|
|
|
|
/* Make sure data can serialize. Use array type, this will
|
|
* generate a serializer for element type as well. */
|
|
bool canSerialize;
|
|
//Make sure replicate data can serialize.
|
|
canSerialize = base.GetClass<GeneralHelper>().HasSerializerAndDeserializer(replicateDataTd.MakeArrayType(), true);
|
|
if (!canSerialize)
|
|
{
|
|
base.LogError($"Replicate data type {replicateDataTd.Name} does not support serialization. Use a supported type or create a custom serializer.");
|
|
return false;
|
|
}
|
|
//Make sure reconcile data can serialize.
|
|
canSerialize = base.GetClass<GeneralHelper>().HasSerializerAndDeserializer(reconcileDataTd, true);
|
|
if (!canSerialize)
|
|
{
|
|
base.LogError($"Reconcile data type {reconcileDataTd.Name} does not support serialization. Use a supported type or create a custom serializer.");
|
|
return false;
|
|
}
|
|
//Creates fields for buffers.
|
|
CreatedPredictionFields predictionFields;
|
|
CreateFields(typeDef, replicateMd, reconcileMd, out predictionFields);
|
|
|
|
PredictionReaders predictionReaders;
|
|
MethodDefinition replicateULMd;
|
|
MethodDefinition reconcileULMd;
|
|
CreatePredictionMethods(typeDef, replicateMd, reconcileMd, predictionFields, rpcCount, out predictionReaders, out replicateULMd, out reconcileULMd);
|
|
InitializeCollections(typeDef, replicateMd, predictionFields);
|
|
InitializeULDelegates(typeDef, predictionFields, replicateMd, reconcileMd, replicateULMd, reconcileULMd);
|
|
RegisterRpcs(typeDef, rpcCount, predictionReaders);
|
|
|
|
rpcCount++;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the tick field for GetTick is non-serializable.
|
|
/// </summary>
|
|
/// <param name="dataTd"></param>
|
|
/// <returns></returns>
|
|
private bool TickFieldIsNonSerializable(TypeDefinition dataTd, bool replicate)
|
|
{
|
|
string methodName = (replicate) ? IReplicateData_GetTick_MethodRef.Name : IReconcileData_GetTick_MethodRef.Name;
|
|
MethodDefinition getMd = dataTd.GetMethod(methodName);
|
|
|
|
//Try to find ldFld.
|
|
Instruction ldFldInst = null;
|
|
foreach (Instruction item in getMd.Body.Instructions)
|
|
{
|
|
if (item.OpCode == OpCodes.Ldfld)
|
|
{
|
|
ldFldInst = item;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//If ldFld not found.
|
|
if (ldFldInst == null)
|
|
{
|
|
base.LogError($"{dataTd.FullName} method {getMd.Name} does not return a field type for the Tick. Make a new private field of uint type and return it's value within {getMd.Name}.");
|
|
return false;
|
|
}
|
|
//Make sure the field is private.
|
|
else
|
|
{
|
|
FieldDefinition fd = (FieldDefinition)ldFldInst.Operand;
|
|
if (!fd.Attributes.HasFlag(FieldAttributes.Private))
|
|
{
|
|
base.LogError($"{dataTd.FullName} method {getMd.Name} returns a tick field but it's not marked as private. Make the field {fd.Name} private.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//All checks pass.
|
|
return true;
|
|
}
|
|
|
|
private void DataImplementInterfaces(MethodDefinition methodDef, bool isReplicate, ref bool interfacesImplemented)
|
|
{
|
|
TypeReference dataTr = methodDef.Parameters[0].ParameterType;
|
|
string interfaceName = (isReplicate) ? IReplicateData_FullName : IReconcileData_FullName;
|
|
//If does not implement.
|
|
if (!dataTr.CachedResolve(base.Session).ImplementsInterfaceRecursive(base.Session, interfaceName))
|
|
{
|
|
string name = (isReplicate) ? typeof(IReplicateData).Name : typeof(IReconcileData).Name;
|
|
base.LogError($"Prediction data type {dataTr.Name} for method {methodDef.Name} in class {methodDef.DeclaringType.Name} must implement the {name} interface.");
|
|
interfacesImplemented = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers RPCs that prediction uses.
|
|
/// </summary>
|
|
private void RegisterRpcs(TypeDefinition typeDef, uint hash, PredictionReaders readers)
|
|
{
|
|
MethodDefinition injectionMethodDef = typeDef.GetMethod(NetworkBehaviourProcessor.NETWORKINITIALIZE_EARLY_INTERNAL_NAME);
|
|
ILProcessor processor = injectionMethodDef.Body.GetILProcessor();
|
|
List<Instruction> insts = new List<Instruction>();
|
|
|
|
Register(readers.ReplicateReader.CachedResolve(base.Session), true);
|
|
Register(readers.ReconcileReader.CachedResolve(base.Session), false);
|
|
|
|
void Register(MethodDefinition readerMd, bool replicate)
|
|
{
|
|
insts.Add(processor.Create(OpCodes.Ldarg_0));
|
|
insts.Add(processor.Create(OpCodes.Ldc_I4, (int)hash));
|
|
/* Create delegate and call NetworkBehaviour method. */
|
|
insts.Add(processor.Create(OpCodes.Ldarg_0));
|
|
insts.Add(processor.Create(OpCodes.Ldftn, readerMd));
|
|
|
|
MethodReference ctorMr;
|
|
MethodReference callMr;
|
|
if (replicate)
|
|
{
|
|
ctorMr = base.GetClass<NetworkBehaviourHelper>().ReplicateRpcDelegateConstructor_MethodRef;
|
|
callMr = base.GetClass<NetworkBehaviourHelper>().RegisterReplicateRpc_MethodRef;
|
|
}
|
|
else
|
|
{
|
|
ctorMr = base.GetClass<NetworkBehaviourHelper>().ReconcileRpcDelegateConstructor_MethodRef;
|
|
callMr = base.GetClass<NetworkBehaviourHelper>().RegisterReconcileRpc_MethodRef;
|
|
}
|
|
|
|
insts.Add(processor.Create(OpCodes.Newobj, ctorMr));
|
|
insts.Add(processor.Create(OpCodes.Call, callMr));
|
|
}
|
|
|
|
processor.InsertLast(insts);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes collection fields made during this process.
|
|
/// </summary>
|
|
/// <param name="predictionFields"></param>
|
|
private void InitializeCollections(TypeDefinition typeDef, MethodDefinition replicateMd, CreatedPredictionFields predictionFields)
|
|
{
|
|
GeneralHelper gh = base.GetClass<GeneralHelper>();
|
|
TypeReference replicateDataTr = replicateMd.Parameters[0].ParameterType;
|
|
MethodDefinition injectionMethodDef = typeDef.GetMethod(NetworkBehaviourProcessor.NETWORKINITIALIZE_EARLY_INTERNAL_NAME);
|
|
ILProcessor processor = injectionMethodDef.Body.GetILProcessor();
|
|
|
|
Generate(predictionFields.ClientReplicateDatas, true);
|
|
Generate(predictionFields.ServerReplicateDatas, false);
|
|
|
|
void Generate(FieldReference fr, bool isList)
|
|
{
|
|
MethodDefinition ctorMd = base.GetClass<GeneralHelper>().List_TypeRef.CachedResolve(base.Session).GetConstructor();
|
|
GenericInstanceType collectionGit;
|
|
if (isList)
|
|
gh.GetGenericLists(replicateDataTr, out collectionGit);
|
|
else
|
|
gh.GetGenericQueues(replicateDataTr, out collectionGit);
|
|
MethodReference ctorMr = ctorMd.MakeHostInstanceGeneric(base.Session, collectionGit);
|
|
|
|
List<Instruction> insts = new List<Instruction>();
|
|
|
|
insts.Add(processor.Create(OpCodes.Ldarg_0));
|
|
insts.Add(processor.Create(OpCodes.Newobj, ctorMr));
|
|
insts.Add(processor.Create(OpCodes.Stfld, fr));
|
|
processor.InsertFirst(insts);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes collection fields made during this process.
|
|
/// </summary>
|
|
/// <param name="predictionFields"></param>
|
|
private void InitializeULDelegates(TypeDefinition typeDef, CreatedPredictionFields predictionFields, MethodDefinition replicateMd, MethodDefinition reconcileMd, MethodDefinition replicateULMd, MethodDefinition reconcileULMd)
|
|
{
|
|
TypeReference replicateDataTr = replicateMd.Parameters[0].ParameterType;
|
|
TypeReference reconcileDataTr = reconcileMd.Parameters[0].ParameterType;
|
|
MethodDefinition injectionMethodDef = typeDef.GetMethod(NetworkBehaviourProcessor.NETWORKINITIALIZE_EARLY_INTERNAL_NAME);
|
|
ILProcessor processor = injectionMethodDef.Body.GetILProcessor();
|
|
List<Instruction> insts = new List<Instruction>();
|
|
|
|
Generate(replicateULMd, replicateDataTr, predictionFields.ReplicateULDelegate, typeof(ReplicateUserLogicDelegate<>), ReplicateULDelegate_TypeRef);
|
|
Generate(reconcileULMd, reconcileDataTr, predictionFields.ReconcileULDelegate, typeof(ReconcileUserLogicDelegate<>), ReconcileULDelegate_TypeRef);
|
|
|
|
void Generate(MethodDefinition ulMd, TypeReference dataTr, FieldReference fr, System.Type delegateType, TypeReference delegateTr)
|
|
{
|
|
insts.Clear();
|
|
|
|
MethodDefinition ctorMd = delegateTr.CachedResolve(base.Session).GetFirstConstructor(base.Session, true);
|
|
GenericInstanceType collectionGit;
|
|
GetGenericULDelegate(dataTr, delegateType, out collectionGit);
|
|
MethodReference ctorMr = ctorMd.MakeHostInstanceGeneric(base.Session, collectionGit);
|
|
|
|
insts.Add(processor.Create(OpCodes.Ldarg_0));
|
|
insts.Add(processor.Create(OpCodes.Ldarg_0));
|
|
insts.Add(processor.Create(OpCodes.Ldftn, ulMd));
|
|
insts.Add(processor.Create(OpCodes.Newobj, ctorMr));
|
|
insts.Add(processor.Create(OpCodes.Stfld, fr));
|
|
processor.InsertFirst(insts);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Creates field buffers for replicate datas.
|
|
/// </summary>
|
|
/// <param name="typeDef"></param>
|
|
/// <param name="replicateMd"></param>
|
|
/// <param name=""></param>
|
|
/// <returns></returns>
|
|
private void CreateFields(TypeDefinition typeDef, MethodDefinition replicateMd, MethodDefinition reconcileMd, out CreatedPredictionFields predictionFields)
|
|
{
|
|
GeneralHelper gh = base.GetClass<GeneralHelper>();
|
|
TypeReference replicateDataTr = replicateMd.Parameters[0].ParameterType;
|
|
TypeReference replicateDataArrTr = replicateDataTr.MakeArrayType();
|
|
TypeReference reconcileDataTr = reconcileMd.Parameters[0].ParameterType;
|
|
|
|
GenericInstanceType replicateULDelegateGit;
|
|
GenericInstanceType reconcileULDelegateGit;
|
|
GenericInstanceType lstDataGit;
|
|
GenericInstanceType queueDataGit;
|
|
GetGenericULDelegate(replicateDataTr, typeof(ReplicateUserLogicDelegate<>), out replicateULDelegateGit);
|
|
GetGenericULDelegate(reconcileDataTr, typeof(ReconcileUserLogicDelegate<>), out reconcileULDelegateGit);
|
|
gh.GetGenericLists(replicateDataTr, out lstDataGit);
|
|
gh.GetGenericQueues(replicateDataTr, out queueDataGit);
|
|
|
|
/* Data buffer. */
|
|
FieldDefinition replicateULDelegateFd = new FieldDefinition($"_replicateULDelegate___{replicateMd.Name}", FieldAttributes.Private, replicateULDelegateGit);
|
|
FieldDefinition reconcileULDelegateFd = new FieldDefinition($"_reconcileULDelegate___{reconcileMd.Name}", FieldAttributes.Private, reconcileULDelegateGit);
|
|
FieldDefinition serverReplicatesFd = new FieldDefinition($"_serverReplicates___{replicateMd.Name}", FieldAttributes.Private, queueDataGit);
|
|
FieldDefinition clientReplicatesFd = new FieldDefinition($"_clientReplicates___{replicateMd.Name}", FieldAttributes.Private, lstDataGit);
|
|
FieldDefinition reconcileDataFd = new FieldDefinition($"_reconcileData___{replicateMd.Name}", FieldAttributes.Private, reconcileDataTr);
|
|
FieldDefinition serverReplicatesReadBufferFd = new FieldDefinition($"{replicateMd.Name}___serverReplicateReadBuffer", FieldAttributes.Private, replicateDataArrTr);
|
|
|
|
typeDef.Fields.Add(replicateULDelegateFd);
|
|
typeDef.Fields.Add(reconcileULDelegateFd);
|
|
typeDef.Fields.Add(serverReplicatesFd);
|
|
typeDef.Fields.Add(clientReplicatesFd);
|
|
typeDef.Fields.Add(reconcileDataFd);
|
|
typeDef.Fields.Add(serverReplicatesReadBufferFd);
|
|
|
|
predictionFields = new CreatedPredictionFields(replicateULDelegateFd, reconcileULDelegateFd, serverReplicatesFd, clientReplicatesFd, reconcileDataFd,
|
|
serverReplicatesReadBufferFd);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns if there are any errors with the prediction methods parameters and will print if so.
|
|
/// </summary>
|
|
private bool HasParameterError(MethodDefinition methodDef, TypeDefinition typeDef, bool replicateMethod)
|
|
{
|
|
//Replicate: data, asServer, channel, replaying.
|
|
//Reconcile: data, asServer, channel.
|
|
int count = (replicateMethod) ? 4 : 3;
|
|
|
|
//Check parameter count.
|
|
if (methodDef.Parameters.Count != count)
|
|
{
|
|
PrintParameterExpectations();
|
|
return true;
|
|
}
|
|
|
|
//Data check.
|
|
if (!methodDef.Parameters[0].ParameterType.IsClassOrStruct(base.Session))
|
|
{
|
|
base.LogError($"Prediction methods must use a class or structure as the first parameter type. Structures are recommended to avoid allocations.");
|
|
return true;
|
|
}
|
|
//asServer
|
|
if (methodDef.Parameters[1].ParameterType.Name != typeof(bool).Name)
|
|
{
|
|
PrintParameterExpectations();
|
|
return true;
|
|
}
|
|
//Channel.
|
|
if (methodDef.Parameters[2].ParameterType.Name != typeof(Channel).Name)
|
|
{
|
|
PrintParameterExpectations();
|
|
return true;
|
|
}
|
|
if (replicateMethod)
|
|
{
|
|
//replaying
|
|
if (methodDef.Parameters[3].ParameterType.Name != typeof(bool).Name)
|
|
{
|
|
PrintParameterExpectations();
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
void PrintParameterExpectations()
|
|
{
|
|
if (replicateMethod)
|
|
base.LogError($"Replicate method {methodDef.Name} within {typeDef.Name} requires exactly {count} parameters. In order: replicate data, asServer boolean, channel = Channel.Unreliable, replaying boolean.");
|
|
else
|
|
base.LogError($"Reconcile method {methodDef.Name} within {typeDef.Name} requires exactly {count} parameters. In order: replicate data, asServer boolean, channel = Channel.Unreliable.");
|
|
}
|
|
|
|
//No errors with parameters.
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates all methods needed for a RPC.
|
|
/// </summary>
|
|
/// <param name="originalMethodDef"></param>
|
|
/// <param name="rpcAttribute"></param>
|
|
/// <returns></returns>
|
|
private bool CreatePredictionMethods(TypeDefinition typeDef, MethodDefinition replicateMd, MethodDefinition reconcileMd, CreatedPredictionFields predictionFields, uint rpcCount, out PredictionReaders predictionReaders, out MethodDefinition replicateULMd, out MethodDefinition reconcileULMd)
|
|
{
|
|
GeneralHelper gh = base.GetClass<GeneralHelper>();
|
|
NetworkBehaviourHelper nbh = base.GetClass<NetworkBehaviourHelper>();
|
|
predictionReaders = null;
|
|
|
|
string copySuffix = "___UL";
|
|
replicateULMd = base.GetClass<GeneralHelper>().CopyIntoNewMethod(replicateMd, $"{replicateMd.Name}{copySuffix}", out _);
|
|
reconcileULMd = base.GetClass<GeneralHelper>().CopyIntoNewMethod(reconcileMd, $"{reconcileMd.Name}{copySuffix}", out _);
|
|
replicateMd.Body.Instructions.Clear();
|
|
reconcileMd.Body.Instructions.Clear();
|
|
|
|
MethodDefinition replicateReader;
|
|
MethodDefinition reconcileReader;
|
|
|
|
if (!CreateReplicate())
|
|
return false;
|
|
if (!CreateReconcile())
|
|
return false;
|
|
|
|
CreateClearReplicateCacheMethod(typeDef, replicateMd.Parameters[0].ParameterType, predictionFields);
|
|
CreateReplicateReader(typeDef, replicateMd, predictionFields, out replicateReader);
|
|
CreateReconcileReader(typeDef, reconcileMd, predictionFields, out reconcileReader);
|
|
predictionReaders = new PredictionReaders(replicateReader, reconcileReader);
|
|
|
|
bool CreateReplicate()
|
|
{
|
|
ILProcessor processor = replicateMd.Body.GetILProcessor();
|
|
ParameterDefinition replicateDataPd = replicateMd.Parameters[0];
|
|
MethodDefinition comparerMd = gh.CreateEqualityComparer(replicateDataPd.ParameterType);
|
|
gh.CreateIsDefaultComparer(replicateDataPd.ParameterType, comparerMd);
|
|
ParameterDefinition asServerPd = replicateMd.Parameters[1];
|
|
ParameterDefinition replayingPd = replicateMd.Parameters[3];
|
|
|
|
Instruction exitMethodInst = processor.Create(OpCodes.Nop);
|
|
//Exit early conditions.
|
|
processor.Emit(OpCodes.Ldarg_0); //base.
|
|
processor.Emit(OpCodes.Ldarg, asServerPd);
|
|
processor.Emit(OpCodes.Ldarg, replayingPd);
|
|
processor.Emit(OpCodes.Call, base.GetClass<NetworkBehaviourHelper>().Replicate_ExitEarly_A_MethodRef);
|
|
processor.Emit(OpCodes.Brtrue, exitMethodInst);
|
|
|
|
//Wrap server content in an asServer if statement.
|
|
Instruction notAsServerInst = processor.Create(OpCodes.Nop);
|
|
processor.Emit(OpCodes.Ldarg, asServerPd);
|
|
processor.Emit(OpCodes.Brfalse, notAsServerInst);
|
|
/***************************/
|
|
ServerCreateReplicate(replicateMd, predictionFields);
|
|
processor.Emit(OpCodes.Br, exitMethodInst);
|
|
/***************************/
|
|
|
|
//Wrap client content in an !asServer if statement.
|
|
processor.Append(notAsServerInst);
|
|
/***************************/
|
|
ClientCreateReplicate(replicateMd, predictionFields, rpcCount);
|
|
/***************************/
|
|
|
|
processor.Append(exitMethodInst);
|
|
processor.Emit(OpCodes.Ret);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool CreateReconcile()
|
|
{
|
|
ILProcessor processor = reconcileMd.Body.GetILProcessor();
|
|
ParameterDefinition reconcileDataPd = reconcileMd.Parameters[0];
|
|
ParameterDefinition asServerPd = reconcileMd.Parameters[1];
|
|
ParameterDefinition channelPd = reconcileMd.Parameters[2];
|
|
TypeReference replicateDataTr = replicateMd.Parameters[0].ParameterType;
|
|
|
|
//ExitEarly A.
|
|
Instruction exitMethodInst = processor.Create(OpCodes.Nop);
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldarg, asServerPd);
|
|
processor.Emit(OpCodes.Ldarga, channelPd);
|
|
processor.Emit(OpCodes.Call, base.GetClass<NetworkBehaviourHelper>().Reconcile_ExitEarly_A_MethodRef);
|
|
processor.Emit(OpCodes.Brtrue, exitMethodInst);
|
|
|
|
//Wrap server content in an asServer if statement.
|
|
Instruction notAsServerInst = processor.Create(OpCodes.Nop);
|
|
processor.Emit(OpCodes.Ldarg, asServerPd);
|
|
processor.Emit(OpCodes.Brfalse, notAsServerInst);
|
|
/***************************/
|
|
ServerCreateReconcile(reconcileMd, predictionFields, ref rpcCount);
|
|
/***************************/
|
|
processor.Emit(OpCodes.Br, exitMethodInst);
|
|
|
|
processor.Append(notAsServerInst);
|
|
|
|
MethodReference reconcileClientGim = nbh.Reconcile_Client_MethodRef.GetMethodReference(
|
|
base.Session, new TypeReference[] { reconcileDataPd.ParameterType, replicateDataTr });
|
|
//<T>(ReplicateULDelegate<T> replicateDel, ReconcileULDelegate<T> reconcileDel, List<T> collection,
|
|
//T data, Channel channel) where T : IReconcileData
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ReconcileULDelegate);
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ReplicateULDelegate);
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ClientReplicateDatas);
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ReconcileData);
|
|
processor.Emit(OpCodes.Ldarg, channelPd);
|
|
processor.Emit(OpCodes.Call, reconcileClientGim);
|
|
|
|
processor.Append(exitMethodInst);
|
|
processor.Emit(OpCodes.Ret);
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#region Universal prediction.
|
|
/// <summary>
|
|
/// Creates an override for the method responsible for resetting replicates.
|
|
/// </summary>
|
|
/// <param name=""></param>
|
|
/// <param name=""></param>
|
|
private void CreateClearReplicateCacheMethod(TypeDefinition typeDef, TypeReference dataTr, CreatedPredictionFields predictionFields)
|
|
{
|
|
GeneralHelper gh = base.GetClass<GeneralHelper>();
|
|
string clearDatasName = base.GetClass<NetworkBehaviourHelper>().ClearReplicateCache_Method_Name;
|
|
MethodDefinition md = typeDef.GetMethod(clearDatasName);
|
|
|
|
//Already exist when it shouldn't.
|
|
if (md != null)
|
|
{
|
|
base.LogWarning($"{typeDef.Name} overrides method {md.Name} when it should not. Logic within {md.Name} will be replaced by code generation.");
|
|
md.Body.Instructions.Clear();
|
|
}
|
|
else
|
|
{
|
|
md = new MethodDefinition(clearDatasName, (MethodAttributes.Public | MethodAttributes.Virtual), base.Module.TypeSystem.Void);
|
|
gh.CreateParameter(md, typeof(bool), "asServer");
|
|
typeDef.Methods.Add(md);
|
|
base.ImportReference(md);
|
|
}
|
|
|
|
ILProcessor processor = md.Body.GetILProcessor();
|
|
|
|
GenericInstanceType dataListGit;
|
|
gh.GetGenericLists(dataTr, out dataListGit);
|
|
//Get clear method.
|
|
MethodReference lstClearMr = gh.List_Clear_MethodRef.MakeHostInstanceGeneric(base.Session, dataListGit);
|
|
ParameterDefinition asServerPd = md.Parameters[0];
|
|
|
|
Instruction afterAsServerInst = processor.Create(OpCodes.Nop);
|
|
Instruction resetTicksInst = processor.Create(OpCodes.Nop);
|
|
|
|
processor.Emit(OpCodes.Ldarg, asServerPd);
|
|
processor.Emit(OpCodes.Brfalse_S, afterAsServerInst);
|
|
|
|
//Clear on server replicates.
|
|
MethodReference clrQueueMr = base.ImportReference(typeof(NetworkBehaviour).GetMethod(nameof(NetworkBehaviour.ClearQueue_Server_Internal)));
|
|
GenericInstanceMethod clrQueueGim = clrQueueMr.MakeGenericMethod(new TypeReference[] { dataTr });
|
|
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ServerReplicateDatas);
|
|
processor.Emit(clrQueueMr.GetCallOpCode(base.Session), clrQueueGim);
|
|
processor.Emit(OpCodes.Br_S, resetTicksInst);
|
|
processor.Append(afterAsServerInst);
|
|
//Clear on client replicates.
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ClientReplicateDatas);
|
|
processor.Emit(lstClearMr.GetCallOpCode(base.Session), lstClearMr);
|
|
|
|
processor.Append(resetTicksInst);
|
|
processor.Emit(OpCodes.Ret);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Outputs generic ReplicateULDelegate for dataTr.
|
|
/// </summary>
|
|
private void GetGenericULDelegate(TypeReference dataTr, System.Type delegateType, out GenericInstanceType git)
|
|
{
|
|
TypeReference delDataTr = base.ImportReference(delegateType);
|
|
git = delDataTr.MakeGenericInstanceType(new TypeReference[] { dataTr });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subtracts 1 from a field.
|
|
/// </summary>
|
|
private List<Instruction> SubtractFromField(MethodDefinition methodDef, FieldDefinition fieldDef)
|
|
{
|
|
List<Instruction> insts = new List<Instruction>();
|
|
ILProcessor processor = methodDef.Body.GetILProcessor();
|
|
|
|
// _field--;
|
|
insts.Add(processor.Create(OpCodes.Ldarg_0));
|
|
insts.Add(processor.Create(OpCodes.Ldarg_0));
|
|
insts.Add(processor.Create(OpCodes.Ldfld, fieldDef));
|
|
insts.Add(processor.Create(OpCodes.Ldc_I4_1));
|
|
insts.Add(processor.Create(OpCodes.Sub));
|
|
insts.Add(processor.Create(OpCodes.Stfld, fieldDef));
|
|
|
|
return insts;
|
|
}
|
|
/// <summary>
|
|
/// Subtracts 1 from a variable.
|
|
/// </summary>
|
|
private List<Instruction> SubtractFromVariable(MethodDefinition methodDef, VariableDefinition variableDef)
|
|
{
|
|
List<Instruction> insts = new List<Instruction>();
|
|
ILProcessor processor = methodDef.Body.GetILProcessor();
|
|
|
|
// variable--;
|
|
insts.Add(processor.Create(OpCodes.Ldloc, variableDef));
|
|
insts.Add(processor.Create(OpCodes.Ldc_I4_1));
|
|
insts.Add(processor.Create(OpCodes.Sub));
|
|
insts.Add(processor.Create(OpCodes.Stloc, variableDef));
|
|
|
|
return insts;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subtracts 1 from a variable.
|
|
/// </summary>
|
|
private List<Instruction> SubtractOneVariableFromAnother(MethodDefinition methodDef, VariableDefinition srcVd, VariableDefinition modifierVd)
|
|
{
|
|
List<Instruction> insts = new List<Instruction>();
|
|
ILProcessor processor = methodDef.Body.GetILProcessor();
|
|
|
|
// variable -= v2;
|
|
insts.Add(processor.Create(OpCodes.Ldloc, srcVd));
|
|
insts.Add(processor.Create(OpCodes.Ldloc, modifierVd));
|
|
insts.Add(processor.Create(OpCodes.Sub));
|
|
insts.Add(processor.Create(OpCodes.Stloc, srcVd));
|
|
|
|
return insts;
|
|
}
|
|
#endregion
|
|
|
|
#region Server side.
|
|
/// <summary>
|
|
/// Creates replicate code for client.
|
|
/// </summary>
|
|
private void ServerCreateReplicate(MethodDefinition replicateMd, CreatedPredictionFields predictionFields)
|
|
{
|
|
ILProcessor processor = replicateMd.Body.GetILProcessor();
|
|
|
|
ParameterDefinition replicateDataPd = replicateMd.Parameters[0];
|
|
ParameterDefinition channelPd = replicateMd.Parameters[2];
|
|
TypeReference replicateDataTr = replicateDataPd.ParameterType;
|
|
|
|
GenericInstanceMethod replicateGim = base.GetClass<NetworkBehaviourHelper>().Replicate_Server_MethodRef.MakeGenericMethod(new TypeReference[] { replicateDataTr });
|
|
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ReplicateULDelegate);
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ServerReplicateDatas);
|
|
processor.Emit(OpCodes.Ldarg, channelPd);
|
|
processor.Emit(OpCodes.Call, replicateGim);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a reader for replicate data received from clients.
|
|
/// </summary>
|
|
private bool CreateReplicateReader(TypeDefinition typeDef, MethodDefinition replicateMd, CreatedPredictionFields predictionFields, out MethodDefinition result)
|
|
{
|
|
string methodName = $"{REPLICATE_READER_PREFIX}{replicateMd.Name}";
|
|
MethodDefinition createdMd = new MethodDefinition(methodName,
|
|
MethodAttributes.Private,
|
|
replicateMd.Module.TypeSystem.Void);
|
|
typeDef.Methods.Add(createdMd);
|
|
createdMd.Body.InitLocals = true;
|
|
|
|
ILProcessor processor = createdMd.Body.GetILProcessor();
|
|
|
|
GeneralHelper gh = base.GetClass<GeneralHelper>();
|
|
NetworkBehaviourHelper nbh = base.GetClass<NetworkBehaviourHelper>();
|
|
|
|
TypeReference dataTr = replicateMd.Parameters[0].ParameterType;
|
|
//Create parameters.
|
|
ParameterDefinition readerPd = gh.CreateParameter(createdMd, typeof(PooledReader));
|
|
ParameterDefinition networkConnectionPd = gh.CreateParameter(createdMd, typeof(NetworkConnection));
|
|
ParameterDefinition channelPd = gh.CreateParameter(createdMd, typeof(Channel));
|
|
|
|
MethodReference replicateReaderGim = nbh.Replicate_Reader_MethodRef.GetMethodReference(base.Session, dataTr);
|
|
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
//Reader, NetworkConnection.
|
|
processor.Emit(OpCodes.Ldarg, readerPd);
|
|
processor.Emit(OpCodes.Ldarg, networkConnectionPd);
|
|
//arrBuffer.
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ServerReplicateReaderBuffer);
|
|
//replicates.
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ServerReplicateDatas);
|
|
//Channel.
|
|
processor.Emit(OpCodes.Ldarg, channelPd);
|
|
processor.Emit(OpCodes.Call, replicateReaderGim);
|
|
//Add end of method.
|
|
processor.Emit(OpCodes.Ret);
|
|
result = createdMd;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates server side code for reconcileMd.
|
|
/// </summary>
|
|
/// <param name="reconcileMd"></param>
|
|
/// <returns></returns>
|
|
private void ServerCreateReconcile(MethodDefinition reconcileMd, CreatedPredictionFields predictionFields, ref uint rpcCount)
|
|
{
|
|
ParameterDefinition reconcileDataPd = reconcileMd.Parameters[0];
|
|
ParameterDefinition channelPd = reconcileMd.Parameters[2];
|
|
ILProcessor processor = reconcileMd.Body.GetILProcessor();
|
|
|
|
GenericInstanceMethod methodGim = base.GetClass<NetworkBehaviourHelper>().Reconcile_Server_MethodRef.MakeGenericMethod(new TypeReference[] { reconcileDataPd.ParameterType });
|
|
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldc_I4, (int)rpcCount);
|
|
processor.Emit(OpCodes.Ldarg, reconcileDataPd);
|
|
processor.Emit(OpCodes.Ldarg, channelPd);
|
|
processor.Emit(OpCodes.Call, methodGim);
|
|
|
|
rpcCount++;
|
|
}
|
|
#endregion
|
|
|
|
#region Client side.
|
|
/// <summary>
|
|
/// Creates replicate code for client.
|
|
/// </summary>
|
|
private void ClientCreateReplicate(MethodDefinition replicateMd, CreatedPredictionFields predictionFields, uint rpcCount)
|
|
{
|
|
ParameterDefinition dataPd = replicateMd.Parameters[0];
|
|
ParameterDefinition channelPd = replicateMd.Parameters[2];
|
|
TypeReference dataTr = dataPd.ParameterType;
|
|
|
|
ILProcessor processor = replicateMd.Body.GetILProcessor();
|
|
|
|
//Make method reference NB.SendReplicateRpc<dataTr>
|
|
GenericInstanceMethod replicateClientGim = base.GetClass<NetworkBehaviourHelper>().Replicate_Client_MethodRef.MakeGenericMethod(new TypeReference[] { dataTr });
|
|
processor.Emit(OpCodes.Ldarg_0);//base.
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ReplicateULDelegate);
|
|
processor.Emit(OpCodes.Ldc_I4, (int)rpcCount);
|
|
processor.Emit(OpCodes.Ldarg_0);//this.
|
|
processor.Emit(OpCodes.Ldfld, predictionFields.ClientReplicateDatas.CachedResolve(base.Session));
|
|
processor.Emit(OpCodes.Ldarg, dataPd);
|
|
processor.Emit(OpCodes.Ldarg, channelPd);
|
|
processor.Emit(OpCodes.Call, replicateClientGim);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a reader for replicate data received from clients.
|
|
/// </summary>
|
|
private bool CreateReconcileReader(TypeDefinition typeDef, MethodDefinition reconcileMd, CreatedPredictionFields predictionFields, out MethodDefinition result)
|
|
{
|
|
string methodName = $"{RECONCILE_READER_PREFIX}{reconcileMd.Name}";
|
|
MethodDefinition createdMd = new MethodDefinition(methodName,
|
|
MethodAttributes.Private,
|
|
reconcileMd.Module.TypeSystem.Void);
|
|
typeDef.Methods.Add(createdMd);
|
|
createdMd.Body.InitLocals = true;
|
|
|
|
ILProcessor processor = createdMd.Body.GetILProcessor();
|
|
|
|
GeneralHelper gh = base.GetClass<GeneralHelper>();
|
|
NetworkBehaviourHelper nbh = base.GetClass<NetworkBehaviourHelper>();
|
|
|
|
TypeReference dataTr = reconcileMd.Parameters[0].ParameterType;
|
|
//Create parameters.
|
|
ParameterDefinition readerPd = gh.CreateParameter(createdMd, typeof(PooledReader));
|
|
ParameterDefinition channelPd = gh.CreateParameter(createdMd, typeof(Channel));
|
|
|
|
MethodReference methodGim = nbh.Reconcile_Reader_MethodRef.GetMethodReference(base.Session, dataTr);
|
|
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
//Reader, data, channel.
|
|
processor.Emit(OpCodes.Ldarg, readerPd);
|
|
//Data to assign read value to.
|
|
processor.Emit(OpCodes.Ldarg_0);
|
|
processor.Emit(OpCodes.Ldflda, predictionFields.ReconcileData);
|
|
//Channel.
|
|
processor.Emit(OpCodes.Ldarg, channelPd);
|
|
processor.Emit(OpCodes.Call, methodGim);
|
|
//Add end of method.
|
|
processor.Emit(OpCodes.Ret);
|
|
|
|
result = createdMd;
|
|
return true;
|
|
}
|
|
#endregion
|
|
}
|
|
} |