#if UNITY_EDITOR using System; using System.Linq.Expressions; using System.Reflection; using UnityEditor; using UnityEditor.Callbacks; using UnityEngine; namespace Kamgam.SkyClouds.URP { /// /// Allows you to register a callback before compilation /// which is then executed automatically after compilation.
///
/// Methods registered are usually executed in FIFO order. Though there /// is no guarantee that this will always be the case. /// /// CrossCompileCallbacks.RegisterCallback(testCallbackA); // It can find the type automatically. /// CrossCompileCallbacks.RegisterCallback(typeof(YourClass), "testCallbackA"); /// ///
public static class CrossCompileCallbacks { /// /// If set to true then the callbacks will not be called immediately but /// within the next editor update cycle.
/// Use this to avoid "Calling ... from assembly reloading callbacks are not supported." errors. ///
public static bool DelayExecutionAfterCompilation { get => SessionState.GetBool(typeName() + ".DelayExecution", false); set => SessionState.SetBool(typeName() + ".DelayExecution", value); } static string typeName() => typeof(CrossCompileCallbacks).FullName; const string _maxIndexKey = ".MaxIndex"; static string maxIndexKey() => typeName() + _maxIndexKey; const string _lastReleasedIndexKey = ".LastReleasedIndex"; static string lastReleasedIndexKey() => typeName() + _lastReleasedIndexKey; const string _indexTypeKey = ".Index[{0}].Type"; static string indexTypeKey(int index) => string.Format(typeName() + _indexTypeKey, index); const string _indexMethodKey = ".Index[{0}].Method"; static string indexMethodKey(int index) => string.Format(typeName() + _indexMethodKey, index); static int getMaxIndex() { return SessionState.GetInt(maxIndexKey(), -1); } static int getNextIndex() { int maxIndex; // Try to reuse an old index (update max index if necessary) int reusableIndex = SessionState.GetInt(lastReleasedIndexKey(), -1); if (reusableIndex >= 0) { SessionState.SetInt(lastReleasedIndexKey(), -1); maxIndex = getMaxIndex(); if(maxIndex < reusableIndex) SessionState.SetInt(maxIndexKey(), reusableIndex); return reusableIndex; } // New index needed (increase max index). maxIndex = SessionState.GetInt(maxIndexKey(), -1); maxIndex++; SessionState.SetInt(maxIndexKey(), maxIndex); return maxIndex; } public static void ReleaseIndex(int index) { if (index < 0) return; SessionState.SetInt(lastReleasedIndexKey(), index); SessionState.EraseString(indexTypeKey(index)); SessionState.EraseString(indexMethodKey(index)); // Decrease or erase max index if needed. int maxIndex = getMaxIndex(); if(index == maxIndex) { maxIndex--; if(maxIndex < 0) SessionState.EraseInt(maxIndexKey()); else SessionState.SetInt(maxIndexKey(), maxIndex); } } public static void ReleaseAllOnType(Type type) { if (type == null) return; int maxIndex = getMaxIndex(); for (int i = maxIndex; i >= 0; i--) { string typeName; GetCallbackInfo(i, out typeName, out _); if(typeName == type.FullName) { ReleaseIndex(i); } } } /// /// Registers a callback and returns an index >= 0 on success and -1 on failure. /// /// A static method without any parameters. /// public static int RegisterCallback(System.Action callback) { if (callback == null) return -1; var methodInfo = callback.GetMethodInfo(); if (methodInfo == null) return -1; if (!methodInfo.IsStatic) { Debug.Log("Method needs to be static."); return -1; } return RegisterCallback(methodInfo.DeclaringType, methodInfo.Name); } /// /// Registers a callback and returns an index >= 0 on success and -1 on failure. /// /// /// A static method without any parameters. /// public static int RegisterCallback(Type type, string staticMethodName) { if (type == null || string.IsNullOrEmpty(staticMethodName)) { Debug.Assert(type != null); Debug.Assert(staticMethodName != null); return -1; } // Check if methods has any parameters (that's not supported) try { var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public; var methodInfo = type.GetMethod(staticMethodName, flags); if (methodInfo == null) { Debug.LogError("No static method '" + staticMethodName + "' found in '" + type.FullName + "'."); return -1; } if (methodInfo.GetParameters().Length > 0) { Debug.Assert(methodInfo.GetParameters().Length == 0); return -1; } } catch (System.Exception e) { Debug.LogError($"CrossCompileCallbacks: Error while checking '{staticMethodName}' method parameters. Error:\n" + e.Message); } int index = getNextIndex(); SessionState.SetString(indexTypeKey(index), type.FullName); SessionState.SetString(indexMethodKey(index), staticMethodName); return index; } public static void GetCallbackInfo(int index, out string typeName, out string methodName) { typeName = SessionState.GetString(indexTypeKey(index), null); methodName = SessionState.GetString(indexMethodKey(index), null); } [DidReloadScripts(-1)] static void onAfterCompilation() { if (DelayExecutionAfterCompilation) { EditorApplication.delayCall -= delayedExecuteRegisteredCallbacks; EditorApplication.delayCall += delayedExecuteRegisteredCallbacks; } else { executeRegisteredCallbacks(); } } static void delayedExecuteRegisteredCallbacks() { EditorApplication.delayCall -= delayedExecuteRegisteredCallbacks; executeRegisteredCallbacks(); } static void executeRegisteredCallbacks() { int maxIndex = getMaxIndex(); for (int i = maxIndex; i >= 0; i--) { string typeName; string methodName; GetCallbackInfo(i, out typeName, out methodName); try { ReleaseIndex(i); if (string.IsNullOrEmpty(typeName) || string.IsNullOrEmpty(methodName)) continue; var methodInfo = findStaticMethod(typeName, methodName); methodInfo.Invoke(null, null); } catch (System.Exception e) { string errorMsg = e.Message; if(errorMsg.Contains("invocation") && e.InnerException != null) { errorMsg += "\n" + e.InnerException.Message; } Debug.LogError($"CrossCompileCallbacks: Calling '{typeName}.{methodName}' failed. Error:\n" + errorMsg); } } } static MethodInfo findStaticMethod(string fullTypeName, string methodName) { var type = findType(fullTypeName); if (type == null) return null; var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public; var methodInfo = type.GetMethod(methodName, flags); return methodInfo; } static Type findType(string fullTypeName) { Debug.Assert(fullTypeName != null); var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in assemblies) { Type t = assembly.GetType(fullTypeName, throwOnError: false); if (t != null) return t; } throw new ArgumentException("Type " + fullTypeName + " doesn't exist in the current app domain."); } /// /// Utility method to store a static parameterless Action /// in the SessionState for retrieval at a later time. /// /// /// /// public static bool StoreAction(string sessionStorageKey, System.Action action) { if (action == null) return false; var methodInfo = action.GetMethodInfo(); if (methodInfo == null) return false; if (!methodInfo.IsStatic) { Debug.Log("Method '"+ methodInfo.Name + "'needs to be static."); return false; } SessionState.SetString(sessionStorageKey + ".Type", methodInfo.DeclaringType.FullName); SessionState.SetString(sessionStorageKey + ".Method", methodInfo.Name); return true; } /// /// Retrieves the Action from the SessionState. /// /// /// public static System.Action GetStoredAction(string sessionStorageKey) { var typeName = SessionState.GetString(sessionStorageKey + ".Type", null); var methodName = SessionState.GetString(sessionStorageKey + ".Method", null); if (string.IsNullOrEmpty(typeName) || string.IsNullOrEmpty(methodName)) { return null; } var type = findType(typeName); if (type == null) return null; var methodInfo = findStaticMethod(typeName, methodName); if (methodInfo == null) return null; return (Action) Delegate.CreateDelegate(typeof(Action), methodInfo); } public static void ClearStoredAction(string sessionStorageKey) { SessionState.EraseString(sessionStorageKey + ".Type"); SessionState.EraseString(sessionStorageKey + ".Method"); } // Testing /* [DidReloadScripts] static void StartTest() { Debug.Log("CrossCompileCallbacks: Starting test."); RegisterCallback(testCallbackA); RegisterCallback(typeof(CrossCompileCallbacks), "testCallbackB"); var action = GetStoredAction("storedActionA"); ClearStoredAction("storedActionA"); if (action != null) action.Invoke(); StoreAction("storedActionA", storedActionA); } static void testCallbackA() { Debug.Log("Test callback A executed."); } static void testCallbackB() { Debug.Log("Test callback B executed."); } static void storedActionA() { Debug.Log("Stored action A executed."); } //*/ } } #endif