summaryrefslogtreecommitdiff
path: root/VRCSDK3Worlds/Assets/Udon/Editor/UdonEditorManager.cs
diff options
context:
space:
mode:
Diffstat (limited to 'VRCSDK3Worlds/Assets/Udon/Editor/UdonEditorManager.cs')
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/UdonEditorManager.cs561
1 files changed, 561 insertions, 0 deletions
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/UdonEditorManager.cs b/VRCSDK3Worlds/Assets/Udon/Editor/UdonEditorManager.cs
new file mode 100644
index 00000000..108e7dc6
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/UdonEditorManager.cs
@@ -0,0 +1,561 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using UnityEditor;
+using UnityEditor.Build;
+using UnityEditor.Build.Reporting;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using VRC.Udon.Common.Interfaces;
+using VRC.Udon.EditorBindings;
+using VRC.Udon.EditorBindings.Interfaces;
+using VRC.Udon.Graph;
+using VRC.Udon.Graph.Interfaces;
+using VRC.Udon.UAssembly.Interfaces;
+
+namespace VRC.Udon.Editor
+{
+ public class UdonEditorManager : IUdonEditorInterface
+ {
+ #region Singleton
+
+ private static UdonEditorManager _instance;
+ public static UdonEditorManager Instance => _instance ?? (_instance = new UdonEditorManager());
+
+ #endregion
+
+ #region Build Preprocessor Class
+
+ private class UdonBuildPreprocessor : IProcessSceneWithReport
+ {
+ public int callbackOrder => 0;
+
+ public void OnProcessScene(Scene scene, BuildReport report)
+ {
+ PopulateSceneSerializedProgramAssetReferences(scene);
+ PopulateAssetDependenciesPrefabSerializedProgramAssetReferences(scene.path);
+ }
+ }
+
+ #endregion
+
+ #region Public Events
+
+ public event Action WantRepaint;
+
+ #endregion
+
+ #region Private Constants
+
+ private const double REFRESH_QUEUE_WAIT_PERIOD = 5.0;
+
+ #endregion
+
+ #region Private Fields
+
+ private readonly UdonEditorInterface _udonEditorInterface;
+
+ private readonly HashSet<AbstractUdonProgramSource> _programSourceRefreshQueue = new HashSet<AbstractUdonProgramSource>();
+
+ #endregion
+
+ #region Initialization
+
+ [InitializeOnLoadMethod]
+ private static void Initialize()
+ {
+ _instance = new UdonEditorManager();
+ }
+
+ #endregion
+
+ #region Constructors
+
+ private UdonEditorManager()
+ {
+ _udonEditorInterface = new UdonEditorInterface();
+ _udonEditorInterface.AddTypeResolver(new UdonBehaviourTypeResolver());
+
+ EditorSceneManager.sceneOpened += OnSceneOpened;
+ EditorSceneManager.sceneSaving += OnSceneSaving;
+ EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
+ }
+
+ #endregion
+
+ #region UdonBehaviour and ProgramSource Refresh
+
+ public void QueueAndRefreshProgram(AbstractUdonProgramSource programSource)
+ {
+ QueueProgramSourceRefresh(programSource);
+ RefreshQueuedProgramSources();
+ }
+
+ public void RefreshQueuedProgramSources()
+ {
+ foreach(AbstractUdonProgramSource programSource in _programSourceRefreshQueue)
+ {
+ if(programSource == null)
+ {
+ return;
+ }
+
+ try
+ {
+ programSource.RefreshProgram();
+ }
+ catch(Exception e)
+ {
+ UnityEngine.Debug.LogError($"Failed to refresh program '{programSource.name}' due to exception '{e}'.");
+ }
+ }
+
+ _programSourceRefreshQueue.Clear();
+
+ WantRepaint?.Invoke();
+ }
+
+ public bool IsProgramSourceRefreshQueued(AbstractUdonProgramSource programSource)
+ {
+ if(_programSourceRefreshQueue.Count <= 0)
+ {
+ return false;
+ }
+
+ if(!_programSourceRefreshQueue.Contains(programSource))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public void QueueProgramSourceRefresh(AbstractUdonProgramSource programSource)
+ {
+ if(Application.isPlaying)
+ {
+ return;
+ }
+
+ if(programSource == null)
+ {
+ return;
+ }
+
+ if(IsProgramSourceRefreshQueued(programSource))
+ {
+ return;
+ }
+
+ _programSourceRefreshQueue.Add(programSource);
+ }
+
+ public void CancelQueuedProgramSourceRefresh(AbstractUdonProgramSource programSource)
+ {
+ if(programSource == null)
+ {
+ return;
+ }
+
+ if(_programSourceRefreshQueue.Contains(programSource))
+ {
+ _programSourceRefreshQueue.Remove(programSource);
+ }
+ }
+
+ [MenuItem("VRChat SDK/Utilities/Re-compile All Program Sources")]
+ public static void RecompileAllProgramSources()
+ {
+ string[] programSourceGUIDs = AssetDatabase.FindAssets("t:AbstractUdonProgramSource");
+ foreach(string guid in programSourceGUIDs)
+ {
+ string assetPath = AssetDatabase.GUIDToAssetPath(guid);
+ AbstractUdonProgramSource programSource = AssetDatabase.LoadAssetAtPath<AbstractUdonProgramSource>(assetPath);
+ if(programSource == null)
+ {
+ continue;
+ }
+ programSource.RefreshProgram();
+ }
+
+ AssetDatabase.SaveAssets();
+
+ PopulateAllPrefabSerializedProgramAssetReferences();
+ }
+
+ [PublicAPI]
+ public static void PopulateAllPrefabSerializedProgramAssetReferences()
+ {
+ foreach(string prefabPath in GetAllPrefabAssetPaths())
+ {
+ PopulatePrefabSerializedProgramAssetReferences(prefabPath);
+ }
+ }
+
+ private static List<UdonBehaviour> prefabBehavioursTempList = new List<UdonBehaviour>();
+
+ [PublicAPI]
+ public static void PopulateAssetDependenciesPrefabSerializedProgramAssetReferences(string assetPath)
+ {
+ IEnumerable<string> prefabDependencyPaths = AssetDatabase.GetDependencies(assetPath, true)
+ .Where(path => path.EndsWith(".prefab"))
+ .Where(path => path.StartsWith("Assets"));
+
+ foreach(string prefabPath in prefabDependencyPaths)
+ {
+ if(!(AssetDatabase.LoadMainAssetAtPath(prefabPath) is GameObject prefab))
+ {
+ return;
+ }
+
+ prefab.GetComponentsInChildren<UdonBehaviour>(prefabBehavioursTempList);
+ if(prefabBehavioursTempList.Count < 1)
+ {
+ return;
+ }
+
+ PopulatePrefabSerializedProgramAssetReferences(prefabPath);
+ }
+ }
+
+ private static void PopulatePrefabSerializedProgramAssetReferences(string prefabPath)
+ {
+ using(EditPrefabAssetScope editScope = new EditPrefabAssetScope(prefabPath))
+ {
+ if(!editScope.IsEditable)
+ {
+ return;
+ }
+
+ editScope.PrefabRoot.GetComponentsInChildren(prefabBehavioursTempList);
+ if(prefabBehavioursTempList.Count < 1)
+ {
+ return;
+ }
+
+ bool dirty = false;
+ foreach(UdonBehaviour udonBehaviour in prefabBehavioursTempList)
+ {
+ if(PopulateSerializedProgramAssetReference(udonBehaviour))
+ {
+ dirty = true;
+ }
+ }
+
+ if(dirty)
+ {
+ editScope.MarkDirty();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Scene Manager Callbacks
+
+ private void OnSceneOpened(Scene scene, OpenSceneMode mode)
+ {
+ RefreshQueuedProgramSources();
+ PopulateSceneSerializedProgramAssetReferences(scene);
+ }
+
+ private void OnSceneSaving(Scene scene, string _)
+ {
+ RefreshQueuedProgramSources();
+ PopulateSceneSerializedProgramAssetReferences(scene);
+ }
+
+ private static void PopulateSceneSerializedProgramAssetReferences(Scene scene)
+ {
+ if (!scene.IsValid())
+ {
+ return;
+ }
+
+ foreach(GameObject sceneGameObject in scene.GetRootGameObjects())
+ {
+ foreach(UdonBehaviour udonBehaviour in sceneGameObject.GetComponentsInChildren<UdonBehaviour>(true))
+ {
+ PopulateSerializedProgramAssetReference(udonBehaviour);
+ }
+ }
+ }
+
+ // Returns true if the serializedProgramProperty was changed, false otherwise.
+ private static bool PopulateSerializedProgramAssetReference(UdonBehaviour udonBehaviour)
+ {
+ SerializedObject serializedUdonBehaviour = new SerializedObject(udonBehaviour);
+ SerializedProperty programSourceSerializedProperty = serializedUdonBehaviour.FindProperty("programSource");
+ SerializedProperty serializedProgramAssetSerializedProperty = serializedUdonBehaviour.FindProperty("serializedProgramAsset");
+
+ if(!(programSourceSerializedProperty.objectReferenceValue is AbstractUdonProgramSource abstractUdonProgramSource))
+ {
+ return false;
+ }
+
+ if(abstractUdonProgramSource == null)
+ {
+ return false;
+ }
+
+ if(serializedProgramAssetSerializedProperty.objectReferenceValue == abstractUdonProgramSource.SerializedProgramAsset)
+ {
+ return false;
+ }
+
+ serializedProgramAssetSerializedProperty.objectReferenceValue = abstractUdonProgramSource.SerializedProgramAsset;
+ serializedUdonBehaviour.ApplyModifiedPropertiesWithoutUndo();
+ return true;
+
+ }
+
+ #endregion
+
+ #region PlayMode Callback
+
+ private void OnPlayModeStateChanged(PlayModeStateChange playModeStateChange)
+ {
+ if(playModeStateChange != PlayModeStateChange.ExitingEditMode)
+ {
+ return;
+ }
+
+ for(int index = 0; index < SceneManager.sceneCount; index++)
+ {
+ PopulateSceneSerializedProgramAssetReferences(SceneManager.GetSceneAt(index));
+ }
+ }
+
+ #endregion
+
+ #region IUdonEditorInterface Methods
+
+ public IUdonVM ConstructUdonVM()
+ {
+ return _udonEditorInterface.ConstructUdonVM();
+ }
+
+ public IUdonProgram Assemble(string assembly)
+ {
+ return _udonEditorInterface.Assemble(assembly);
+ }
+
+ public IUdonWrapper GetWrapper()
+ {
+ return _udonEditorInterface.GetWrapper();
+ }
+
+ public IUdonHeap ConstructUdonHeap()
+ {
+ return _udonEditorInterface.ConstructUdonHeap();
+ }
+
+ public IUdonHeap ConstructUdonHeap(uint heapSize)
+ {
+ return _udonEditorInterface.ConstructUdonHeap(heapSize);
+ }
+
+ public string CompileGraph(
+ IUdonCompilableGraph graph, INodeRegistry nodeRegistry,
+ out Dictionary<string, (string uid, string fullName, int index)> linkedSymbols,
+ out Dictionary<string, (object value, Type type)> heapDefaultValues
+ )
+ {
+ return _udonEditorInterface.CompileGraph(graph, nodeRegistry, out linkedSymbols, out heapDefaultValues);
+ }
+
+ public Type GetTypeFromTypeString(string typeString)
+ {
+ return _udonEditorInterface.GetTypeFromTypeString(typeString);
+ }
+
+ public void AddTypeResolver(IUAssemblyTypeResolver typeResolver)
+ {
+ _udonEditorInterface.AddTypeResolver(typeResolver);
+ }
+
+ public string[] DisassembleProgram(IUdonProgram program)
+ {
+ return _udonEditorInterface.DisassembleProgram(program);
+ }
+
+ public string DisassembleInstruction(IUdonProgram program, ref uint offset)
+ {
+ return _udonEditorInterface.DisassembleInstruction(program, ref offset);
+ }
+
+ public UdonNodeDefinition GetNodeDefinition(string identifier)
+ {
+ return _udonEditorInterface.GetNodeDefinition(identifier);
+ }
+
+ public IEnumerable<UdonNodeDefinition> GetNodeDefinitions()
+ {
+ return _udonEditorInterface.GetNodeDefinitions();
+ }
+
+ public Dictionary<string, INodeRegistry> GetNodeRegistries()
+ {
+ return _udonEditorInterface.GetNodeRegistries();
+ }
+
+ private IReadOnlyDictionary<string, ReadOnlyCollection<KeyValuePair<string, INodeRegistry>>> _topRegistries;
+
+ public IReadOnlyDictionary<string, ReadOnlyCollection<KeyValuePair<string, INodeRegistry>>> GetTopRegistries()
+ {
+ if (_topRegistries != null) return _topRegistries;
+
+ var topRegistries = new Dictionary<string, List<KeyValuePair<string, INodeRegistry>>>()
+ {
+ {"System", new List<KeyValuePair<string, INodeRegistry>>()},
+ {"Udon", new List<KeyValuePair<string, INodeRegistry>>()},
+ {"VRC", new List<KeyValuePair<string, INodeRegistry>>()},
+ {"UnityEngine", new List<KeyValuePair<string, INodeRegistry>>()},
+ };
+
+ // Go through each node registry and put it in the right parent registry
+ foreach (KeyValuePair<string, INodeRegistry> nodeRegistry in GetNodeRegistries())
+ {
+ if (nodeRegistry.Key.StartsWith("System"))
+ {
+ topRegistries["System"].Add(nodeRegistry);
+ }
+ else if (nodeRegistry.Key.StartsWith("Udon"))
+ {
+ topRegistries["Udon"].Add(nodeRegistry);
+ }
+ else if (nodeRegistry.Key.StartsWith("VRC"))
+ {
+ topRegistries["VRC"].Add(nodeRegistry);
+ }
+ else if (nodeRegistry.Key.StartsWith("UnityEngine"))
+ {
+ topRegistries["UnityEngine"].Add(nodeRegistry);
+ }
+ else if (nodeRegistry.Key.StartsWith("Cinemachine"))
+ {
+ topRegistries["UnityEngine"].Add(nodeRegistry);
+ }
+ else if (nodeRegistry.Key.StartsWith("TMPro"))
+ {
+ topRegistries["UnityEngine"].Add(nodeRegistry);
+ }
+ else
+ {
+ // Todo: note and handle these
+ UnityEngine.Debug.Log($"The Registry {nodeRegistry.Key} needs to be Added Somewhere");
+ }
+ }
+
+ // Save result as cached variable
+ _topRegistries = new ReadOnlyDictionary<string, ReadOnlyCollection<KeyValuePair<string, INodeRegistry>>>
+ (
+ topRegistries.ToDictionary(entry => entry.Key, entry => entry.Value.AsReadOnly())
+ );
+
+ // return cached version
+ return _topRegistries;
+ }
+
+ private Dictionary<string, INodeRegistry> _registryLookup;
+
+ private void CacheRegistryLookup()
+ {
+ _registryLookup = new Dictionary<string, INodeRegistry>();
+
+ foreach (KeyValuePair<string, INodeRegistry> topRegistry in GetNodeRegistries())
+ {
+ // save top-level registry. do we need to do this? probably not
+ _registryLookup.Add(topRegistry.Key, topRegistry.Value);
+
+ foreach (KeyValuePair<string, INodeRegistry> registry in topRegistry.Value.GetNodeRegistries())
+ {
+ _registryLookup.Add(registry.Key, registry.Value);
+ }
+ }
+ }
+
+ public bool TryGetRegistry(string name, out INodeRegistry registry)
+ {
+ if(_registryLookup == null)
+ {
+ CacheRegistryLookup();
+ }
+ return _registryLookup.TryGetValue(name, out registry);
+ }
+
+ public IEnumerable<UdonNodeDefinition> GetNodeDefinitions(string baseIdentifier)
+ {
+ return _udonEditorInterface.GetNodeDefinitions(baseIdentifier);
+ }
+
+ #endregion
+
+ #region Prefab Utilities
+
+ private static IEnumerable<string> GetAllPrefabAssetPaths()
+ {
+ return AssetDatabase.GetAllAssetPaths()
+ .Where(path => path.EndsWith(".prefab"))
+ .Where(path => path.StartsWith("Assets"));
+ }
+
+ private class EditPrefabAssetScope : IDisposable
+ {
+ private readonly string _assetPath;
+ private readonly GameObject _prefabRoot;
+ public GameObject PrefabRoot => _disposed ? null : _prefabRoot;
+
+ private readonly bool _isEditable;
+ public bool IsEditable => !_disposed && _isEditable;
+
+ private bool _dirty = false;
+ private bool _disposed;
+
+ public EditPrefabAssetScope(string assetPath)
+ {
+ _assetPath = assetPath;
+ _prefabRoot = PrefabUtility.LoadPrefabContents(_assetPath);
+ _isEditable = !PrefabUtility.IsPartOfImmutablePrefab(_prefabRoot);
+ }
+
+ public void MarkDirty()
+ {
+ _dirty = true;
+ }
+
+ public void Dispose()
+ {
+ if(_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if(_dirty)
+ {
+ try
+ {
+ PrefabUtility.SaveAsPrefabAsset(_prefabRoot, _assetPath);
+ }
+ catch(Exception e)
+ {
+ UnityEngine.Debug.LogError($"Failed to save changes to prefab at '{_assetPath}' due to exception '{e}'.");
+ }
+ }
+
+ PrefabUtility.UnloadPrefabContents(_prefabRoot);
+ }
+ }
+
+ #endregion
+ }
+}