#if UNITY_EDITOR || LiveScriptReload_Enabled using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using HarmonyLib; using ImmersiveVRTools.Runtime.Common; using ImmersiveVRTools.Runtime.Common.Extensions; using ImmersiveVrToolsCommon.Runtime.Logging; using UnityEngine; namespace FastScriptReload.Runtime { [PreventHotReload] #if UNITY_EDITOR [UnityEditor.InitializeOnLoad] #endif public class AssemblyChangesLoader: IAssemblyChangesLoader { const BindingFlags ALL_BINDING_FLAGS = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.FlattenHierarchy; const BindingFlags ALL_DECLARED_METHODS_BINDING_FLAGS = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly; //only declared methods can be redirected, otherwise it'll result in hang public const string ClassnamePatchedPostfix = "__Patched_"; public const string ON_HOT_RELOAD_METHOD_NAME = "OnScriptHotReload"; public const string ON_HOT_RELOAD_NO_INSTANCE_STATIC_METHOD_NAME = "OnScriptHotReloadNoInstance"; private static readonly List ExcludeMethodsDefinedOnTypes = new List { typeof(MonoBehaviour), typeof(Behaviour), typeof(UnityEngine.Object), typeof(Component), typeof(System.Object) }; //TODO: move out and possibly define a way to exclude all non-client created code? as this will crash editor private static AssemblyChangesLoader _instance; public static AssemblyChangesLoader Instance => _instance ?? (_instance = new AssemblyChangesLoader()); private Dictionary _existingTypeToRedirectedType = new Dictionary(); public void DynamicallyUpdateMethodsForCreatedAssembly(Assembly dynamicallyLoadedAssemblyWithUpdates, AssemblyChangesLoaderEditorOptionsNeededInBuild editorOptions) { try { var sw = new Stopwatch(); sw.Start(); foreach (var createdType in dynamicallyLoadedAssemblyWithUpdates.GetTypes() .Where(t => (t.IsClass && !typeof(Delegate).IsAssignableFrom(t)) //don't redirect delegates // || (t.IsValueType && !t.IsPrimitive) //struct check, ensure works ) ) { if (createdType.GetCustomAttribute() != null) { //TODO: ideally type would be excluded from compilation not just from detour LoggerScoped.Log($"Type: {createdType.Name} marked as {nameof(PreventHotReload)} - ignoring change."); continue; } var createdTypeNameWithoutPatchedPostfix = RemoveClassPostfix(createdType.FullName); if (ProjectTypeCache.AllTypesInNonDynamicGeneratedAssemblies.TryGetValue(createdTypeNameWithoutPatchedPostfix, out var matchingTypeInExistingAssemblies)) { _existingTypeToRedirectedType[matchingTypeInExistingAssemblies] = createdType; if (!editorOptions.IsDidFieldsOrPropertyCountChangedCheckDisabled && !editorOptions.EnableExperimentalAddedFieldsSupport && DidFieldsOrPropertyCountChanged(createdType, matchingTypeInExistingAssemblies)) { continue; } var allDeclaredMethodsInExistingType = matchingTypeInExistingAssemblies.GetMethods(ALL_DECLARED_METHODS_BINDING_FLAGS) .Where(m => !ExcludeMethodsDefinedOnTypes.Contains(m.DeclaringType)) .ToList(); foreach (var createdTypeMethodToUpdate in createdType.GetMethods(ALL_DECLARED_METHODS_BINDING_FLAGS) .Where(m => !ExcludeMethodsDefinedOnTypes.Contains(m.DeclaringType))) { var createdTypeMethodToUpdateFullDescriptionWithoutPatchedClassPostfix = RemoveClassPostfix(createdTypeMethodToUpdate.FullDescription()); var matchingMethodInExistingType = allDeclaredMethodsInExistingType.SingleOrDefault(m => m.FullDescription() == createdTypeMethodToUpdateFullDescriptionWithoutPatchedClassPostfix); if (matchingMethodInExistingType != null) { if (matchingMethodInExistingType.IsGenericMethod) { LoggerScoped.LogWarning($"Method: '{matchingMethodInExistingType.FullDescription()}' is generic. Hot-Reload for generic methods is not supported yet, you won't see changes for that method."); continue; } if (matchingMethodInExistingType.DeclaringType != null && matchingMethodInExistingType.DeclaringType.IsGenericType) { LoggerScoped.LogWarning($"Type for method: '{matchingMethodInExistingType.FullDescription()}' is generic. Hot-Reload for generic types is not supported yet, you won't see changes for that type."); continue; } LoggerScoped.LogDebug($"Trying to detour method, from: '{matchingMethodInExistingType.FullDescription()}' to: '{createdTypeMethodToUpdate.FullDescription()}'"); DetourCrashHandler.LogDetour(matchingMethodInExistingType.ResolveFullName()); Memory.DetourMethod(matchingMethodInExistingType, createdTypeMethodToUpdate); } else { LoggerScoped.LogWarning($"Method: {createdTypeMethodToUpdate.FullDescription()} does not exist in initially compiled type: {matchingTypeInExistingAssemblies.FullName}. " + $"Adding new methods at runtime is not fully supported. \r\n" + $"It'll only work new method is only used by declaring class (eg private method)\r\n" + $"Make sure to add method before initial compilation."); } } FindAndExecuteStaticOnScriptHotReloadNoInstance(createdType); FindAndExecuteOnScriptHotReload(matchingTypeInExistingAssemblies); } else { LoggerScoped.LogWarning($"FSR: Unable to find existing type for: '{createdType.FullName}', this is not an issue if you added new type. If it's an existing type please do a full domain-reload - one of optimisations is to cache existing types for later lookup on first call."); FindAndExecuteStaticOnScriptHotReloadNoInstance(createdType); FindAndExecuteOnScriptHotReload(createdType); } } LoggerScoped.Log($"Hot-reload completed (took {sw.ElapsedMilliseconds}ms)"); } finally { DetourCrashHandler.ClearDetourLog(); } } public Type GetRedirectedType(Type forExistingType) { return _existingTypeToRedirectedType[forExistingType]; } private static bool DidFieldsOrPropertyCountChanged(Type createdType, Type matchingTypeInExistingAssemblies) { var createdTypeFieldAndProperties = createdType.GetFields(ALL_BINDING_FLAGS).Concat(createdType.GetProperties(ALL_BINDING_FLAGS).Cast()).ToList(); var matchingTypeFieldAndProperties = matchingTypeInExistingAssemblies.GetFields(ALL_BINDING_FLAGS).Concat(matchingTypeInExistingAssemblies.GetProperties(ALL_BINDING_FLAGS).Cast()).ToList(); if (createdTypeFieldAndProperties.Count != matchingTypeFieldAndProperties.Count) { var addedMemberNames = createdTypeFieldAndProperties.Select(m => m.Name).Except(matchingTypeFieldAndProperties.Select(m => m.Name)).ToList(); LoggerScoped.LogError($"It seems you've added/removed field to changed script. This is not supported and will result in undefined behaviour. Hot-reload will not be performed for type: {matchingTypeInExistingAssemblies.Name}" + $"\r\n\r\nYou can skip the check and force reload anyway if needed, to do so go to: 'Window -> Fast Script Reload -> Start Screen -> Reload -> tick 'Disable added/removed fields check'" + (addedMemberNames.Any() ? $"\r\nAdded: {string.Join(", ", addedMemberNames)}" : "")); LoggerScoped.Log( $"There's an experimental feature that allows to add new fields (which are adjustable in editor), to enable please:" + $"\r\n - Open Settings 'Window -> Fast Script Reload -> Start Screen -> New Fields -> tick 'Enable experimental added field support'"); return true; } return false; } private static void FindAndExecuteStaticOnScriptHotReloadNoInstance(Type createdType) { var onScriptHotReloadStaticFnForType = createdType.GetMethod(ON_HOT_RELOAD_NO_INSTANCE_STATIC_METHOD_NAME, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (onScriptHotReloadStaticFnForType != null) { UnityMainThreadDispatcher.Instance.Enqueue(() => { onScriptHotReloadStaticFnForType.Invoke(null, null); }); } } private static void FindAndExecuteOnScriptHotReload(Type type) { var onScriptHotReloadFnForType = type.GetMethod(ON_HOT_RELOAD_METHOD_NAME, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (onScriptHotReloadFnForType != null) { UnityMainThreadDispatcher.Instance.Enqueue(() => { if (!typeof(MonoBehaviour).IsAssignableFrom(type)) { LoggerScoped.LogWarning($"Type: {type.Name} is not {nameof(MonoBehaviour)}, {ON_HOT_RELOAD_METHOD_NAME} method can't be executed. You can still use static version: {ON_HOT_RELOAD_NO_INSTANCE_STATIC_METHOD_NAME}"); return; } foreach (var instanceOfType in GameObject.FindObjectsOfType(type)) //TODO: perf - could find them in different way? { onScriptHotReloadFnForType.Invoke(instanceOfType, null); } }); } } private static string RemoveClassPostfix(string fqdn) { return fqdn.Replace(ClassnamePatchedPostfix, string.Empty); } } [AttributeUsage(AttributeTargets.Assembly)] public class DynamicallyCreatedAssemblyAttribute : Attribute { public DynamicallyCreatedAssemblyAttribute() { } } [AttributeUsage(AttributeTargets.Class)] public class PreventHotReload : Attribute { } public interface IAssemblyChangesLoader { void DynamicallyUpdateMethodsForCreatedAssembly(Assembly dynamicallyLoadedAssemblyWithUpdates, AssemblyChangesLoaderEditorOptionsNeededInBuild editorOptions); } [Serializable] public class AssemblyChangesLoaderEditorOptionsNeededInBuild { public bool IsDidFieldsOrPropertyCountChangedCheckDisabled; public bool EnableExperimentalAddedFieldsSupport; public AssemblyChangesLoaderEditorOptionsNeededInBuild(bool isDidFieldsOrPropertyCountChangedCheckDisabled, bool enableExperimentalAddedFieldsSupport) { IsDidFieldsOrPropertyCountChangedCheckDisabled = isDidFieldsOrPropertyCountChangedCheckDisabled; EnableExperimentalAddedFieldsSupport = enableExperimentalAddedFieldsSupport; } #pragma warning disable 0618 [Obsolete("Needed for network serialization")] #pragma warning restore 0618 public AssemblyChangesLoaderEditorOptionsNeededInBuild() { } //WARN: make sure it has same params as ctor public void UpdateValues(bool isDidFieldsOrPropertyCountChangedCheckDisabled, bool enableExperimentalAddedFieldsSupport) { IsDidFieldsOrPropertyCountChangedCheckDisabled = isDidFieldsOrPropertyCountChangedCheckDisabled; EnableExperimentalAddedFieldsSupport = enableExperimentalAddedFieldsSupport; } } } #endif