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(Session)) { LogError($"{copyTypeDef.FullName} contains nested NetworkBehaviours. These are not supported."); return modified; } } typeDefs.Add(copyTypeDef); } copyTypeDef = TypeDefinitionExtensionsOld.GetNextBaseClassToProcess(copyTypeDef, 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 (GetClass().Process(td)) { _usesPredictionTypeDefs.Add(td); modified = true; } // 25ms /* RPCs. */ modified |= 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 |= 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. 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}_{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 = GetClass(); RpcProcessor rpcProcessor = GetClass(); foreach (TypeDefinition typeDef in typeDefs) { // Inherits, don't need to check. if (typeDef.InheritsNetworkBehaviour(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)) { 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)) { 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(Session)) return; // Base Awake. MethodReference baseAwakeMr = td.GetMethodReferenceInBase(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. 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 = GetClass().GetTypeReference(typeof(bool)); FieldReference fr = typeDef.GetOrCreateFieldReference(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(Session, mdName); // Not found in base. if (baseMr == null) continue; MethodDefinition baseMd = baseMr.CachedResolve(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(Session, methodName); ILProcessor processor = awakeMethodDef.Body.GetILProcessor(); processor.Emit(OpCodes.Ldarg_0); processor.Emit(networkInitMr.GetCallOpCode(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(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(Session, name); processor.Emit(OpCodes.Ldarg_0); processor.Emit(initIfDisabledMr.GetCallOpCode(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) 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(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) { 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); } } }