using System.Collections.Generic; using System.Linq; using Mono.CecilX; using Mono.CecilX.Cil; namespace Mirror.Weaver { /// /// Processes [SyncVar] in NetworkBehaviour /// public static class SyncVarProcessor { // ulong = 64 bytes const int SyncVarLimit = 64; static string HookParameterMessage(string hookName, TypeReference ValueType) => string.Format("void {0}({1} oldValue, {1} newValue)", hookName, ValueType); // Get hook method if any public static MethodDefinition GetHookMethod(TypeDefinition td, FieldDefinition syncVar) { CustomAttribute syncVarAttr = syncVar.GetCustomAttribute(); if (syncVarAttr == null) return null; string hookFunctionName = syncVarAttr.GetField("hook", null); if (hookFunctionName == null) return null; return FindHookMethod(td, syncVar, hookFunctionName); } static MethodDefinition FindHookMethod(TypeDefinition td, FieldDefinition syncVar, string hookFunctionName) { List methods = td.GetMethods(hookFunctionName); List methodsWith2Param = new List(methods.Where(m => m.Parameters.Count == 2)); if (methodsWith2Param.Count == 0) { Weaver.Error($"Could not find hook for '{syncVar.Name}', hook name '{hookFunctionName}'. " + $"Method signature should be {HookParameterMessage(hookFunctionName, syncVar.FieldType)}", syncVar); return null; } foreach (MethodDefinition method in methodsWith2Param) { if (MatchesParameters(syncVar, method)) { return method; } } Weaver.Error($"Wrong type for Parameter in hook for '{syncVar.Name}', hook name '{hookFunctionName}'. " + $"Method signature should be {HookParameterMessage(hookFunctionName, syncVar.FieldType)}", syncVar); return null; } static bool MatchesParameters(FieldDefinition syncVar, MethodDefinition method) { // matches void onValueChange(T oldValue, T newValue) return method.Parameters[0].ParameterType.FullName == syncVar.FieldType.FullName && method.Parameters[1].ParameterType.FullName == syncVar.FieldType.FullName; } public static MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string originalName, FieldDefinition netFieldId) { //Create the get method MethodDefinition get = new MethodDefinition( "get_Network" + originalName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, fd.FieldType); ILProcessor worker = get.Body.GetILProcessor(); // [SyncVar] GameObject? if (fd.FieldType.Is()) { // return this.GetSyncVarGameObject(ref field, uint netId); // this. worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, netFieldId); worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, fd); worker.Emit(OpCodes.Call, WeaverTypes.getSyncVarGameObjectReference); worker.Emit(OpCodes.Ret); } // [SyncVar] NetworkIdentity? else if (fd.FieldType.Is()) { // return this.GetSyncVarNetworkIdentity(ref field, uint netId); // this. worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, netFieldId); worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, fd); worker.Emit(OpCodes.Call, WeaverTypes.getSyncVarNetworkIdentityReference); worker.Emit(OpCodes.Ret); } else if (fd.FieldType.IsDerivedFrom()) { // return this.GetSyncVarNetworkBehaviour(ref field, uint netId); // this. worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, netFieldId); worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, fd); MethodReference getFunc = WeaverTypes.getSyncVarNetworkBehaviourReference.MakeGeneric(fd.FieldType); worker.Emit(OpCodes.Call, getFunc); worker.Emit(OpCodes.Ret); } // [SyncVar] int, string, etc. else { worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, fd); worker.Emit(OpCodes.Ret); } get.Body.Variables.Add(new VariableDefinition(fd.FieldType)); get.Body.InitLocals = true; get.SemanticsAttributes = MethodSemanticsAttributes.Getter; return get; } public static MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition fd, string originalName, long dirtyBit, FieldDefinition netFieldId) { //Create the set method MethodDefinition set = new MethodDefinition("set_Network" + originalName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, WeaverTypes.Import(typeof(void))); ILProcessor worker = set.Body.GetILProcessor(); // if (!SyncVarEqual(value, ref playerData)) Instruction endOfMethod = worker.Create(OpCodes.Nop); // this worker.Emit(OpCodes.Ldarg_0); // new value to set worker.Emit(OpCodes.Ldarg_1); // reference to field to set // make generic version of SetSyncVar with field type if (fd.FieldType.Is()) { // reference to netId Field to set worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, netFieldId); worker.Emit(OpCodes.Call, WeaverTypes.syncVarGameObjectEqualReference); } else if (fd.FieldType.Is()) { // reference to netId Field to set worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, netFieldId); worker.Emit(OpCodes.Call, WeaverTypes.syncVarNetworkIdentityEqualReference); } else if (fd.FieldType.IsDerivedFrom()) { // reference to netId Field to set worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, netFieldId); MethodReference getFunc = WeaverTypes.syncVarNetworkBehaviourEqualReference.MakeGeneric(fd.FieldType); worker.Emit(OpCodes.Call, getFunc); } else { worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, fd); GenericInstanceMethod syncVarEqualGm = new GenericInstanceMethod(WeaverTypes.syncVarEqualReference); syncVarEqualGm.GenericArguments.Add(fd.FieldType); worker.Emit(OpCodes.Call, syncVarEqualGm); } worker.Emit(OpCodes.Brtrue, endOfMethod); // T oldValue = value; // TODO for GO/NI we need to backup the netId don't we? VariableDefinition oldValue = new VariableDefinition(fd.FieldType); set.Body.Variables.Add(oldValue); worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, fd); worker.Emit(OpCodes.Stloc, oldValue); // this worker.Emit(OpCodes.Ldarg_0); // new value to set worker.Emit(OpCodes.Ldarg_1); // reference to field to set worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, fd); // dirty bit // 8 byte integer aka long worker.Emit(OpCodes.Ldc_I8, dirtyBit); if (fd.FieldType.Is()) { // reference to netId Field to set worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, netFieldId); worker.Emit(OpCodes.Call, WeaverTypes.setSyncVarGameObjectReference); } else if (fd.FieldType.Is()) { // reference to netId Field to set worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, netFieldId); worker.Emit(OpCodes.Call, WeaverTypes.setSyncVarNetworkIdentityReference); } else if (fd.FieldType.IsDerivedFrom()) { // reference to netId Field to set worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, netFieldId); MethodReference getFunc = WeaverTypes.setSyncVarNetworkBehaviourReference.MakeGeneric(fd.FieldType); worker.Emit(OpCodes.Call, getFunc); } else { // make generic version of SetSyncVar with field type GenericInstanceMethod gm = new GenericInstanceMethod(WeaverTypes.setSyncVarReference); gm.GenericArguments.Add(fd.FieldType); // invoke SetSyncVar worker.Emit(OpCodes.Call, gm); } MethodDefinition hookMethod = GetHookMethod(td, fd); if (hookMethod != null) { //if (NetworkServer.localClientActive && !getSyncVarHookGuard(dirtyBit)) Instruction label = worker.Create(OpCodes.Nop); worker.Emit(OpCodes.Call, WeaverTypes.NetworkServerGetLocalClientActive); worker.Emit(OpCodes.Brfalse, label); worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldc_I8, dirtyBit); worker.Emit(OpCodes.Call, WeaverTypes.getSyncVarHookGuard); worker.Emit(OpCodes.Brtrue, label); // setSyncVarHookGuard(dirtyBit, true); worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldc_I8, dirtyBit); worker.Emit(OpCodes.Ldc_I4_1); worker.Emit(OpCodes.Call, WeaverTypes.setSyncVarHookGuard); // call hook (oldValue, newValue) // Generates: OnValueChanged(oldValue, value); WriteCallHookMethodUsingArgument(worker, hookMethod, oldValue); // setSyncVarHookGuard(dirtyBit, false); worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldc_I8, dirtyBit); worker.Emit(OpCodes.Ldc_I4_0); worker.Emit(OpCodes.Call, WeaverTypes.setSyncVarHookGuard); worker.Append(label); } worker.Append(endOfMethod); worker.Emit(OpCodes.Ret); set.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.In, fd.FieldType)); set.SemanticsAttributes = MethodSemanticsAttributes.Setter; return set; } public static void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary syncVarNetIds, long dirtyBit) { string originalName = fd.Name; Weaver.DLog(td, "Sync Var " + fd.Name + " " + fd.FieldType); // GameObject/NetworkIdentity SyncVars have a new field for netId FieldDefinition netIdField = null; // NetworkBehaviour has different field type than other NetworkIdentityFields if (fd.FieldType.IsDerivedFrom()) { netIdField = new FieldDefinition("___" + fd.Name + "NetId", FieldAttributes.Private, WeaverTypes.Import()); syncVarNetIds[fd] = netIdField; } else if (fd.FieldType.IsNetworkIdentityField()) { netIdField = new FieldDefinition("___" + fd.Name + "NetId", FieldAttributes.Private, WeaverTypes.Import()); syncVarNetIds[fd] = netIdField; } MethodDefinition get = GenerateSyncVarGetter(fd, originalName, netIdField); MethodDefinition set = GenerateSyncVarSetter(td, fd, originalName, dirtyBit, netIdField); //NOTE: is property even needed? Could just use a setter function? //create the property PropertyDefinition propertyDefinition = new PropertyDefinition("Network" + originalName, PropertyAttributes.None, fd.FieldType) { GetMethod = get, SetMethod = set }; //add the methods and property to the type. td.Methods.Add(get); td.Methods.Add(set); td.Properties.Add(propertyDefinition); Weaver.WeaveLists.replacementSetterProperties[fd] = set; // replace getter field if GameObject/NetworkIdentity so it uses // netId instead // -> only for GameObjects, otherwise an int syncvar's getter would // end up in recursion. if (fd.FieldType.IsNetworkIdentityField()) { Weaver.WeaveLists.replacementGetterProperties[fd] = get; } } public static (List syncVars, Dictionary syncVarNetIds) ProcessSyncVars(TypeDefinition td) { List syncVars = new List(); Dictionary syncVarNetIds = new Dictionary(); // the mapping of dirtybits to sync-vars is implicit in the order of the fields here. this order is recorded in m_replacementProperties. // start assigning syncvars at the place the base class stopped, if any int dirtyBitCounter = Weaver.WeaveLists.GetSyncVarStart(td.BaseType.FullName); // find syncvars foreach (FieldDefinition fd in td.Fields) { if (fd.HasCustomAttribute()) { if ((fd.Attributes & FieldAttributes.Static) != 0) { Weaver.Error($"{fd.Name} cannot be static", fd); continue; } if (fd.FieldType.IsArray) { Weaver.Error($"{fd.Name} has invalid type. Use SyncLists instead of arrays", fd); continue; } if (SyncObjectInitializer.ImplementsSyncObject(fd.FieldType)) { Weaver.Warning($"{fd.Name} has [SyncVar] attribute. SyncLists should not be marked with SyncVar", fd); } else { syncVars.Add(fd); ProcessSyncVar(td, fd, syncVarNetIds, 1L << dirtyBitCounter); dirtyBitCounter += 1; if (dirtyBitCounter == SyncVarLimit) { Weaver.Error($"{td.Name} has too many SyncVars. Consider refactoring your class into multiple components", td); continue; } } } } // add all the new SyncVar __netId fields foreach (FieldDefinition fd in syncVarNetIds.Values) { td.Fields.Add(fd); } Weaver.WeaveLists.SetNumSyncVars(td.FullName, syncVars.Count); return (syncVars, syncVarNetIds); } public static void WriteCallHookMethodUsingArgument(ILProcessor worker, MethodDefinition hookMethod, VariableDefinition oldValue) { WriteCallHookMethod(worker, hookMethod, oldValue, null); } public static void WriteCallHookMethodUsingField(ILProcessor worker, MethodDefinition hookMethod, VariableDefinition oldValue, FieldDefinition newValue) { if (newValue == null) { Weaver.Error("NewValue field was null when writing SyncVar hook"); } WriteCallHookMethod(worker, hookMethod, oldValue, newValue); } static void WriteCallHookMethod(ILProcessor worker, MethodDefinition hookMethod, VariableDefinition oldValue, FieldDefinition newValue) { WriteStartFunctionCall(); // write args WriteOldValue(); WriteNewValue(); WriteEndFunctionCall(); // *** Local functions used to write OpCodes *** // Local functions have access to function variables, no need to pass in args void WriteOldValue() { worker.Emit(OpCodes.Ldloc, oldValue); } void WriteNewValue() { // write arg1 or this.field if (newValue == null) { worker.Emit(OpCodes.Ldarg_1); } else { // this. worker.Emit(OpCodes.Ldarg_0); // syncvar.get worker.Emit(OpCodes.Ldfld, newValue); } } // Writes this before method if it is not static void WriteStartFunctionCall() { // don't add this (Ldarg_0) if method is static if (!hookMethod.IsStatic) { // this before method call // e.g. this.onValueChanged worker.Emit(OpCodes.Ldarg_0); } } // Calls method void WriteEndFunctionCall() { // only use Callvirt when not static OpCode opcode = hookMethod.IsStatic ? OpCodes.Call : OpCodes.Callvirt; worker.Emit(opcode, hookMethod); } } } }