// [SyncVar] int health; // is replaced with: // public int Networkhealth { get; set; } properties. // this class processes all access to 'health' and replaces it with 'Networkhealth' using System; using Mono.CecilX; using Mono.CecilX.Cil; namespace Mirror.Weaver { public static class SyncVarAttributeAccessReplacer { // process the module public static void Process(Logger Log, ModuleDefinition moduleDef, SyncVarAccessLists syncVarAccessLists) { DateTime startTime = DateTime.Now; // process all classes in this module foreach (TypeDefinition td in moduleDef.Types) { if (td.IsClass) { ProcessClass(Log, syncVarAccessLists, td); } } Console.WriteLine($" ProcessSitesModule {moduleDef.Name} elapsed time:{(DateTime.Now - startTime)}"); } static void ProcessClass(Logger Log, SyncVarAccessLists syncVarAccessLists, TypeDefinition td) { //Console.WriteLine($" ProcessClass {td}"); // process all methods in this class foreach (MethodDefinition md in td.Methods) { ProcessMethod(Log, syncVarAccessLists, md); } // processes all nested classes in this class recursively foreach (TypeDefinition nested in td.NestedTypes) { ProcessClass(Log, syncVarAccessLists, nested); } } static void ProcessMethod(Logger Log, SyncVarAccessLists syncVarAccessLists, MethodDefinition md) { // process all references to replaced members with properties //Log.Warning($" ProcessSiteMethod {md}"); // skip static constructor, "MirrorProcessed", "InvokeUserCode_" if (md.Name == ".cctor" || md.Name == NetworkBehaviourProcessor.ProcessedFunctionName || md.Name.StartsWith(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix)) return; // skip abstract if (md.IsAbstract) { return; } // go through all instructions of this method if (md.Body != null && md.Body.Instructions != null) { for (int i = 0; i < md.Body.Instructions.Count;) { Instruction instr = md.Body.Instructions[i]; i += ProcessInstruction(Log, syncVarAccessLists, md, instr, i); } } } static int ProcessInstruction(Logger Log, SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction instr, int iCount) { // stfld (sets value of a field)? if (instr.OpCode == OpCodes.Stfld) { // operand is a FieldDefinition in the same assembly? if (instr.Operand is FieldDefinition opFieldst) { ProcessSetInstruction(syncVarAccessLists, md, instr, opFieldst); } // operand is a FieldReference in another assembly? // this is not supported just yet. // compilation error is better than silently failing SyncVar serialization at runtime. // https://github.com/MirrorNetworking/Mirror/issues/3525 else if (instr.Operand is FieldReference opFieldstRef) { // resolve it from the other assembly FieldDefinition field = opFieldstRef.Resolve(); // [SyncVar]? if (field.HasCustomAttribute()) { // ILPostProcessor would need to Process() the assembly's // references before processing this one. // we can not control the order. // instead, Log an error to suggest adding a SetSyncVar(value) function. // this is a very easy solution for a very rare edge case. Log.Error($"'[SyncVar] {opFieldstRef.DeclaringType.Name}.{opFieldstRef.Name}' in '{field.Module.Name}' is modified by '{md.FullName}' in '{md.Module.Name}'. Modifying a [SyncVar] from another assembly is not supported. Please add a: 'public void Set{opFieldstRef.Name}(value) {{ this.{opFieldstRef.Name} = value; }}' method in '{opFieldstRef.DeclaringType.Name}' and call this function from '{md.FullName}' instead."); } } } // ldfld (load value of a field)? if (instr.OpCode == OpCodes.Ldfld) { // operand is a FieldDefinition in the same assembly? if (instr.Operand is FieldDefinition opFieldld) { // this instruction gets the value of a field. cache the field reference. ProcessGetInstruction(syncVarAccessLists, md, instr, opFieldld); } } // ldflda (load field address aka reference) if (instr.OpCode == OpCodes.Ldflda) { // operand is a FieldDefinition in the same assembly? if (instr.Operand is FieldDefinition opFieldlda) { // watch out for initobj instruction // see https://github.com/vis2k/Mirror/issues/696 return ProcessLoadAddressInstruction(syncVarAccessLists, md, instr, opFieldlda, iCount); } } // we processed one instruction (instr) return 1; } // replaces syncvar write access with the NetworkXYZ.set property calls static void ProcessSetInstruction(SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction i, FieldDefinition opField) { // don't replace property call sites in constructors if (md.Name == ".ctor") return; // does it set a field that we replaced? if (syncVarAccessLists.replacementSetterProperties.TryGetValue(opField, out MethodDefinition replacement)) { //replace with property //Log.Warning($" replacing {md.Name}:{i}", opField); i.OpCode = OpCodes.Call; i.Operand = replacement; //Log.Warning($" replaced {md.Name}:{i}", opField); } } // replaces syncvar read access with the NetworkXYZ.get property calls static void ProcessGetInstruction(SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction i, FieldDefinition opField) { // don't replace property call sites in constructors if (md.Name == ".ctor") return; // does it set a field that we replaced? if (syncVarAccessLists.replacementGetterProperties.TryGetValue(opField, out MethodDefinition replacement)) { //replace with property //Log.Warning($" replacing {md.Name}:{i}"); i.OpCode = OpCodes.Call; i.Operand = replacement; //Log.Warning($" replaced {md.Name}:{i}"); } } static int ProcessLoadAddressInstruction(SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction instr, FieldDefinition opField, int iCount) { // don't replace property call sites in constructors if (md.Name == ".ctor") return 1; // does it set a field that we replaced? if (syncVarAccessLists.replacementSetterProperties.TryGetValue(opField, out MethodDefinition replacement)) { // we have a replacement for this property // is the next instruction a initobj? Instruction nextInstr = md.Body.Instructions[iCount + 1]; if (nextInstr.OpCode == OpCodes.Initobj) { // we need to replace this code with: // var tmp = new MyStruct(); // this.set_Networkxxxx(tmp); ILProcessor worker = md.Body.GetILProcessor(); VariableDefinition tmpVariable = new VariableDefinition(opField.FieldType); md.Body.Variables.Add(tmpVariable); worker.InsertBefore(instr, worker.Create(OpCodes.Ldloca, tmpVariable)); worker.InsertBefore(instr, worker.Create(OpCodes.Initobj, opField.FieldType)); worker.InsertBefore(instr, worker.Create(OpCodes.Ldloc, tmpVariable)); worker.InsertBefore(instr, worker.Create(OpCodes.Call, replacement)); worker.Remove(instr); worker.Remove(nextInstr); return 4; } } return 1; } } }