562 lines
18 KiB
C#
562 lines
18 KiB
C#
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
|
|
}
|
|
}
|