quantum-space-buddies/MirrorWeaver/Weaver/Processors/NetworkBehaviourProcessor.cs
2022-02-25 23:18:46 -08:00

1013 lines
42 KiB
C#

using System.Collections.Generic;
using Mono.Cecil;
using Mono.Cecil.Cil;
namespace Mirror.Weaver
{
public enum RemoteCallType
{
Command,
ClientRpc,
TargetRpc
}
// processes SyncVars, Cmds, Rpcs, etc. of NetworkBehaviours
class NetworkBehaviourProcessor
{
AssemblyDefinition assembly;
WeaverTypes weaverTypes;
SyncVarAccessLists syncVarAccessLists;
SyncVarAttributeProcessor syncVarAttributeProcessor;
Writers writers;
Readers readers;
Logger Log;
List<FieldDefinition> syncVars = new List<FieldDefinition>();
List<FieldDefinition> syncObjects = new List<FieldDefinition>();
// <SyncVarField,NetIdField>
Dictionary<FieldDefinition, FieldDefinition> syncVarNetIds = new Dictionary<FieldDefinition, FieldDefinition>();
readonly List<CmdResult> commands = new List<CmdResult>();
readonly List<ClientRpcResult> clientRpcs = new List<ClientRpcResult>();
readonly List<MethodDefinition> targetRpcs = new List<MethodDefinition>();
readonly List<MethodDefinition> commandInvocationFuncs = new List<MethodDefinition>();
readonly List<MethodDefinition> clientRpcInvocationFuncs = new List<MethodDefinition>();
readonly List<MethodDefinition> targetRpcInvocationFuncs = new List<MethodDefinition>();
readonly TypeDefinition netBehaviourSubclass;
public struct CmdResult
{
public MethodDefinition method;
public bool requiresAuthority;
}
public struct ClientRpcResult
{
public MethodDefinition method;
public bool includeOwner;
}
public NetworkBehaviourProcessor(AssemblyDefinition assembly, WeaverTypes weaverTypes, SyncVarAccessLists syncVarAccessLists, Writers writers, Readers readers, Logger Log, TypeDefinition td)
{
this.assembly = assembly;
this.weaverTypes = weaverTypes;
this.syncVarAccessLists = syncVarAccessLists;
this.writers = writers;
this.readers = readers;
this.Log = Log;
syncVarAttributeProcessor = new SyncVarAttributeProcessor(assembly, weaverTypes, syncVarAccessLists, Log);
netBehaviourSubclass = td;
}
// return true if modified
public bool Process(ref bool WeavingFailed)
{
// only process once
if (WasProcessed(netBehaviourSubclass))
{
return false;
}
MarkAsProcessed(netBehaviourSubclass);
// deconstruct tuple and set fields
(syncVars, syncVarNetIds) = syncVarAttributeProcessor.ProcessSyncVars(netBehaviourSubclass, ref WeavingFailed);
syncObjects = SyncObjectProcessor.FindSyncObjectsFields(writers, readers, Log, netBehaviourSubclass, ref WeavingFailed);
ProcessMethods(ref WeavingFailed);
if (WeavingFailed)
{
// originally Process returned true in every case, except if already processed.
// maybe return false here in the future.
return true;
}
// inject initializations into static & instance constructor
InjectIntoStaticConstructor(ref WeavingFailed);
InjectIntoInstanceConstructor(ref WeavingFailed);
GenerateSerialization(ref WeavingFailed);
if (WeavingFailed)
{
// originally Process returned true in every case, except if already processed.
// maybe return false here in the future.
return true;
}
GenerateDeSerialization(ref WeavingFailed);
return true;
}
/*
generates code like:
if (!NetworkClient.active)
Debug.LogError((object) "Command function CmdRespawn called on server.");
which is used in InvokeCmd, InvokeRpc, etc.
*/
public static void WriteClientActiveCheck(ILProcessor worker, WeaverTypes weaverTypes, string mdName, Instruction label, string errString)
{
// client active check
worker.Emit(OpCodes.Call, weaverTypes.NetworkClientGetActive);
worker.Emit(OpCodes.Brtrue, label);
worker.Emit(OpCodes.Ldstr, $"{errString} {mdName} called on server.");
worker.Emit(OpCodes.Call, weaverTypes.logErrorReference);
worker.Emit(OpCodes.Ret);
worker.Append(label);
}
/*
generates code like:
if (!NetworkServer.active)
Debug.LogError((object) "Command CmdMsgWhisper called on client.");
*/
public static void WriteServerActiveCheck(ILProcessor worker, WeaverTypes weaverTypes, string mdName, Instruction label, string errString)
{
// server active check
worker.Emit(OpCodes.Call, weaverTypes.NetworkServerGetActive);
worker.Emit(OpCodes.Brtrue, label);
worker.Emit(OpCodes.Ldstr, $"{errString} {mdName} called on client.");
worker.Emit(OpCodes.Call, weaverTypes.logErrorReference);
worker.Emit(OpCodes.Ret);
worker.Append(label);
}
public static void WriteSetupLocals(ILProcessor worker, WeaverTypes weaverTypes)
{
worker.Body.InitLocals = true;
worker.Body.Variables.Add(new VariableDefinition(weaverTypes.Import<PooledNetworkWriter>()));
}
public static void WriteCreateWriter(ILProcessor worker, WeaverTypes weaverTypes)
{
// create writer
worker.Emit(OpCodes.Call, weaverTypes.GetPooledWriterReference);
worker.Emit(OpCodes.Stloc_0);
}
public static void WriteRecycleWriter(ILProcessor worker, WeaverTypes weaverTypes)
{
// NetworkWriterPool.Recycle(writer);
worker.Emit(OpCodes.Ldloc_0);
worker.Emit(OpCodes.Call, weaverTypes.RecycleWriterReference);
}
public static bool WriteArguments(ILProcessor worker, Writers writers, Logger Log, MethodDefinition method, RemoteCallType callType, ref bool WeavingFailed)
{
// write each argument
// example result
/*
writer.WritePackedInt32(someNumber);
writer.WriteNetworkIdentity(someTarget);
*/
bool skipFirst = callType == RemoteCallType.TargetRpc
&& TargetRpcProcessor.HasNetworkConnectionParameter(method);
// arg of calling function, arg 0 is "this" so start counting at 1
int argNum = 1;
foreach (ParameterDefinition param in method.Parameters)
{
// NetworkConnection is not sent via the NetworkWriter so skip it here
// skip first for NetworkConnection in TargetRpc
if (argNum == 1 && skipFirst)
{
argNum += 1;
continue;
}
// skip SenderConnection in Command
if (IsSenderConnection(param, callType))
{
argNum += 1;
continue;
}
MethodReference writeFunc = writers.GetWriteFunc(param.ParameterType, ref WeavingFailed);
if (writeFunc == null)
{
Log.Error($"{method.Name} has invalid parameter {param}", method);
WeavingFailed = true;
return false;
}
// use built-in writer func on writer object
// NetworkWriter object
worker.Emit(OpCodes.Ldloc_0);
// add argument to call
worker.Emit(OpCodes.Ldarg, argNum);
// call writer extension method
worker.Emit(OpCodes.Call, writeFunc);
argNum += 1;
}
return true;
}
#region mark / check type as processed
public const string ProcessedFunctionName = "MirrorProcessed";
// by adding an empty MirrorProcessed() function
public static bool WasProcessed(TypeDefinition td)
{
return td.GetMethod(ProcessedFunctionName) != null;
}
public void MarkAsProcessed(TypeDefinition td)
{
if (!WasProcessed(td))
{
MethodDefinition versionMethod = new MethodDefinition(ProcessedFunctionName, MethodAttributes.Private, weaverTypes.Import(typeof(void)));
ILProcessor worker = versionMethod.Body.GetILProcessor();
worker.Emit(OpCodes.Ret);
td.Methods.Add(versionMethod);
}
}
#endregion
// helper function to remove 'Ret' from the end of the method if 'Ret'
// is the last instruction.
// returns false if there was an issue
static bool RemoveFinalRetInstruction(MethodDefinition method)
{
// remove the return opcode from end of function. will add our own later.
if (method.Body.Instructions.Count != 0)
{
Instruction retInstr = method.Body.Instructions[method.Body.Instructions.Count - 1];
if (retInstr.OpCode == OpCodes.Ret)
{
method.Body.Instructions.RemoveAt(method.Body.Instructions.Count - 1);
return true;
}
return false;
}
// we did nothing, but there was no error.
return true;
}
// we need to inject several initializations into NetworkBehaviour cctor
void InjectIntoStaticConstructor(ref bool WeavingFailed)
{
if (commands.Count == 0 && clientRpcs.Count == 0 && targetRpcs.Count == 0)
return;
// find static constructor
MethodDefinition cctor = netBehaviourSubclass.GetMethod(".cctor");
bool cctorFound = cctor != null;
if (cctor != null)
{
// remove the return opcode from end of function. will add our own later.
if (!RemoveFinalRetInstruction(cctor))
{
Log.Error($"{netBehaviourSubclass.Name} has invalid class constructor", cctor);
WeavingFailed = true;
return;
}
}
else
{
// make one!
cctor = new MethodDefinition(".cctor", MethodAttributes.Private |
MethodAttributes.HideBySig |
MethodAttributes.SpecialName |
MethodAttributes.RTSpecialName |
MethodAttributes.Static,
weaverTypes.Import(typeof(void)));
}
ILProcessor cctorWorker = cctor.Body.GetILProcessor();
// register all commands in cctor
for (int i = 0; i < commands.Count; ++i)
{
CmdResult cmdResult = commands[i];
GenerateRegisterCommandDelegate(cctorWorker, weaverTypes.registerCommandReference, commandInvocationFuncs[i], cmdResult);
}
// register all client rpcs in cctor
for (int i = 0; i < clientRpcs.Count; ++i)
{
ClientRpcResult clientRpcResult = clientRpcs[i];
GenerateRegisterRemoteDelegate(cctorWorker, weaverTypes.registerRpcReference, clientRpcInvocationFuncs[i], clientRpcResult.method.FullName);
}
// register all target rpcs in cctor
for (int i = 0; i < targetRpcs.Count; ++i)
{
GenerateRegisterRemoteDelegate(cctorWorker, weaverTypes.registerRpcReference, targetRpcInvocationFuncs[i], targetRpcs[i].FullName);
}
// add final 'Ret' instruction to cctor
cctorWorker.Append(cctorWorker.Create(OpCodes.Ret));
if (!cctorFound)
{
netBehaviourSubclass.Methods.Add(cctor);
}
// in case class had no cctor, it might have BeforeFieldInit, so injected cctor would be called too late
netBehaviourSubclass.Attributes &= ~TypeAttributes.BeforeFieldInit;
}
// we need to inject several initializations into NetworkBehaviour ctor
void InjectIntoInstanceConstructor(ref bool WeavingFailed)
{
if (syncObjects.Count == 0)
return;
// find instance constructor
MethodDefinition ctor = netBehaviourSubclass.GetMethod(".ctor");
if (ctor == null)
{
Log.Error($"{netBehaviourSubclass.Name} has invalid constructor", netBehaviourSubclass);
WeavingFailed = true;
return;
}
// remove the return opcode from end of function. will add our own later.
if (!RemoveFinalRetInstruction(ctor))
{
Log.Error($"{netBehaviourSubclass.Name} has invalid constructor", ctor);
WeavingFailed = true;
return;
}
ILProcessor ctorWorker = ctor.Body.GetILProcessor();
// initialize all sync objects in ctor
foreach (FieldDefinition fd in syncObjects)
{
SyncObjectInitializer.GenerateSyncObjectInitializer(ctorWorker, weaverTypes, fd);
}
// add final 'Ret' instruction to ctor
ctorWorker.Append(ctorWorker.Create(OpCodes.Ret));
}
/*
// This generates code like:
NetworkBehaviour.RegisterCommandDelegate(base.GetType(), "CmdThrust", new NetworkBehaviour.CmdDelegate(ShipControl.InvokeCmdCmdThrust));
*/
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
void GenerateRegisterRemoteDelegate(ILProcessor worker, MethodReference registerMethod, MethodDefinition func, string functionFullName)
{
worker.Emit(OpCodes.Ldtoken, netBehaviourSubclass);
worker.Emit(OpCodes.Call, weaverTypes.getTypeFromHandleReference);
worker.Emit(OpCodes.Ldstr, functionFullName);
worker.Emit(OpCodes.Ldnull);
worker.Emit(OpCodes.Ldftn, func);
worker.Emit(OpCodes.Newobj, weaverTypes.RemoteCallDelegateConstructor);
//
worker.Emit(OpCodes.Call, registerMethod);
}
void GenerateRegisterCommandDelegate(ILProcessor worker, MethodReference registerMethod, MethodDefinition func, CmdResult cmdResult)
{
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
string cmdName = cmdResult.method.FullName;
bool requiresAuthority = cmdResult.requiresAuthority;
worker.Emit(OpCodes.Ldtoken, netBehaviourSubclass);
worker.Emit(OpCodes.Call, weaverTypes.getTypeFromHandleReference);
worker.Emit(OpCodes.Ldstr, cmdName);
worker.Emit(OpCodes.Ldnull);
worker.Emit(OpCodes.Ldftn, func);
worker.Emit(OpCodes.Newobj, weaverTypes.RemoteCallDelegateConstructor);
// requiresAuthority ? 1 : 0
worker.Emit(requiresAuthority ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
worker.Emit(OpCodes.Call, registerMethod);
}
void GenerateSerialization(ref bool WeavingFailed)
{
const string SerializeMethodName = "SerializeSyncVars";
if (netBehaviourSubclass.GetMethod(SerializeMethodName) != null)
return;
if (syncVars.Count == 0)
{
// no synvars, no need for custom OnSerialize
return;
}
MethodDefinition serialize = new MethodDefinition(SerializeMethodName,
MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig,
weaverTypes.Import<bool>());
serialize.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, weaverTypes.Import<NetworkWriter>()));
serialize.Parameters.Add(new ParameterDefinition("forceAll", ParameterAttributes.None, weaverTypes.Import<bool>()));
ILProcessor worker = serialize.Body.GetILProcessor();
serialize.Body.InitLocals = true;
// loc_0, this local variable is to determine if any variable was dirty
VariableDefinition dirtyLocal = new VariableDefinition(weaverTypes.Import<bool>());
serialize.Body.Variables.Add(dirtyLocal);
MethodReference baseSerialize = Resolvers.TryResolveMethodInParents(netBehaviourSubclass.BaseType, assembly, SerializeMethodName);
if (baseSerialize != null)
{
// base
worker.Emit(OpCodes.Ldarg_0);
// writer
worker.Emit(OpCodes.Ldarg_1);
// forceAll
worker.Emit(OpCodes.Ldarg_2);
worker.Emit(OpCodes.Call, baseSerialize);
// set dirtyLocal to result of base.OnSerialize()
worker.Emit(OpCodes.Stloc_0);
}
// Generates: if (forceAll);
Instruction initialStateLabel = worker.Create(OpCodes.Nop);
// forceAll
worker.Emit(OpCodes.Ldarg_2);
worker.Emit(OpCodes.Brfalse, initialStateLabel);
foreach (FieldDefinition syncVarDef in syncVars)
{
FieldReference syncVar = syncVarDef;
if (netBehaviourSubclass.HasGenericParameters)
{
syncVar = syncVarDef.MakeHostInstanceGeneric();
}
// Generates a writer call for each sync variable
// writer
worker.Emit(OpCodes.Ldarg_1);
// this
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, syncVar);
MethodReference writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed);
if (writeFunc != null)
{
worker.Emit(OpCodes.Call, writeFunc);
}
else
{
Log.Error($"{syncVar.Name} has unsupported type. Use a supported Mirror type instead", syncVar);
WeavingFailed = true;
return;
}
}
// always return true if forceAll
// Generates: return true
worker.Emit(OpCodes.Ldc_I4_1);
worker.Emit(OpCodes.Ret);
// Generates: end if (forceAll);
worker.Append(initialStateLabel);
// write dirty bits before the data fields
// Generates: writer.WritePackedUInt64 (base.get_syncVarDirtyBits ());
// writer
worker.Emit(OpCodes.Ldarg_1);
// base
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference);
MethodReference writeUint64Func = writers.GetWriteFunc(weaverTypes.Import<ulong>(), ref WeavingFailed);
worker.Emit(OpCodes.Call, writeUint64Func);
// generate a writer call for any dirty variable in this class
// start at number of syncvars in parent
int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName);
foreach (FieldDefinition syncVarDef in syncVars)
{
FieldReference syncVar = syncVarDef;
if (netBehaviourSubclass.HasGenericParameters)
{
syncVar = syncVarDef.MakeHostInstanceGeneric();
}
Instruction varLabel = worker.Create(OpCodes.Nop);
// Generates: if ((base.get_syncVarDirtyBits() & 1uL) != 0uL)
// base
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference);
// 8 bytes = long
worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit);
worker.Emit(OpCodes.And);
worker.Emit(OpCodes.Brfalse, varLabel);
// Generates a call to the writer for that field
// writer
worker.Emit(OpCodes.Ldarg_1);
// base
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldfld, syncVar);
MethodReference writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed);
if (writeFunc != null)
{
worker.Emit(OpCodes.Call, writeFunc);
}
else
{
Log.Error($"{syncVar.Name} has unsupported type. Use a supported Mirror type instead", syncVar);
WeavingFailed = true;
return;
}
// something was dirty
worker.Emit(OpCodes.Ldc_I4_1);
// set dirtyLocal to true
worker.Emit(OpCodes.Stloc_0);
worker.Append(varLabel);
dirtyBit += 1;
}
// add a log message if needed for debugging
//worker.Emit(OpCodes.Ldstr, $"Injected Serialize {netBehaviourSubclass.Name}");
//worker.Emit(OpCodes.Call, WeaverTypes.logErrorReference);
// generate: return dirtyLocal
worker.Emit(OpCodes.Ldloc_0);
worker.Emit(OpCodes.Ret);
netBehaviourSubclass.Methods.Add(serialize);
}
void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool WeavingFailed)
{
// put 'this.' onto stack for 'this.syncvar' below
worker.Append(worker.Create(OpCodes.Ldarg_0));
// push 'ref T this.field'
worker.Emit(OpCodes.Ldarg_0);
// if the netbehaviour class is generic, we need to make the field reference generic as well for correct IL
if (netBehaviourSubclass.HasGenericParameters)
{
worker.Emit(OpCodes.Ldflda, syncVar.MakeHostInstanceGeneric());
}
else
{
worker.Emit(OpCodes.Ldflda, syncVar);
}
// hook? then push 'new Action<T,T>(Hook)' onto stack
MethodDefinition hookMethod = syncVarAttributeProcessor.GetHookMethod(netBehaviourSubclass, syncVar, ref WeavingFailed);
if (hookMethod != null)
{
syncVarAttributeProcessor.GenerateNewActionFromHookMethod(syncVar, worker, hookMethod);
}
// otherwise push 'null' as hook
else
{
worker.Emit(OpCodes.Ldnull);
}
// call GeneratedSyncVarDeserialize<T>.
// special cases for GameObject/NetworkIdentity/NetworkBehaviour
// passing netId too for persistence.
if (syncVar.FieldType.Is<UnityEngine.GameObject>())
{
// reader
worker.Emit(OpCodes.Ldarg_1);
// GameObject setter needs one more parameter: netId field ref
FieldDefinition netIdField = syncVarNetIds[syncVar];
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, netIdField);
worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_GameObject);
}
else if (syncVar.FieldType.Is<NetworkIdentity>())
{
// reader
worker.Emit(OpCodes.Ldarg_1);
// NetworkIdentity deserialize needs one more parameter: netId field ref
FieldDefinition netIdField = syncVarNetIds[syncVar];
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, netIdField);
worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity);
}
// TODO this only uses the persistent netId for types DERIVED FROM NB.
// not if the type is just 'NetworkBehaviour'.
// this is what original implementation did too. fix it after.
else if (syncVar.FieldType.IsDerivedFrom<NetworkBehaviour>())
{
// reader
worker.Emit(OpCodes.Ldarg_1);
// NetworkIdentity deserialize needs one more parameter: netId field ref
// (actually its a NetworkBehaviourSyncVar type)
FieldDefinition netIdField = syncVarNetIds[syncVar];
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Ldflda, netIdField);
// make generic version of GeneratedSyncVarSetter_NetworkBehaviour<T>
MethodReference getFunc = weaverTypes.generatedSyncVarDeserialize_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, syncVar.FieldType);
worker.Emit(OpCodes.Call, getFunc);
}
else
{
// T value = reader.ReadT();
// this is still in IL because otherwise weaver generated
// readers/writers don't seem to work in tests.
// besides, this also avoids reader.Read<T> overhead.
MethodReference readFunc = readers.GetReadFunc(syncVar.FieldType, ref WeavingFailed);
if (readFunc == null)
{
Log.Error($"{syncVar.Name} has unsupported type. Use a supported Mirror type instead", syncVar);
WeavingFailed = true;
return;
}
// reader. for 'reader.Read()' below
worker.Emit(OpCodes.Ldarg_1);
// reader.Read()
worker.Emit(OpCodes.Call, readFunc);
// make generic version of GeneratedSyncVarDeserialize<T>
MethodReference generic = weaverTypes.generatedSyncVarDeserialize.MakeGeneric(assembly.MainModule, syncVar.FieldType);
worker.Emit(OpCodes.Call, generic);
}
}
void GenerateDeSerialization(ref bool WeavingFailed)
{
const string DeserializeMethodName = "DeserializeSyncVars";
if (netBehaviourSubclass.GetMethod(DeserializeMethodName) != null)
return;
if (syncVars.Count == 0)
{
// no synvars, no need for custom OnDeserialize
return;
}
MethodDefinition serialize = new MethodDefinition(DeserializeMethodName,
MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig,
weaverTypes.Import(typeof(void)));
serialize.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, weaverTypes.Import<NetworkReader>()));
serialize.Parameters.Add(new ParameterDefinition("initialState", ParameterAttributes.None, weaverTypes.Import<bool>()));
ILProcessor serWorker = serialize.Body.GetILProcessor();
// setup local for dirty bits
serialize.Body.InitLocals = true;
VariableDefinition dirtyBitsLocal = new VariableDefinition(weaverTypes.Import<long>());
serialize.Body.Variables.Add(dirtyBitsLocal);
MethodReference baseDeserialize = Resolvers.TryResolveMethodInParents(netBehaviourSubclass.BaseType, assembly, DeserializeMethodName);
if (baseDeserialize != null)
{
// base
serWorker.Append(serWorker.Create(OpCodes.Ldarg_0));
// reader
serWorker.Append(serWorker.Create(OpCodes.Ldarg_1));
// initialState
serWorker.Append(serWorker.Create(OpCodes.Ldarg_2));
serWorker.Append(serWorker.Create(OpCodes.Call, baseDeserialize));
}
// Generates: if (initialState);
Instruction initialStateLabel = serWorker.Create(OpCodes.Nop);
serWorker.Append(serWorker.Create(OpCodes.Ldarg_2));
serWorker.Append(serWorker.Create(OpCodes.Brfalse, initialStateLabel));
foreach (FieldDefinition syncVar in syncVars)
{
DeserializeField(syncVar, serWorker, ref WeavingFailed);
}
serWorker.Append(serWorker.Create(OpCodes.Ret));
// Generates: end if (initialState);
serWorker.Append(initialStateLabel);
// get dirty bits
serWorker.Append(serWorker.Create(OpCodes.Ldarg_1));
serWorker.Append(serWorker.Create(OpCodes.Call, readers.GetReadFunc(weaverTypes.Import<ulong>(), ref WeavingFailed)));
serWorker.Append(serWorker.Create(OpCodes.Stloc_0));
// conditionally read each syncvar
// start at number of syncvars in parent
int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName);
foreach (FieldDefinition syncVar in syncVars)
{
Instruction varLabel = serWorker.Create(OpCodes.Nop);
// check if dirty bit is set
serWorker.Append(serWorker.Create(OpCodes.Ldloc_0));
serWorker.Append(serWorker.Create(OpCodes.Ldc_I8, 1L << dirtyBit));
serWorker.Append(serWorker.Create(OpCodes.And));
serWorker.Append(serWorker.Create(OpCodes.Brfalse, varLabel));
DeserializeField(syncVar, serWorker, ref WeavingFailed);
serWorker.Append(varLabel);
dirtyBit += 1;
}
// add a log message if needed for debugging
//serWorker.Append(serWorker.Create(OpCodes.Ldstr, $"Injected Deserialize {netBehaviourSubclass.Name}"));
//serWorker.Append(serWorker.Create(OpCodes.Call, WeaverTypes.logErrorReference));
serWorker.Append(serWorker.Create(OpCodes.Ret));
netBehaviourSubclass.Methods.Add(serialize);
}
public static bool ReadArguments(MethodDefinition method, Readers readers, Logger Log, ILProcessor worker, RemoteCallType callType, ref bool WeavingFailed)
{
// read each argument
// example result
/*
CallCmdDoSomething(reader.ReadPackedInt32(), reader.ReadNetworkIdentity());
*/
bool skipFirst = callType == RemoteCallType.TargetRpc
&& TargetRpcProcessor.HasNetworkConnectionParameter(method);
// arg of calling function, arg 0 is "this" so start counting at 1
int argNum = 1;
foreach (ParameterDefinition param in method.Parameters)
{
// NetworkConnection is not sent via the NetworkWriter so skip it here
// skip first for NetworkConnection in TargetRpc
if (argNum == 1 && skipFirst)
{
argNum += 1;
continue;
}
// skip SenderConnection in Command
if (IsSenderConnection(param, callType))
{
argNum += 1;
continue;
}
MethodReference readFunc = readers.GetReadFunc(param.ParameterType, ref WeavingFailed);
if (readFunc == null)
{
Log.Error($"{method.Name} has invalid parameter {param}. Unsupported type {param.ParameterType}, use a supported Mirror type instead", method);
WeavingFailed = true;
return false;
}
worker.Emit(OpCodes.Ldarg_1);
worker.Emit(OpCodes.Call, readFunc);
// conversion.. is this needed?
if (param.ParameterType.Is<float>())
{
worker.Emit(OpCodes.Conv_R4);
}
else if (param.ParameterType.Is<double>())
{
worker.Emit(OpCodes.Conv_R8);
}
}
return true;
}
public static void AddInvokeParameters(WeaverTypes weaverTypes, ICollection<ParameterDefinition> collection)
{
collection.Add(new ParameterDefinition("obj", ParameterAttributes.None, weaverTypes.Import<NetworkBehaviour>()));
collection.Add(new ParameterDefinition("reader", ParameterAttributes.None, weaverTypes.Import<NetworkReader>()));
// senderConnection is only used for commands but NetworkBehaviour.CmdDelegate is used for all remote calls
collection.Add(new ParameterDefinition("senderConnection", ParameterAttributes.None, weaverTypes.Import<NetworkConnectionToClient>()));
}
// check if a Command/TargetRpc/Rpc function & parameters are valid for weaving
public bool ValidateRemoteCallAndParameters(MethodDefinition method, RemoteCallType callType, ref bool WeavingFailed)
{
if (method.IsStatic)
{
Log.Error($"{method.Name} must not be static", method);
WeavingFailed = true;
return false;
}
return ValidateFunction(method, ref WeavingFailed) &&
ValidateParameters(method, callType, ref WeavingFailed);
}
// check if a Command/TargetRpc/Rpc function is valid for weaving
bool ValidateFunction(MethodReference md, ref bool WeavingFailed)
{
if (md.ReturnType.Is<System.Collections.IEnumerator>())
{
Log.Error($"{md.Name} cannot be a coroutine", md);
WeavingFailed = true;
return false;
}
if (!md.ReturnType.Is(typeof(void)))
{
Log.Error($"{md.Name} cannot return a value. Make it void instead", md);
WeavingFailed = true;
return false;
}
if (md.HasGenericParameters)
{
Log.Error($"{md.Name} cannot have generic parameters", md);
WeavingFailed = true;
return false;
}
return true;
}
// check if all Command/TargetRpc/Rpc function's parameters are valid for weaving
bool ValidateParameters(MethodReference method, RemoteCallType callType, ref bool WeavingFailed)
{
for (int i = 0; i < method.Parameters.Count; ++i)
{
ParameterDefinition param = method.Parameters[i];
if (!ValidateParameter(method, param, callType, i == 0, ref WeavingFailed))
{
return false;
}
}
return true;
}
// validate parameters for a remote function call like Rpc/Cmd
bool ValidateParameter(MethodReference method, ParameterDefinition param, RemoteCallType callType, bool firstParam, ref bool WeavingFailed)
{
// need to check this before any type lookups since those will fail since generic types don't resolve
if (param.ParameterType.IsGenericParameter)
{
Log.Error($"{method.Name} cannot have generic parameters", method);
WeavingFailed = true;
return false;
}
bool isNetworkConnection = param.ParameterType.Is<NetworkConnection>();
bool isSenderConnection = IsSenderConnection(param, callType);
if (param.IsOut)
{
Log.Error($"{method.Name} cannot have out parameters", method);
WeavingFailed = true;
return false;
}
// if not SenderConnection And not TargetRpc NetworkConnection first param
if (!isSenderConnection && isNetworkConnection && !(callType == RemoteCallType.TargetRpc && firstParam))
{
if (callType == RemoteCallType.Command)
{
Log.Error($"{method.Name} has invalid parameter {param}, Cannot pass NetworkConnections. Instead use 'NetworkConnectionToClient conn = null' to get the sender's connection on the server", method);
}
else
{
Log.Error($"{method.Name} has invalid parameter {param}. Cannot pass NetworkConnections", method);
}
WeavingFailed = true;
return false;
}
// sender connection can be optional
if (param.IsOptional && !isSenderConnection)
{
Log.Error($"{method.Name} cannot have optional parameters", method);
WeavingFailed = true;
return false;
}
return true;
}
public static bool IsSenderConnection(ParameterDefinition param, RemoteCallType callType)
{
if (callType != RemoteCallType.Command)
{
return false;
}
TypeReference type = param.ParameterType;
return type.Is<NetworkConnectionToClient>()
|| type.Resolve().IsDerivedFrom<NetworkConnectionToClient>();
}
void ProcessMethods(ref bool WeavingFailed)
{
HashSet<string> names = new HashSet<string>();
// copy the list of methods because we will be adding methods in the loop
List<MethodDefinition> methods = new List<MethodDefinition>(netBehaviourSubclass.Methods);
// find command and RPC functions
foreach (MethodDefinition md in methods)
{
foreach (CustomAttribute ca in md.CustomAttributes)
{
if (ca.AttributeType.Is<CommandAttribute>())
{
ProcessCommand(names, md, ca, ref WeavingFailed);
break;
}
if (ca.AttributeType.Is<TargetRpcAttribute>())
{
ProcessTargetRpc(names, md, ca, ref WeavingFailed);
break;
}
if (ca.AttributeType.Is<ClientRpcAttribute>())
{
ProcessClientRpc(names, md, ca, ref WeavingFailed);
break;
}
}
}
}
void ProcessClientRpc(HashSet<string> names, MethodDefinition md, CustomAttribute clientRpcAttr, ref bool WeavingFailed)
{
if (md.IsAbstract)
{
Log.Error("Abstract ClientRpc are currently not supported, use virtual method instead", md);
WeavingFailed = true;
return;
}
if (!ValidateRemoteCallAndParameters(md, RemoteCallType.ClientRpc, ref WeavingFailed))
{
return;
}
bool includeOwner = clientRpcAttr.GetField("includeOwner", true);
names.Add(md.Name);
clientRpcs.Add(new ClientRpcResult
{
method = md,
includeOwner = includeOwner
});
MethodDefinition rpcCallFunc = RpcProcessor.ProcessRpcCall(weaverTypes, writers, Log, netBehaviourSubclass, md, clientRpcAttr, ref WeavingFailed);
// need null check here because ProcessRpcCall returns null if it can't write all the args
if (rpcCallFunc == null) { return; }
MethodDefinition rpcFunc = RpcProcessor.ProcessRpcInvoke(weaverTypes, writers, readers, Log, netBehaviourSubclass, md, rpcCallFunc, ref WeavingFailed);
if (rpcFunc != null)
{
clientRpcInvocationFuncs.Add(rpcFunc);
}
}
void ProcessTargetRpc(HashSet<string> names, MethodDefinition md, CustomAttribute targetRpcAttr, ref bool WeavingFailed)
{
if (md.IsAbstract)
{
Log.Error("Abstract TargetRpc are currently not supported, use virtual method instead", md);
WeavingFailed = true;
return;
}
if (!ValidateRemoteCallAndParameters(md, RemoteCallType.TargetRpc, ref WeavingFailed))
return;
names.Add(md.Name);
targetRpcs.Add(md);
MethodDefinition rpcCallFunc = TargetRpcProcessor.ProcessTargetRpcCall(weaverTypes, writers, Log, netBehaviourSubclass, md, targetRpcAttr, ref WeavingFailed);
MethodDefinition rpcFunc = TargetRpcProcessor.ProcessTargetRpcInvoke(weaverTypes, readers, Log, netBehaviourSubclass, md, rpcCallFunc, ref WeavingFailed);
if (rpcFunc != null)
{
targetRpcInvocationFuncs.Add(rpcFunc);
}
}
void ProcessCommand(HashSet<string> names, MethodDefinition md, CustomAttribute commandAttr, ref bool WeavingFailed)
{
if (md.IsAbstract)
{
Log.Error("Abstract Commands are currently not supported, use virtual method instead", md);
WeavingFailed = true;
return;
}
if (!ValidateRemoteCallAndParameters(md, RemoteCallType.Command, ref WeavingFailed))
return;
bool requiresAuthority = commandAttr.GetField("requiresAuthority", true);
names.Add(md.Name);
commands.Add(new CmdResult
{
method = md,
requiresAuthority = requiresAuthority
});
MethodDefinition cmdCallFunc = CommandProcessor.ProcessCommandCall(weaverTypes, writers, Log, netBehaviourSubclass, md, commandAttr, ref WeavingFailed);
MethodDefinition cmdFunc = CommandProcessor.ProcessCommandInvoke(weaverTypes, readers, Log, netBehaviourSubclass, md, cmdCallFunc, ref WeavingFailed);
if (cmdFunc != null)
{
commandInvocationFuncs.Add(cmdFunc);
}
}
}
}