This commit is contained in:
CortexCore
2023-10-02 23:24:56 +08:00
parent 8ef5c7ec0a
commit 947e52e748
183 changed files with 107857 additions and 9378 deletions

View File

@@ -1,243 +0,0 @@
#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<Type> ExcludeMethodsDefinedOnTypes = new List<Type>
{
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<Type, Type> _existingTypeToRedirectedType = new Dictionary<Type, Type>();
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<PreventHotReload>() != 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. <color=orange>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.</color>");
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<MemberInfo>()).ToList();
var matchingTypeFieldAndProperties = matchingTypeInExistingAssemblies.GetFields(ALL_BINDING_FLAGS).Concat(matchingTypeInExistingAssemblies.GetProperties(ALL_BINDING_FLAGS).Cast<MemberInfo>()).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(
$"<color=orange>There's an experimental feature that allows to add new fields (which are adjustable in editor), to enable please:</color>" +
$"\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 enable 0618
public AssemblyChangesLoaderEditorOptionsNeededInBuild()
{
}
//WARN: make sure it has same params as ctor
public void UpdateValues(bool isDidFieldsOrPropertyCountChangedCheckDisabled, bool enableExperimentalAddedFieldsSupport)
{
IsDidFieldsOrPropertyCountChangedCheckDisabled = isDidFieldsOrPropertyCountChangedCheckDisabled;
EnableExperimentalAddedFieldsSupport = enableExperimentalAddedFieldsSupport;
}
}
}
#endif

View File

@@ -1,43 +0,0 @@
using System;
using System.Linq;
using System.Reflection;
using ImmersiveVRTools.Runtime.Common.Utilities;
#if UNITY_EDITOR || LiveScriptReload_Enabled
namespace FastScriptReload.Runtime
{
public class AssemblyChangesLoaderResolver
{
private static AssemblyChangesLoaderResolver _instance;
public static AssemblyChangesLoaderResolver Instance => _instance ?? (_instance = new AssemblyChangesLoaderResolver());
private IAssemblyChangesLoader _cachedNetworkLoader;
public IAssemblyChangesLoader Resolve()
{
#if LiveScriptReload_Enabled
//network loader is in add-on that's not referenced by this lib, use reflection to get instance
if (_cachedNetworkLoader == null)
{
_cachedNetworkLoader = (IAssemblyChangesLoader)ReflectionHelper.GetAllTypes()
.First(t => t.FullName == "LiveScriptReload.Runtime.NetworkedAssemblyChangesSender")
.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy)
.GetValue(null);
}
if (_cachedNetworkLoader == null)
{
throw new Exception("Unable to resolve NetworkedAssemblyChangesSender, Live Script Reload will not work - please contact support");
}
return _cachedNetworkLoader;
#else
return AssemblyChangesLoader.Instance;
#endif
}
}
}
#endif

View File

@@ -1,76 +0,0 @@
#if UNITY_EDITOR || LiveScriptReload_Enabled
using System;
using System.IO;
using System.Linq;
using ImmersiveVrToolsCommon.Runtime.Logging;
using UnityEngine;
namespace FastScriptReload.Runtime
{
#if UNITY_EDITOR
[UnityEditor.InitializeOnLoad]
#endif
public class DetourCrashHandler
{
//TODO: add device support / android crashes / how to report issues back?
public static string LastDetourFilePath;
static DetourCrashHandler()
{
#if UNITY_EDITOR
Init();
#else
LoggerScoped.Log($"{nameof(DetourCrashHandler)}: currently only supported in Editor");
#endif
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
static void Init()
{
#if UNITY_EDITOR
LastDetourFilePath = Path.GetTempPath() + Application.productName + "-last-detour.txt";
foreach (var c in Path.GetInvalidFileNameChars())
{
LastDetourFilePath = LastDetourFilePath.Replace(c, '-');
}
#else
LoggerScoped.Log($"{nameof(DetourCrashHandler)}: currently only supported in Editor");
#endif
}
public static void LogDetour(string fullName)
{
#if UNITY_EDITOR
File.AppendAllText(LastDetourFilePath, fullName + Environment.NewLine);
#else
LoggerScoped.Log($"{nameof(DetourCrashHandler)}: currently only supported in Editor");
#endif
}
public static string RetrieveLastDetour()
{
#if UNITY_EDITOR
if (File.Exists(LastDetourFilePath))
{
var lines = File.ReadAllLines(LastDetourFilePath);
return lines.Length > 0 ? lines.Last() : string.Empty;
}
return string.Empty;
#else
LoggerScoped.Log($"{nameof(DetourCrashHandler)}: currently only supported in Editor");
return string.Empty;
#endif
}
public static void ClearDetourLog()
{
#if UNITY_EDITOR
File.Delete(LastDetourFilePath);
#else
LoggerScoped.Log($"{nameof(DetourCrashHandler)}: currently only supported in Editor");
#endif
}
}
}
#endif

View File

@@ -1,19 +0,0 @@
{
"name": "FastScriptReload.Runtime",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"0Harmony.dll",
"ImmersiveVRTools.Common.Runtime.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_EDITOR || LiveScriptReload_IncludeInBuild_Enabled"
],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -1,22 +0,0 @@
using ImmersiveVrToolsCommon.Runtime.Logging;
using UnityEngine;
namespace FastScriptReload.Runtime
{
#if UNITY_EDITOR
[UnityEditor.InitializeOnLoad]
#endif
public static class LoggerScopedInitializer
{
static LoggerScopedInitializer()
{
Init();
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
static void Init()
{
LoggerScoped.LogPrefix = "FSR: ";
}
}
}

View File

@@ -1,49 +0,0 @@
#if UNITY_EDITOR || LiveScriptReload_Enabled
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
namespace FastScriptReload.Runtime
{
public class ProjectTypeCache
{
private static bool _isInitialized;
private static Dictionary<string, Type> _allTypesInNonDynamicGeneratedAssemblies;
public static Dictionary<string, Type> AllTypesInNonDynamicGeneratedAssemblies
{
get
{
if (!_isInitialized)
{
Init();
}
return _allTypesInNonDynamicGeneratedAssemblies;
}
}
private static void Init()
{
if (_allTypesInNonDynamicGeneratedAssemblies == null)
{
var typeLookupSw = new Stopwatch();
typeLookupSw.Start();
_allTypesInNonDynamicGeneratedAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !CustomAttributeExtensions.GetCustomAttributes<DynamicallyCreatedAssemblyAttribute>((Assembly)a).Any())
.SelectMany(a => a.GetTypes())
.GroupBy(t => t.FullName)
.Select(g => g.First()) //TODO: quite odd that same type full name can be defined multiple times? eg Microsoft.CodeAnalysis.EmbeddedAttribute throws 'An item with the same key has already been added'
.ToDictionary(t => t.FullName, t => t);
#if ImmersiveVrTools_DebugEnabled
LoggerScoped.Log($"Initialized type-lookup dictionary, took: {typeLookupSw.ElapsedMilliseconds}ms - cached");
#endif
}
}
}
}
#endif

View File

@@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.Dynamic;
using UnityEngine;
using Object = System.Object;
namespace FastScriptReload.Scripts.Runtime
{
public static class TemporaryNewFieldValues
{
public delegate object GetNewFieldInitialValue(Type forNewlyGeneratedType);
public delegate Type GetNewFieldType(Type forNewlyGeneratedType);
private static readonly Dictionary<object, ExpandoForType> _existingObjectToFiledNameValueMap = new Dictionary<object, ExpandoForType>();
private static readonly Dictionary<Type, Dictionary<string, GetNewFieldInitialValue>> _existingObjectTypeToFieldNameToCreateDetaultValueFn = new Dictionary<Type, Dictionary<string, GetNewFieldInitialValue>>();
private static readonly Dictionary<Type, Dictionary<string, GetNewFieldType>> _existingObjectTypeToFieldNameToType = new Dictionary<Type, Dictionary<string, GetNewFieldType>>();
//Unity by default will auto init some classes, like gradient, but those are not value types so need to be initialized manually
private static Dictionary<Type, Func<object>> ReferenceTypeToCreateDefaultValueFn = new Dictionary<Type, Func<object>>()
{
[typeof(Gradient)] = () => new Gradient(),
[typeof(AnimationCurve)] = () => new AnimationCurve(),
};
public static void RegisterNewFields(Type existingType, Dictionary<string, GetNewFieldInitialValue> fieldNameToGenerateDefaultValueFn, Dictionary<string, GetNewFieldType> fieldNameToGetTypeFn)
{
_existingObjectTypeToFieldNameToCreateDetaultValueFn[existingType] = fieldNameToGenerateDefaultValueFn;
_existingObjectTypeToFieldNameToType[existingType] = fieldNameToGetTypeFn;
}
public static dynamic ResolvePatchedObject<TCreatedType>(object original)
{
if (!_existingObjectToFiledNameValueMap.TryGetValue(original, out var existingExpandoToObjectTypePair))
{
var patchedObject = new ExpandoObject();
var expandoForType = new ExpandoForType { ForType = typeof(TCreatedType), Object = patchedObject };
InitializeAdditionalFieldValues<TCreatedType>(original, patchedObject);
_existingObjectToFiledNameValueMap[original] = expandoForType;
return patchedObject;
}
else
{
if (existingExpandoToObjectTypePair.ForType != typeof(TCreatedType))
{
InitializeAdditionalFieldValues<TCreatedType>(original, existingExpandoToObjectTypePair.Object);
existingExpandoToObjectTypePair.ForType = typeof(TCreatedType);
}
return existingExpandoToObjectTypePair.Object;
}
}
public static bool TryGetDynamicallyAddedFieldValues(object forObject, out IDictionary<string, object> addedFieldValues)
{
if (_existingObjectToFiledNameValueMap.TryGetValue(forObject, out var expandoForType))
{
addedFieldValues = expandoForType.Object;
return true;
}
addedFieldValues = null;
return false;
}
private static void InitializeAdditionalFieldValues<TCreatedType>(object original, ExpandoObject patchedObject)
{
var originalType = original.GetType(); //TODO: PERF: resolve via TOriginal, not getType
var patchedObjectAsDict = patchedObject as IDictionary<string, Object>;
foreach (var fieldNameToGenerateDefaultValueFn in _existingObjectTypeToFieldNameToCreateDetaultValueFn[originalType])
{
if (!patchedObjectAsDict.ContainsKey(fieldNameToGenerateDefaultValueFn.Key))
{
patchedObjectAsDict[fieldNameToGenerateDefaultValueFn.Key] = fieldNameToGenerateDefaultValueFn.Value(typeof(TCreatedType));
if (patchedObjectAsDict[fieldNameToGenerateDefaultValueFn.Key] == null)
{
var fieldType = _existingObjectTypeToFieldNameToType[originalType][fieldNameToGenerateDefaultValueFn.Key](typeof(TCreatedType));
if (ReferenceTypeToCreateDefaultValueFn.TryGetValue(fieldType, out var createValueFn))
{
patchedObjectAsDict[fieldNameToGenerateDefaultValueFn.Key] = createValueFn();
}
}
}
}
}
}
public class ExpandoForType {
public Type ForType;
public ExpandoObject Object;
}
}