using FishNet.Broadcast; using FishNet.CodeGenerating.Extension; using FishNet.CodeGenerating.Helping; using FishNet.CodeGenerating.Helping.Extension; using FishNet.CodeGenerating.Processing; using FishNet.CodeGenerating.Processing.Rpc; using FishNet.Configuring; using FishNet.Serializing.Helping; using MonoFN.Cecil; using MonoFN.Cecil.Cil; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Unity.CompilationPipeline.Common.ILPostProcessing; namespace FishNet.CodeGenerating.ILCore { public class FishNetILPP : ILPostProcessor { #region Const. internal const string RUNTIME_ASSEMBLY_NAME = "FishNet.Runtime"; #endregion public override bool WillProcess(ICompiledAssembly compiledAssembly) { if (compiledAssembly.Name.StartsWith("Unity.")) return false; if (compiledAssembly.Name.StartsWith("UnityEngine.")) return false; if (compiledAssembly.Name.StartsWith("UnityEditor.")) return false; if (compiledAssembly.Name.Contains("Editor")) return false; /* This line contradicts the one below where referencesFishNet * becomes true if the assembly is FishNetAssembly. This is here * intentionally to stop codegen from running on the runtime * fishnet assembly, but the option below is for debugging. I would * comment out this check if I wanted to compile fishnet runtime. */ //if (CODEGEN_THIS_NAMESPACE.Length == 0) //{ // if (compiledAssembly.Name == RUNTIME_ASSEMBLY_NAME) // return false; //} bool referencesFishNet = FishNetILPP.IsFishNetAssembly(compiledAssembly) || compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == RUNTIME_ASSEMBLY_NAME); return referencesFishNet; } public override ILPostProcessor GetInstance() => this; public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) { AssemblyDefinition assemblyDef = ILCoreHelper.GetAssemblyDefinition(compiledAssembly); if (assemblyDef == null) return null; //Check WillProcess again; somehow certain editor scripts skip the WillProcess check. if (!WillProcess(compiledAssembly)) return null; CodegenSession session = new CodegenSession(); if (!session.Initialize(assemblyDef.MainModule)) return null; bool modified = false; bool fnAssembly = IsFishNetAssembly(compiledAssembly); if (fnAssembly) modified |= ModifyMakePublicMethods(session); /* If one or more scripts use RPCs but don't inherit NetworkBehaviours * then don't bother processing the rest. */ if (session.GetClass().NonNetworkBehaviourHasInvalidAttributes(session.Module.Types)) return new ILPostProcessResult(null, session.Diagnostics); modified |= session.GetClass().Process(); modified |= session.GetClass().Process(); modified |= CreateDeclaredSerializerDelegates(session); modified |= CreateDeclaredSerializers(session); modified |= CreateDeclaredComparerDelegates(session); modified |= CreateIBroadcast(session); modified |= CreateQOLAttributes(session); modified |= CreateNetworkBehaviours(session); modified |= CreateGenericReadWriteDelegates(session); if (fnAssembly) { AssemblyNameReference anr = session.Module.AssemblyReferences.FirstOrDefault(x => x.FullName == session.Module.Assembly.FullName); if (anr != null) session.Module.AssemblyReferences.Remove(anr); } /* If there are warnings about SyncVars being in different assemblies. * This is awful ... codegen would need to be reworked to save * syncvars across all assemblies so that scripts referencing them from * another assembly can have it's instructions changed. This however is an immense * amount of work so it will have to be put on hold, for... a long.. long while. */ if (session.DifferentAssemblySyncVars.Count > 0) { StringBuilder sb = new StringBuilder(); sb.AppendLine($"Assembly {session.Module.Name} has inherited access to SyncVars in different assemblies. When accessing SyncVars across assemblies be sure to use Get/Set methods withinin the inherited assembly script to change SyncVars. Accessible fields are:"); foreach (FieldDefinition item in session.DifferentAssemblySyncVars) sb.AppendLine($"Field {item.Name} within {item.DeclaringType.FullName} in assembly {item.Module.Name}."); session.LogWarning("v------- IMPORTANT -------v"); session.LogWarning(sb.ToString()); session.DifferentAssemblySyncVars.Clear(); } //session.LogWarning($"Assembly {compiledAssembly.Name} took {stopwatch.ElapsedMilliseconds}."); if (!modified) { return null; } else { MemoryStream pe = new MemoryStream(); MemoryStream pdb = new MemoryStream(); WriterParameters writerParameters = new WriterParameters { SymbolWriterProvider = new PortablePdbWriterProvider(), SymbolStream = pdb, WriteSymbols = true }; assemblyDef.Write(pe, writerParameters); return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), session.Diagnostics); } } /// /// Makees methods public scope which use CodegenMakePublic attribute. /// /// private bool ModifyMakePublicMethods(CodegenSession session) { string makePublicTypeFullName = typeof(CodegenMakePublicAttribute).FullName; foreach (TypeDefinition td in session.Module.Types) { foreach (MethodDefinition md in td.Methods) { foreach (CustomAttribute ca in md.CustomAttributes) { if (ca.AttributeType.FullName == makePublicTypeFullName) { md.Attributes &= ~MethodAttributes.Assembly; md.Attributes |= MethodAttributes.Public; } } } } //There is always at least one modified. return true; } /// /// Creates delegates for user declared serializers. /// internal bool CreateDeclaredSerializerDelegates(CodegenSession session) { bool modified = false; TypeAttributes readWriteExtensionTypeAttr = (TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract); List allTypeDefs = session.Module.Types.ToList(); foreach (TypeDefinition td in allTypeDefs) { if (session.GetClass().IgnoreTypeDefinition(td)) continue; if (td.Attributes.HasFlag(readWriteExtensionTypeAttr)) modified |= session.GetClass().CreateSerializerDelegates(td, true); } return modified; } /// /// Creates serializers for custom types within user declared serializers. /// private bool CreateDeclaredSerializers(CodegenSession session) { bool modified = false; TypeAttributes readWriteExtensionTypeAttr = (TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract); List allTypeDefs = session.Module.Types.ToList(); foreach (TypeDefinition td in allTypeDefs) { if (session.GetClass().IgnoreTypeDefinition(td)) continue; if (td.Attributes.HasFlag(readWriteExtensionTypeAttr)) modified |= session.GetClass().CreateSerializers(td); } return modified; } /// /// Creates delegates for user declared comparers. /// internal bool CreateDeclaredComparerDelegates(CodegenSession session) { bool modified = false; List allTypeDefs = session.Module.Types.ToList(); foreach (TypeDefinition td in allTypeDefs) { if (session.GetClass().IgnoreTypeDefinition(td)) continue; modified |= session.GetClass().CreateComparerDelegates(td); } return modified; } /// /// Creaters serializers and calls for IBroadcast. /// /// /// private bool CreateIBroadcast(CodegenSession session) { bool modified = false; string networkBehaviourFullName = session.GetClass().FullName; HashSet typeDefs = new HashSet(); foreach (TypeDefinition td in session.Module.Types) { TypeDefinition climbTd = td; do { //Reached NetworkBehaviour class. if (climbTd.FullName == networkBehaviourFullName) break; ///* Check initial class as well all types within // * the class. Then check all of it's base classes. */ if (climbTd.ImplementsInterface()) typeDefs.Add(climbTd); //7ms //Add nested. Only going to go a single layer deep. foreach (TypeDefinition nestedTypeDef in td.NestedTypes) { if (nestedTypeDef.ImplementsInterface()) typeDefs.Add(nestedTypeDef); } //0ms climbTd = climbTd.GetNextBaseTypeDefinition(session); //this + name check 40ms } while (climbTd != null); } //Create reader/writers for found typeDefs. foreach (TypeDefinition td in typeDefs) { TypeReference typeRef = session.ImportReference(td); bool canSerialize = session.GetClass().HasSerializerAndDeserializer(typeRef, true); if (!canSerialize) session.LogError($"Broadcast {td.Name} does not support serialization. Use a supported type or create a custom serializer."); else modified = true; } return modified; } /// /// Handles QOLAttributes such as [Server]. /// /// private bool CreateQOLAttributes(CodegenSession session) { bool modified = false; bool codeStripping = false; List allTypeDefs = session.Module.Types.ToList(); /* First pass, potentially only pass. * If code stripping them this will be run again. The first iteration * is to ensure things are removed in the proper order. */ foreach (TypeDefinition td in allTypeDefs) { if (session.GetClass().IgnoreTypeDefinition(td)) continue; modified |= session.GetClass().Process(td, codeStripping); } return modified; } /// /// Creates NetworkBehaviour changes. /// /// /// private bool CreateNetworkBehaviours(CodegenSession session) { bool modified = false; //Get all network behaviours to process. List networkBehaviourTypeDefs = session.Module.Types .Where(td => td.IsSubclassOf(session, session.GetClass().FullName)) .ToList(); //Moment a NetworkBehaviour exist the assembly is considered modified. if (networkBehaviourTypeDefs.Count > 0) modified = true; /* Remove types which are inherited. This gets the child most networkbehaviours. * Since processing iterates all parent classes there's no reason to include them */ RemoveInheritedTypeDefinitions(networkBehaviourTypeDefs); //Set how many rpcs are in children classes for each typedef. Dictionary inheritedRpcCounts = new Dictionary(); SetChildRpcCounts(inheritedRpcCounts, networkBehaviourTypeDefs); //Set how many synctypes are in children classes for each typedef. Dictionary inheritedSyncTypeCounts = new Dictionary(); SetChildSyncTypeCounts(inheritedSyncTypeCounts, networkBehaviourTypeDefs); /* This holds all sync types created, synclist, dictionary, var * and so on. This data is used after all syncvars are made so * other methods can look for references to created synctypes and * replace accessors accordingly. */ List<(SyncType, ProcessedSync)> allProcessedSyncs = new List<(SyncType, ProcessedSync)>(); HashSet allProcessedCallbacks = new HashSet(); List processedClasses = new List(); foreach (TypeDefinition typeDef in networkBehaviourTypeDefs) { session.ImportReference(typeDef); //Synctypes processed for this nb and it's inherited classes. List<(SyncType, ProcessedSync)> processedSyncs = new List<(SyncType, ProcessedSync)>(); session.GetClass().Process(typeDef, processedSyncs, inheritedSyncTypeCounts, inheritedRpcCounts); //Add to all processed. allProcessedSyncs.AddRange(processedSyncs); } /* Must run through all scripts should user change syncvar * from outside the networkbehaviour. */ if (allProcessedSyncs.Count > 0) { foreach (TypeDefinition td in session.Module.Types) { session.GetClass().ReplaceGetSets(td, allProcessedSyncs); session.GetClass().RedirectBaseCalls(); } } /* Removes typedefinitions which are inherited by * another within tds. For example, if the collection * td contains A, B, C and our structure is * A : B : C then B and C will be removed from the collection * Since they are both inherited by A. */ void RemoveInheritedTypeDefinitions(List tds) { HashSet inheritedTds = new HashSet(); /* Remove any networkbehaviour typedefs which are inherited by * another networkbehaviour typedef. When a networkbehaviour typedef * is processed so are all of the inherited types. */ for (int i = 0; i < tds.Count; i++) { /* Iterates all base types and * adds them to inheritedTds so long * as the base type is not a NetworkBehaviour. */ TypeDefinition copyTd = tds[i].GetNextBaseTypeDefinition(session); while (copyTd != null) { //Class is NB. if (copyTd.FullName == session.GetClass().FullName) break; inheritedTds.Add(copyTd); copyTd = copyTd.GetNextBaseTypeDefinition(session); } } //Remove all inherited types. foreach (TypeDefinition item in inheritedTds) tds.Remove(item); } /* Sets how many Rpcs are within the children * of each typedefinition. EG: if our structure is * A : B : C, with the following RPC counts... * A 3 * B 1 * C 2 * then B child rpc counts will be 3, and C will be 4. */ void SetChildRpcCounts(Dictionary typeDefCounts, List tds) { foreach (TypeDefinition typeDef in tds) { //Number of RPCs found while climbing typeDef. uint childCount = 0; TypeDefinition copyTd = typeDef; do { //How many RPCs are in copyTd. uint copyCount = session.GetClass().GetRpcCount(copyTd); /* If not found it this is the first time being * processed. When this occurs set the value * to 0. It will be overwritten below if baseCount * is higher. */ uint previousCopyChildCount = 0; if (!typeDefCounts.TryGetValue(copyTd, out previousCopyChildCount)) typeDefCounts[copyTd] = 0; /* If baseCount is higher then replace count for copyTd. * This can occur when a class is inherited by several types * and the first processed type might only have 1 rpc, while * the next has 2. This could be better optimized but to keep * the code easier to read, it will stay like this. */ if (childCount > previousCopyChildCount) typeDefCounts[copyTd] = childCount; //Increase baseCount with RPCs found here. childCount += copyCount; copyTd = copyTd.GetNextBaseClassToProcess(session); } while (copyTd != null); } } /* This performs the same functionality as SetChildRpcCounts * but for SyncTypes. */ void SetChildSyncTypeCounts(Dictionary typeDefCounts, List tds) { foreach (TypeDefinition typeDef in tds) { //Number of RPCs found while climbing typeDef. uint childCount = 0; TypeDefinition copyTd = typeDef; /* Iterate up to the parent script and then reverse * the order. This is so that the topmost is 0 * and each inerhiting script adds onto that. * Setting child types this way makes it so parent * types don't need to have their synctype/rpc counts * rebuilt when scripts are later to be found * inheriting from them. */ List reversedTypeDefs = new List(); do { reversedTypeDefs.Add(copyTd); copyTd = copyTd.GetNextBaseClassToProcess(session); } while (copyTd != null); reversedTypeDefs.Reverse(); foreach (TypeDefinition td in reversedTypeDefs) { //How many RPCs are in copyTd. uint copyCount = session.GetClass().GetSyncTypeCount(td); /* If not found it this is the first time being * processed. When this occurs set the value * to 0. It will be overwritten below if baseCount * is higher. */ uint previousCopyChildCount = 0; if (!typeDefCounts.TryGetValue(td, out previousCopyChildCount)) typeDefCounts[td] = 0; /* If baseCount is higher then replace count for copyTd. * This can occur when a class is inherited by several types * and the first processed type might only have 1 rpc, while * the next has 2. This could be better optimized but to keep * the code easier to read, it will stay like this. */ if (childCount > previousCopyChildCount) typeDefCounts[td] = childCount; //Increase baseCount with RPCs found here. childCount += copyCount; } } } return modified; } /// /// Creates generic delegates for all read and write methods. /// /// /// private bool CreateGenericReadWriteDelegates(CodegenSession session) { session.GetClass().CreateStaticMethodDelegates(); session.GetClass().CreateStaticMethodDelegates(); return true; } internal static bool IsFishNetAssembly(ICompiledAssembly assembly) => (assembly.Name == FishNetILPP.RUNTIME_ASSEMBLY_NAME); internal static bool IsFishNetAssembly(CodegenSession session) => (session.Module.Assembly.Name.Name == FishNetILPP.RUNTIME_ASSEMBLY_NAME); internal static bool IsFishNetAssembly(ModuleDefinition moduleDef) => (moduleDef.Assembly.Name.Name == FishNetILPP.RUNTIME_ASSEMBLY_NAME); } }