using FishNet.CodeGenerating.Extension; using FishNet.CodeGenerating.Helping; using FishNet.CodeGenerating.Helping.Extension; using FishNet.CodeGenerating.Processing.Rpc; using FishNet.Configuring; using FishNet.Object; using MonoFN.Cecil; using MonoFN.Cecil.Cil; using MonoFN.Collections.Generic; using System.Collections.Generic; using System.Linq; namespace FishNet.CodeGenerating.Processing { internal class NetworkBehaviourProcessor : CodegenBase { #region Private. /// /// Classes which have been processed for all NetworkBehaviour features. /// private HashSet _processedClasses = new(); #endregion #region Const. internal const string NETWORKINITIALIZE_EARLY_INTERNAL_NAME = "NetworkInitialize___Early"; internal const string NETWORKINITIALIZE_LATE_INTERNAL_NAME = "NetworkInitialize___Late"; private readonly string[] _networkInitializeMethodNames = new string[] { NETWORKINITIALIZE_EARLY_INTERNAL_NAME, NETWORKINITIALIZE_LATE_INTERNAL_NAME }; #endregion internal bool ProcessLocal(TypeDefinition typeDef) { bool modified = false; TypeDefinition copyTypeDef = typeDef; //TypeDefs which are using prediction. List _usesPredictionTypeDefs = new(); //Make collection of NBs to processor. List typeDefs = new(); do { if (!HasClassBeenProcessed(copyTypeDef)) { //Disallow nested network behaviours. ICollection nestedTds = copyTypeDef.NestedTypes; foreach (TypeDefinition item in nestedTds) { if (item.InheritsNetworkBehaviour(base.Session)) { base.LogError($"{copyTypeDef.FullName} contains nested NetworkBehaviours. These are not supported."); return modified; } } typeDefs.Add(copyTypeDef); } copyTypeDef = TypeDefinitionExtensionsOld.GetNextBaseClassToProcess(copyTypeDef, base.Session); } while (copyTypeDef != null); /* Reverse type definitions so that the parent * is first. This counts indexes up as we go further * down the children. By doing so we do not have to * rebuild rpc or synctype indexes when a parent is inherited * multiple times. EG: with this solution if parent had 1 sync type * and childA had 2 the parent would be index 0, and childA would have 1 and 2. * But also, if childB inherited childA it would have 3+. * * Going in reverse also gaurantees the awake method will already be created * or modified in any class a child inherits. This lets us call it appropriately * as well error if the awake does not exist, such as could not be created. */ typeDefs.Reverse(); foreach (TypeDefinition td in typeDefs) { /* Create NetworkInitialize before-hand so the other procesors * can use it. */ MethodDefinition networkInitializeIfDisabledMd; CreateNetworkInitializeMethods(td, out networkInitializeIfDisabledMd); CallNetworkInitialize(networkInitializeIfDisabledMd); /* Prediction. */ /* Run prediction first since prediction will modify * user data passed into prediction methods. Because of this * other RPCs should use the modified version and reader/writers * made for prediction. */ if (base.GetClass().Process(td)) { _usesPredictionTypeDefs.Add(td); modified = true; } //25ms /* RPCs. */ modified |= base.GetClass().ProcessLocal(td); //30ms /* //perf rpcCounts can be optimized by having different counts * for target, observers, server, replicate, and reoncile rpcs. Since * each registers to their own delegates this is possible. */ /* SyncTypes. */ modified |= base.GetClass().ProcessLocal(td); //Call base networkinitialize early/late. CallBaseOnNetworkInitializeMethods(td); //Add networkinitialize executed check to early/late. AddNetworkInitializeExecutedChecks(td); //Copy user logic from awake into a new method. CopyAwakeUserLogic(td); /* Create awake method or if already exist make * it public virtual. */ if (!ModifyAwakeMethod(td, out bool awakeCreated)) { //This is a hard fail and will break the solution so throw here. base.LogError($"{td.FullName} has an Awake method which could not be modified, or could not be found. This often occurs when a child class is in an assembly different from the parent, and the parent does not implement Awake. To resolve this make an Awake in {td.Name} public virtual."); return modified; } //Calls NetworkInitializeEarly from awake. CallMethodFromAwake(td, NETWORKINITIALIZE_EARLY_INTERNAL_NAME); //Only call base if awake was created. Otherwise let the users implementation handle base calling. if (awakeCreated) CallBaseAwake(td); //Call logic user may have put in awake. CallAwakeUserLogic(td); //NetworkInitializeLate from awake. CallMethodFromAwake(td, NETWORKINITIALIZE_LATE_INTERNAL_NAME); //Since awake methods are erased ret has to be added at the end. AddReturnToAwake(td); //70ms _processedClasses.Add(td); } /* If here then all inerited classes for firstTypeDef have * been processed. */ return modified; } /// /// Gets the name to use for user awake logic method. /// internal string GetAwakeUserLogicMethodDefinition(TypeDefinition td) => $"Awake_UserLogic_{td.FullName}_{base.Module.Name}"; /// /// Returns if a class has been processed. /// /// /// private bool HasClassBeenProcessed(TypeDefinition typeDef) { return _processedClasses.Contains(typeDef); } /// /// Returns if any typeDefs have attributes which are not allowed to be used outside NetworkBehaviour. /// /// /// internal bool NonNetworkBehaviourHasInvalidAttributes(Collection typeDefs) { SyncTypeProcessor stProcessor = base.GetClass(); RpcProcessor rpcProcessor = base.GetClass(); foreach (TypeDefinition typeDef in typeDefs) { //Inherits, don't need to check. if (typeDef.InheritsNetworkBehaviour(base.Session)) continue; //Check each method for attribute. foreach (MethodDefinition md in typeDef.Methods) { //Has RPC attribute but doesn't inherit from NB. if (rpcProcessor.Attributes.HasRpcAttributes(md)) { base.LogError($"{typeDef.FullName} has one or more RPC attributes but does not inherit from NetworkBehaviour."); return true; } } //Check fields for attribute. foreach (FieldDefinition fd in typeDef.Fields) { if (stProcessor.IsSyncType(fd)) { base.LogError($"{typeDef.FullName} implements one or more SyncTypes but does not inherit from NetworkBehaviour."); return true; } } } //Fallthrough / pass. return false; } /// /// Calls the next awake method if the nested awake was created by codegen. /// /// private void CallBaseAwake(TypeDefinition td) { /* If base is not a class which can be processed then there * is no need to continue. */ if (!td.CanProcessBaseType(base.Session)) return; //Base Awake. MethodReference baseAwakeMr = td.GetMethodReferenceInBase(base.Session, NetworkBehaviourHelper.AWAKE_METHOD_NAME); //This Awake. MethodDefinition tdAwakeMd = td.GetMethod(NetworkBehaviourHelper.AWAKE_METHOD_NAME); ILProcessor processor = tdAwakeMd.Body.GetILProcessor(); processor.Emit(OpCodes.Ldarg_0); //base. processor.Emit(OpCodes.Call, baseAwakeMr); } /// /// Calls the next awake method if the nested awake was created by codegen. /// /// private void CallAwakeUserLogic(TypeDefinition td) { //UserLogic. MethodDefinition userLogicMd = td.GetMethod(GetAwakeUserLogicMethodDefinition(td)); /* Userlogic may be null if Awake was created. * If so, there's no need to proceed. */ if (userLogicMd == null) return; //This Awake. MethodDefinition awakeMd = td.GetMethod(NetworkBehaviourHelper.AWAKE_METHOD_NAME); //Call logic. base.GetClass().CallCopiedMethod(awakeMd, userLogicMd); } /// /// Adds a check to NetworkInitialize to see if it has already run. /// /// private void AddNetworkInitializeExecutedChecks(TypeDefinition typeDef) { AddCheck(NETWORKINITIALIZE_EARLY_INTERNAL_NAME); AddCheck(NETWORKINITIALIZE_LATE_INTERNAL_NAME); void AddCheck(string methodName) { string fieldName = $"{methodName}{typeDef.FullName}{typeDef.Module.Name}_Excuted"; MethodDefinition md = typeDef.GetMethod(methodName); if (md == null) return; TypeReference boolTr = base.GetClass().GetTypeReference(typeof(bool)); FieldReference fr = typeDef.GetOrCreateFieldReference(base.Session, fieldName, FieldAttributes.Private, boolTr, out bool created); if (created) { List insts = new(); ILProcessor processor = md.Body.GetILProcessor(); //Add check if already called. //if (alreadyInitialized) return; Instruction skipFirstRetInst = processor.Create(OpCodes.Nop); insts.Add(processor.Create(OpCodes.Ldarg_0)); insts.Add(processor.Create(OpCodes.Ldfld, fr)); insts.Add(processor.Create(OpCodes.Brfalse_S, skipFirstRetInst)); insts.Add(processor.Create(OpCodes.Ret)); insts.Add(skipFirstRetInst); //Set field to true. insts.Add(processor.Create(OpCodes.Ldarg_0)); insts.Add(processor.Create(OpCodes.Ldc_I4_1)); insts.Add(processor.Create(OpCodes.Stfld, fr)); processor.InsertFirst(insts); } } } /// /// Calls base for NetworkInitializeEarly/Late on a TypeDefinition. /// private void CallBaseOnNetworkInitializeMethods(TypeDefinition typeDef) { //If base is null cannot call base. if (typeDef.BaseType == null) return; foreach (string mdName in _networkInitializeMethodNames) { MethodReference baseMr = typeDef.GetMethodReferenceInBase(base.Session, mdName); //Not found in base. if (baseMr == null) continue; MethodDefinition baseMd = baseMr.CachedResolve(base.Session); MethodDefinition thisMd = typeDef.GetMethod(mdName); ILProcessor processor = thisMd.Body.GetILProcessor(); bool alreadyHasBaseCall = false; //Check if already calls baseAwake. foreach (Instruction item in thisMd.Body.Instructions) { //If a call or call virt. Although, callvirt should never occur. if (item.OpCode == OpCodes.Call || item.OpCode == OpCodes.Callvirt) { if (item.Operand != null && item.Operand.GetType().Name == nameof(MethodDefinition)) { MethodDefinition md = (MethodDefinition)item.Operand; if (md == baseMd) { alreadyHasBaseCall = true; break; } } } } if (!alreadyHasBaseCall) { //Create instructions for base call. List instructions = new() { processor.Create(OpCodes.Ldarg_0), //this. processor.Create(OpCodes.Call, baseMr) }; processor.InsertFirst(instructions); } } } /// /// Adds returns awake method definitions within awakeDatas. /// private void AddReturnToAwake(TypeDefinition td) { //This Awake. MethodDefinition awakeMd = td.GetMethod(NetworkBehaviourHelper.AWAKE_METHOD_NAME); ILProcessor processor = awakeMd.Body.GetILProcessor(); //If no instructions or the last instruction isnt ret. if (processor.Body.Instructions.Count == 0 || processor.Body.Instructions[processor.Body.Instructions.Count - 1].OpCode != OpCodes.Ret) { processor.Emit(OpCodes.Ret); } } /// /// Calls a method by name from awake. /// private void CallMethodFromAwake(TypeDefinition typeDef, string methodName) { //Will never be null because we added it previously. MethodDefinition awakeMethodDef = typeDef.GetMethod(NetworkBehaviourHelper.AWAKE_METHOD_NAME); MethodReference networkInitMr = typeDef.GetMethodReference(base.Session, methodName); ILProcessor processor = awakeMethodDef.Body.GetILProcessor(); processor.Emit(OpCodes.Ldarg_0); processor.Emit(networkInitMr.GetCallOpCode(base.Session), networkInitMr); } /// /// Creates an 'NetworkInitialize' method which is called by the childmost class to initialize scripts on Awake. /// private void CreateNetworkInitializeMethods(TypeDefinition typeDef, out MethodDefinition networkInitializeIfDisabledMd) { CreateMethod(NETWORKINITIALIZE_EARLY_INTERNAL_NAME); CreateMethod(NETWORKINITIALIZE_LATE_INTERNAL_NAME); networkInitializeIfDisabledMd = CreateMethod(nameof(NetworkBehaviour.NetworkInitializeIfDisabled)); MethodDefinition CreateMethod(string name, MethodDefinition copied = null) { bool created; MethodDefinition md = typeDef.GetOrCreateMethodDefinition(base.Session, name, MethodDefinitionExtensions.PUBLIC_VIRTUAL_ATTRIBUTES, typeDef.Module.TypeSystem.Void, out created); if (created) { //Emit ret into new method. ILProcessor processor = md.Body.GetILProcessor(); //End of method return. processor.Emit(OpCodes.Ret); } return md; } } /// /// Creates an 'NetworkInitialize' method which is called by the childmost class to initialize scripts on Awake. /// private void CallNetworkInitialize(MethodDefinition networkIntializeMd) { ILProcessor processor = networkIntializeMd.Body.GetILProcessor(); networkIntializeMd.Body.Instructions.Clear(); CallMethod(NETWORKINITIALIZE_EARLY_INTERNAL_NAME); CallMethod(NETWORKINITIALIZE_LATE_INTERNAL_NAME); processor.Emit(OpCodes.Ret); void CallMethod(string name) { MethodReference initIfDisabledMr = networkIntializeMd.DeclaringType.GetMethodReference(base.Session, name); processor.Emit(OpCodes.Ldarg_0); processor.Emit(initIfDisabledMr.GetCallOpCode(base.Session), initIfDisabledMr); } } /// /// Copies logic from users Awake if present, to a new method. /// private void CopyAwakeUserLogic(TypeDefinition typeDef) { MethodDefinition awakeMd = typeDef.GetMethod(NetworkBehaviourHelper.AWAKE_METHOD_NAME); //If found copy. if (awakeMd != null) base.GetClass().CopyIntoNewMethod(awakeMd, GetAwakeUserLogicMethodDefinition(typeDef), out _); } /// /// Erases content in awake if it already exist, otherwise makes a new Awake. /// Makes Awake public and virtual. /// /// True if successful. private bool ModifyAwakeMethod(TypeDefinition typeDef, out bool created) { MethodDefinition awakeMd = typeDef.GetOrCreateMethodDefinition(base.Session, NetworkBehaviourHelper.AWAKE_METHOD_NAME, MethodDefinitionExtensions.PUBLIC_VIRTUAL_ATTRIBUTES, typeDef.Module.TypeSystem.Void, out created); //Awake is found. Check for invalid return type. if (!created) { if (awakeMd.ReturnType != typeDef.Module.TypeSystem.Void) { base.LogError($"IEnumerator Awake methods are not supported within NetworkBehaviours."); return false; } //Make public if good. awakeMd.SetPublicAttributes(); } //Already was made. else { ILProcessor processor = awakeMd.Body.GetILProcessor(); processor.Emit(OpCodes.Ret); } //Clear original awake. awakeMd.Body.Instructions.Clear(); return true; } /// /// Makes all Awake methods within typeDef and base classes public and virtual. /// /// internal void CreateFirstNetworkInitializeCall(TypeDefinition typeDef, MethodDefinition firstUserAwakeMethodDef, MethodDefinition firstNetworkInitializeMethodDef) { ILProcessor processor; //Get awake for current method. MethodDefinition thisAwakeMethodDef = typeDef.GetMethod(NetworkBehaviourHelper.AWAKE_METHOD_NAME); bool created = false; //If no awake then make one. if (thisAwakeMethodDef == null) { created = true; thisAwakeMethodDef = new(NetworkBehaviourHelper.AWAKE_METHOD_NAME, MethodDefinitionExtensions.PUBLIC_VIRTUAL_ATTRIBUTES, typeDef.Module.TypeSystem.Void); thisAwakeMethodDef.Body.InitLocals = true; typeDef.Methods.Add(thisAwakeMethodDef); processor = thisAwakeMethodDef.Body.GetILProcessor(); processor.Emit(OpCodes.Ret); } //MethodRefs for networkinitialize and awake. MethodReference networkInitializeMethodRef = typeDef.Module.ImportReference(firstNetworkInitializeMethodDef); processor = thisAwakeMethodDef.Body.GetILProcessor(); //Create instructions for base call. List instructions = new() { processor.Create(OpCodes.Ldarg_0), //this. processor.Create(OpCodes.Call, networkInitializeMethodRef) }; /* If awake was created then make a call to the users * first awake. There's no reason to do this if awake * already existed because the user would have control * over making that call. */ if (created && firstUserAwakeMethodDef != null) { MethodReference baseAwakeMethodRef = typeDef.Module.ImportReference(firstUserAwakeMethodDef); instructions.Add(processor.Create(OpCodes.Ldarg_0)); //this. instructions.Add(processor.Create(OpCodes.Call, baseAwakeMethodRef)); } processor.InsertFirst(instructions); } } }