using JetBrains.Annotations; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; using System.Text; using UdonSharp; using UdonSharp.Serialization; using UnityEditor; using UnityEngine; using UnityEngine.Profiling; using VRC.Udon; using VRC.Udon.Common.Interfaces; using VRC.Udon.Editor.ProgramSources; using VRC.Udon.EditorBindings; namespace UdonSharpEditor { public static class UdonSharpEditorUtility { /// /// Creates a new UdonAssemblyProgramAsset from an UdonSharpProgramAsset for the sake of portability. Most info used for the inspector gets stripped so this isn't a great solution for remotely complex assets. /// /// The source program asset /// The save path for the asset file. Save path is only needed here because Udon needs a GUID for saving the serialized program asset and it'd be a pain to break that requirement at the moment /// The exported UdonAssemblyProgramAsset [PublicAPI] public static UdonAssemblyProgramAsset UdonSharpProgramToAssemblyProgram(UdonSharpProgramAsset udonSharpProgramAsset, string savePath) { if (EditorApplication.isPlaying) throw new System.NotSupportedException("USharpEditorUtility.UdonSharpProgramToAssemblyProgram() cannot be called in play mode"); UdonAssemblyProgramAsset newProgramAsset = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(newProgramAsset, savePath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); newProgramAsset = AssetDatabase.LoadAssetAtPath(savePath); udonSharpProgramAsset.CompileCsProgram(); string programAssembly = UdonSharpEditorCache.Instance.GetUASMStr(udonSharpProgramAsset); // Strip comments/inline code StringBuilder asmBuilder = new StringBuilder(); using (StringReader reader = new StringReader(programAssembly)) { string line = reader.ReadLine(); while (line != null) { if (!string.IsNullOrWhiteSpace(line) && !line.TrimStart().StartsWith("#", System.StringComparison.Ordinal)) asmBuilder.AppendFormat("{0}\n", line); line = reader.ReadLine(); } } programAssembly = asmBuilder.ToString(); FieldInfo assemblyField = typeof(UdonAssemblyProgramAsset).GetField("udonAssembly", BindingFlags.NonPublic | BindingFlags.Instance); assemblyField.SetValue(newProgramAsset, programAssembly); IUdonProgram program = null; try { UdonSharp.HeapFactory heapFactory = new UdonSharp.HeapFactory(); UdonEditorInterface editorInterface = new UdonEditorInterface(null, heapFactory, null, null, null, null, null, null, null); heapFactory.FactoryHeapSize = udonSharpProgramAsset.GetSerializedUdonProgramAsset().RetrieveProgram().Heap.GetHeapCapacity(); program = editorInterface.Assemble(programAssembly); } catch (System.Exception e) { Debug.LogError(e); return null; } FieldInfo assemblyProgramField = typeof(UdonProgramAsset).GetField("program", BindingFlags.NonPublic | BindingFlags.Instance); assemblyProgramField.SetValue(newProgramAsset, program); IUdonProgram uSharpProgram = udonSharpProgramAsset.GetRealProgram(); IUdonProgram assemblyProgram = (IUdonProgram)assemblyProgramField.GetValue(newProgramAsset); if (uSharpProgram == null || assemblyProgram == null) return null; ImmutableArray symbols = uSharpProgram.SymbolTable.GetSymbols(); foreach (string symbol in symbols) { uint symbolAddress = uSharpProgram.SymbolTable.GetAddressFromSymbol(symbol); System.Type symbolType = uSharpProgram.Heap.GetHeapVariableType(symbolAddress); object symbolValue = uSharpProgram.Heap.GetHeapVariable(symbolAddress); assemblyProgram.Heap.SetHeapVariable(assemblyProgram.SymbolTable.GetAddressFromSymbol(symbol), symbolValue, symbolType); } EditorUtility.SetDirty(newProgramAsset); newProgramAsset.SerializedProgramAsset.StoreProgram(assemblyProgram); EditorUtility.SetDirty(newProgramAsset.SerializedProgramAsset); AssetDatabase.SaveAssets(); // This doesn't work unfortunately due to how Udon tries to locate the serialized asset when importing an assembly //string serializedAssetPath = $"{Path.GetDirectoryName(savePath)}/{Path.GetFileNameWithoutExtension(savePath)}_serialized.asset"; //AssetDatabase.MoveAsset(AssetDatabase.GetAssetPath(newProgramAsset.SerializedProgramAsset), serializedAssetPath); //AssetDatabase.SaveAssets(); return newProgramAsset; } /// /// Deletes an UdonSharp program asset and the serialized program asset associated with it /// /// [PublicAPI] public static void DeleteProgramAsset(UdonSharpProgramAsset programAsset) { if (programAsset == null) return; AbstractSerializedUdonProgramAsset serializedAsset = programAsset.GetSerializedUdonProgramAsset(); if (serializedAsset != null) { string assetPath = AssetDatabase.GetAssetPath(serializedAsset); serializedAsset = AssetDatabase.LoadAssetAtPath(assetPath); if (serializedAsset != null) { AssetDatabase.DeleteAsset(assetPath); } } string programAssetPath = AssetDatabase.GetAssetPath(programAsset); programAsset = AssetDatabase.LoadAssetAtPath(programAssetPath); if (programAsset != null) AssetDatabase.DeleteAsset(programAssetPath); } /// /// Converts a set of UdonSharpBehaviour components to their equivalent UdonBehaviour components /// /// /// [PublicAPI] public static UdonBehaviour[] ConvertToUdonBehaviours(UdonSharpBehaviour[] components, bool convertChildren = false) { return ConvertToUdonBehavioursInternal(components, false, false, convertChildren); } /// /// Converts a set of UdonSharpBehaviour components to their equivalent UdonBehaviour components /// Registers an Undo operation for the conversion /// /// /// [PublicAPI] public static UdonBehaviour[] ConvertToUdonBehavioursWithUndo(UdonSharpBehaviour[] components, bool convertChildren = false) { return ConvertToUdonBehavioursInternal(components, true, false, convertChildren); } static internal Dictionary _programAssetLookup; static internal Dictionary _programAssetTypeLookup; private static void InitTypeLookups() { if (_programAssetLookup == null) { _programAssetLookup = new Dictionary(); _programAssetTypeLookup = new Dictionary(); UdonSharpProgramAsset[] udonSharpProgramAssets = UdonSharpProgramAsset.GetAllUdonSharpPrograms(); foreach (UdonSharpProgramAsset programAsset in udonSharpProgramAssets) { if (programAsset && programAsset.sourceCsScript != null && !_programAssetLookup.ContainsKey(programAsset.sourceCsScript)) { _programAssetLookup.Add(programAsset.sourceCsScript, programAsset); if (programAsset.GetClass() != null) _programAssetTypeLookup.Add(programAsset.GetClass(), programAsset); } } } } private static UdonSharpProgramAsset GetUdonSharpProgramAsset(MonoScript programScript) { InitTypeLookups(); _programAssetLookup.TryGetValue(programScript, out UdonSharpProgramAsset foundProgramAsset); return foundProgramAsset; } /// /// Gets the UdonSharpProgramAsset that represents the program for the given UdonSharpBehaviour /// /// /// [PublicAPI] public static UdonSharpProgramAsset GetUdonSharpProgramAsset(UdonSharpBehaviour udonSharpBehaviour) { return GetUdonSharpProgramAsset(MonoScript.FromMonoBehaviour(udonSharpBehaviour)); } [PublicAPI] public static UdonSharpProgramAsset GetUdonSharpProgramAsset(System.Type type) { InitTypeLookups(); _programAssetTypeLookup.TryGetValue(type, out UdonSharpProgramAsset foundProgramAsset); return foundProgramAsset; } private static readonly FieldInfo _backingBehaviourField = typeof(UdonSharpBehaviour).GetField("_backingUdonBehaviour", BindingFlags.NonPublic | BindingFlags.Instance); /// /// Gets the backing UdonBehaviour for a proxy /// /// /// [PublicAPI] public static UdonBehaviour GetBackingUdonBehaviour(UdonSharpBehaviour behaviour) { return (UdonBehaviour)_backingBehaviourField.GetValue(behaviour); } internal static void SetBackingUdonBehaviour(UdonSharpBehaviour behaviour, UdonBehaviour backingBehaviour) { _backingBehaviourField.SetValue(behaviour, backingBehaviour); } /// /// Returns true if the given behaviour is a proxy behaviour that's linked to an UdonBehaviour. /// /// /// [PublicAPI] public static bool IsProxyBehaviour(UdonSharpBehaviour behaviour) { return GetBackingUdonBehaviour(behaviour) != null; } static Dictionary _proxyBehaviourLookup = new Dictionary(); /// /// Finds an existing proxy behaviour, if none exists returns null /// /// /// [PublicAPI] public static UdonSharpBehaviour FindProxyBehaviour(UdonBehaviour udonBehaviour) { return FindProxyBehaviour(udonBehaviour, ProxySerializationPolicy.Default); } /// /// Finds an existing proxy behaviour, if none exists returns null /// /// /// /// [PublicAPI] public static UdonSharpBehaviour FindProxyBehaviour(UdonBehaviour udonBehaviour, ProxySerializationPolicy proxySerializationPolicy) { if (_proxyBehaviourLookup.TryGetValue(udonBehaviour, out UdonSharpBehaviour proxyBehaviour)) { if (proxyBehaviour != null) { CopyUdonToProxy(proxyBehaviour, proxySerializationPolicy); SetIgnoreEvents(true); try { proxyBehaviour.enabled = false; } finally { SetIgnoreEvents(false); } return proxyBehaviour; } else { _proxyBehaviourLookup.Remove(udonBehaviour); } } UdonSharpBehaviour[] behaviours = udonBehaviour.GetComponents(); foreach (UdonSharpBehaviour udonSharpBehaviour in behaviours) { IUdonBehaviour backingBehaviour = GetBackingUdonBehaviour(udonSharpBehaviour); if (backingBehaviour != null && ReferenceEquals(backingBehaviour, udonBehaviour)) { _proxyBehaviourLookup.Add(udonBehaviour, udonSharpBehaviour); CopyUdonToProxy(udonSharpBehaviour, proxySerializationPolicy); SetIgnoreEvents(true); try { udonSharpBehaviour.enabled = false; } finally { SetIgnoreEvents(false); } return udonSharpBehaviour; } } return null; } /// /// Gets the C# version of an UdonSharpBehaviour that proxies an UdonBehaviour with the program asset for the matching UdonSharpBehaviour type /// /// /// [PublicAPI] public static UdonSharpBehaviour GetProxyBehaviour(UdonBehaviour udonBehaviour) { return GetProxyBehaviour(udonBehaviour, ProxySerializationPolicy.Default); } /// /// Returns if the given UdonBehaviour is an UdonSharpBehaviour /// /// /// [PublicAPI] public static bool IsUdonSharpBehaviour(UdonBehaviour udonBehaviour) { return udonBehaviour.programSource != null && udonBehaviour.programSource is UdonSharpProgramAsset programAsset && programAsset.sourceCsScript != null; } /// /// Gets the UdonSharpBehaviour type from the given behaviour. /// If the behaviour is not an UdonSharpBehaviour, returns null. /// /// /// [PublicAPI] public static System.Type GetUdonSharpBehaviourType(UdonBehaviour udonBehaviour) { if (!IsUdonSharpBehaviour(udonBehaviour)) return null; return ((UdonSharpProgramAsset)udonBehaviour.programSource).GetClass(); } static FieldInfo _skipEventsField = null; /// /// Used to disable sending events to UdonSharpBehaviours for OnEnable, OnDisable, and OnDestroy since they are not always in a valid state to be recognized as proxies during these events. /// /// internal static void SetIgnoreEvents(bool ignore) { if (_skipEventsField == null) _skipEventsField = typeof(UdonSharpBehaviour).GetField("_skipEvents", BindingFlags.Static | BindingFlags.NonPublic); _skipEventsField.SetValue(null, ignore); } /// /// Gets the C# version of an UdonSharpBehaviour that proxies an UdonBehaviour with the program asset for the matching UdonSharpBehaviour type /// /// /// /// [PublicAPI] public static UdonSharpBehaviour GetProxyBehaviour(UdonBehaviour udonBehaviour, ProxySerializationPolicy proxySerializationPolicy) { if (udonBehaviour == null) throw new System.ArgumentNullException("Source Udon Behaviour cannot be null"); if (udonBehaviour.programSource == null) throw new System.ArgumentNullException("Program source on UdonBehaviour cannot be null"); UdonSharpProgramAsset udonSharpProgram = udonBehaviour.programSource as UdonSharpProgramAsset; if (udonSharpProgram == null) throw new System.ArgumentException("UdonBehaviour must be using an UdonSharp program"); UdonSharpBehaviour proxyBehaviour = FindProxyBehaviour(udonBehaviour, proxySerializationPolicy); if (proxyBehaviour) return proxyBehaviour; // We've failed to find an existing proxy behaviour so we need to create one System.Type scriptType = udonSharpProgram.GetClass(); if (scriptType == null) return null; SetIgnoreEvents(true); try { proxyBehaviour = (UdonSharpBehaviour)udonBehaviour.gameObject.AddComponent(scriptType); proxyBehaviour.hideFlags = HideFlags.DontSaveInBuild | #if !UDONSHARP_DEBUG HideFlags.HideInInspector | #endif HideFlags.DontSaveInEditor; proxyBehaviour.enabled = false; } finally { SetIgnoreEvents(false); } SetBackingUdonBehaviour(proxyBehaviour, udonBehaviour); _proxyBehaviourLookup.Add(udonBehaviour, proxyBehaviour); CopyUdonToProxy(proxyBehaviour, proxySerializationPolicy); return proxyBehaviour; } /// /// Copies the state of the proxy to its backing UdonBehaviour /// /// [PublicAPI] public static void CopyProxyToUdon(UdonSharpBehaviour proxy) { CopyProxyToUdon(proxy, ProxySerializationPolicy.Default); } /// /// Copies the state of the UdonBehaviour to its proxy object /// /// [PublicAPI] public static void CopyUdonToProxy(UdonSharpBehaviour proxy) { CopyUdonToProxy(proxy, ProxySerializationPolicy.Default); } /// /// Copies the state of the proxy to its backing UdonBehaviour /// /// /// [PublicAPI] public static void CopyProxyToUdon(UdonSharpBehaviour proxy, ProxySerializationPolicy serializationPolicy) { if (serializationPolicy.MaxSerializationDepth == 0) return; Profiler.BeginSample("CopyProxyToUdon"); SimpleValueStorage udonBehaviourStorage = new SimpleValueStorage(GetBackingUdonBehaviour(proxy)); ProxySerializationPolicy lastPolicy = USBSerializationContext.currentPolicy; USBSerializationContext.currentPolicy = serializationPolicy; Serializer.CreatePooled(proxy.GetType()).WriteWeak(udonBehaviourStorage, proxy); USBSerializationContext.currentPolicy = lastPolicy; Profiler.EndSample(); } /// /// Copies the state of the UdonBehaviour to its proxy object /// /// /// [PublicAPI] public static void CopyUdonToProxy(UdonSharpBehaviour proxy, ProxySerializationPolicy serializationPolicy) { if (serializationPolicy.MaxSerializationDepth == 0) return; Profiler.BeginSample("CopyUdonToProxy"); SimpleValueStorage udonBehaviourStorage = new SimpleValueStorage(GetBackingUdonBehaviour(proxy)); ProxySerializationPolicy lastPolicy = USBSerializationContext.currentPolicy; USBSerializationContext.currentPolicy = serializationPolicy; object proxyObj = proxy; Serializer.CreatePooled(proxy.GetType()).ReadWeak(ref proxyObj, udonBehaviourStorage); USBSerializationContext.currentPolicy = lastPolicy; Profiler.EndSample(); } [PublicAPI] public static UdonBehaviour CreateBehavourForProxy(UdonSharpBehaviour udonSharpBehaviour) { UdonBehaviour backingBehaviour = GetBackingUdonBehaviour(udonSharpBehaviour); if (backingBehaviour == null) { backingBehaviour = udonSharpBehaviour.gameObject.AddComponent(); backingBehaviour.programSource = GetUdonSharpProgramAsset(udonSharpBehaviour); } CopyProxyToUdon(udonSharpBehaviour); return backingBehaviour; } /// /// Destroys an UdonSharpBehaviour proxy and its underlying UdonBehaviour /// /// [PublicAPI] public static void DestroyImmediate(UdonSharpBehaviour behaviour) { UdonBehaviour backingBehaviour = GetBackingUdonBehaviour(behaviour); Object.DestroyImmediate(behaviour); if (backingBehaviour) { _proxyBehaviourLookup.Remove(backingBehaviour); SetIgnoreEvents(true); try { Object.DestroyImmediate(backingBehaviour); } finally { SetIgnoreEvents(false); } } } #region Internal utilities internal static void CollectUdonSharpBehaviourReferencesInternal(object rootObject, HashSet gatheredSet, HashSet visitedSet = null) { if (gatheredSet == null) gatheredSet = new HashSet(); if (visitedSet == null) visitedSet = new HashSet(new VRC.Udon.Serialization.OdinSerializer.Utilities.ReferenceEqualityComparer()); if (rootObject == null) return; if (visitedSet.Contains(rootObject)) return; System.Type objectType = rootObject.GetType(); if (objectType.IsValueType) return; if (VRC.Udon.Serialization.OdinSerializer.FormatterUtilities.IsPrimitiveType(objectType)) return; visitedSet.Add(rootObject); if (objectType == typeof(UdonSharpBehaviour) || objectType.IsSubclassOf(typeof(UdonSharpBehaviour))) { gatheredSet.Add((UdonSharpBehaviour)rootObject); } if (objectType.IsArray) { foreach (object arrayElement in (System.Array)rootObject) { CollectUdonSharpBehaviourReferencesInternal(arrayElement, gatheredSet, visitedSet); } } else { FieldInfo[] objectFields = objectType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); foreach (FieldInfo fieldInfo in objectFields) { object fieldValue = fieldInfo.GetValue(rootObject); CollectUdonSharpBehaviourReferencesInternal(fieldValue, gatheredSet, visitedSet); } } } internal static UdonBehaviour[] ConvertToUdonBehavioursInternal(UdonSharpBehaviour[] components, bool shouldUndo, bool showPrompts, bool convertChildren) { components = components.Distinct().ToArray(); if (showPrompts) { HashSet allReferencedBehaviours = new HashSet(); // Check if any of these need child component conversion foreach (UdonSharpBehaviour targetObject in components) { HashSet referencedBehaviours = new HashSet(); CollectUdonSharpBehaviourReferencesInternal(targetObject, referencedBehaviours); if (referencedBehaviours.Count > 1) { foreach (UdonSharpBehaviour referencedBehaviour in referencedBehaviours) { if (referencedBehaviour != targetObject) allReferencedBehaviours.Add(referencedBehaviour); } } } if (allReferencedBehaviours.Count > 0) { // This is an absolute mess, it should probably just be simplified to counting the number of affected behaviours string referencedBehaviourStr; if (allReferencedBehaviours.Count <= 2) referencedBehaviourStr = string.Join(", ", allReferencedBehaviours.Select(e => $"'{e.ToString()}'")); else referencedBehaviourStr = $"{allReferencedBehaviours.Count} behaviours"; string rootBehaviourStr; if (components.Length <= 2) rootBehaviourStr = $"{string.Join(", ", components.Select(e => $"'{e.ToString()}'"))} reference{(components.Length == 1 ? "s" : "")} "; else rootBehaviourStr = $"{components.Length} behaviours to convert reference "; string messageStr = $"{rootBehaviourStr}{referencedBehaviourStr}. Do you want to convert all referenced behaviours as well? If no, references to these behaviours will be set to null."; int result = EditorUtility.DisplayDialogComplex("Dependent behaviours found", messageStr, "Yes", "Cancel", "No"); if (result == 2) // No convertChildren = false; else if (result == 1) // Cancel return null; } } if (shouldUndo) Undo.RegisterCompleteObjectUndo(components, "Convert to UdonBehaviour"); List createdComponents = new List(); foreach (UdonSharpBehaviour targetObject in components) { MonoScript behaviourScript = MonoScript.FromMonoBehaviour(targetObject); UdonSharpProgramAsset programAsset = GetUdonSharpProgramAsset(behaviourScript); if (programAsset == null) { if (showPrompts) { string scriptPath = AssetDatabase.GetAssetPath(behaviourScript); string scriptDirectory = Path.GetDirectoryName(scriptPath); string scriptFileName = Path.GetFileNameWithoutExtension(scriptPath); string assetPath = Path.Combine(scriptDirectory, $"{scriptFileName}.asset").Replace('\\', '/'); if (EditorUtility.DisplayDialog("No linked program asset", $"There was no UdonSharpProgramAsset found for '{behaviourScript.GetClass()}', do you want to create one?", "Ok", "Cancel")) { if (AssetDatabase.LoadAssetAtPath(assetPath) != null) { if (!EditorUtility.DisplayDialog("Existing file found", $"Asset file {assetPath} already exists, do you want to overwrite it?", "Ok", "Cancel")) continue; } } else continue; programAsset = ScriptableObject.CreateInstance(); programAsset.sourceCsScript = behaviourScript; AssetDatabase.CreateAsset(programAsset, assetPath); AssetDatabase.SaveAssets(); UdonSharpProgramAsset.ClearProgramAssetCache(); programAsset.CompileCsProgram(); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); } else { Debug.LogWarning($"Could not convert U# behaviour '{behaviourScript.GetClass()}' on '{targetObject.gameObject}' because it does not have a corresponding UdonSharpProgramAsset"); continue; } } GameObject targetGameObject = targetObject.gameObject; UdonBehaviour udonBehaviour = null; if (shouldUndo) udonBehaviour = Undo.AddComponent(targetGameObject); else udonBehaviour = targetGameObject.AddComponent(); udonBehaviour.programSource = programAsset; #pragma warning disable CS0618 // Type or member is obsolete udonBehaviour.SynchronizePosition = false; udonBehaviour.AllowCollisionOwnershipTransfer = false; #pragma warning restore CS0618 // Type or member is obsolete udonBehaviour.Reliable = programAsset.behaviourSyncMode == BehaviourSyncMode.Manual; //if (shouldUndo) // Undo.RegisterCompleteObjectUndo(targetObject, "Convert C# to U# behaviour"); UdonSharpEditorUtility.SetBackingUdonBehaviour(targetObject, udonBehaviour); try { if (convertChildren) UdonSharpEditorUtility.CopyProxyToUdon(targetObject, shouldUndo ? ProxySerializationPolicy.AllWithCreateUndo : ProxySerializationPolicy.AllWithCreate); else UdonSharpEditorUtility.CopyProxyToUdon(targetObject, ProxySerializationPolicy.RootOnly); } catch (System.Exception e) { Debug.LogError(e); } UdonSharpEditorUtility.SetBackingUdonBehaviour(targetObject, null); System.Type behaviourType = targetObject.GetType(); UdonSharpBehaviour newProxy; SetIgnoreEvents(true); try { if (shouldUndo) newProxy = (UdonSharpBehaviour)Undo.AddComponent(targetObject.gameObject, behaviourType); else newProxy = (UdonSharpBehaviour)targetObject.gameObject.AddComponent(behaviourType); UdonSharpEditorUtility.SetBackingUdonBehaviour(newProxy, udonBehaviour); try { UdonSharpEditorUtility.CopyUdonToProxy(newProxy); } catch (System.Exception e) { Debug.LogError(e); } if (shouldUndo) Undo.DestroyObjectImmediate(targetObject); else Object.DestroyImmediate(targetObject); newProxy.hideFlags = HideFlags.DontSaveInBuild | #if !UDONSHARP_DEBUG HideFlags.HideInInspector | #endif HideFlags.DontSaveInEditor; newProxy.enabled = false; } finally { SetIgnoreEvents(false); } createdComponents.Add(udonBehaviour); } return createdComponents.ToArray(); } #endregion } }