summaryrefslogtreecommitdiff
path: root/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources
diff options
context:
space:
mode:
Diffstat (limited to 'VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources')
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes.meta8
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs18
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs57
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram.meta8
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs96
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs9
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs27
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram.meta8
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI.meta8
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView.meta8
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields.meta8
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs24
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs24
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs23
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs.meta3
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs24
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs24
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs23
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs.meta3
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs23
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs.meta3
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs23
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs.meta3
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs23
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs.meta3
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs23
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs.meta3
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs23
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements.meta8
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs81
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs86
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs156
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs220
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs48
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs213
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs57
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs1093
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes.meta8
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs30
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs.meta3
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs28
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs.meta3
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs42
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs.meta3
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs25
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs164
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs.meta3
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs450
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs6
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs122
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search.meta8
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs64
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs113
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs124
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs183
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs119
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs195
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs52
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs29
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs180
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs1392
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs29
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs144
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs58
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs324
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs58
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs224
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs60
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs212
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs200
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs637
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs210
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs21
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram.meta8
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs2080
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs.meta11
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs19
-rw-r--r--VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs.meta11
122 files changed, 10628 insertions, 0 deletions
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes.meta
new file mode 100644
index 00000000..1e05c23e
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 564cb755fc823d94d98948f3bb3f95cd
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs
new file mode 100644
index 00000000..17be4606
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace VRC.Udon.Editor.ProgramSources.Attributes
+{
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public class UdonProgramSourceNewMenuAttribute : Attribute
+ {
+ public Type Type { get; }
+ public string DisplayName { get; }
+
+ public UdonProgramSourceNewMenuAttribute(Type type, string displayName)
+ {
+ Type = type;
+ DisplayName = displayName;
+ }
+ }
+}
+
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs.meta
new file mode 100644
index 00000000..ea45c1be
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/Attributes/UdonProgramSourceNewMenuAttribute.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0e5ced9511d591140b191bbd9e948e61
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs
new file mode 100644
index 00000000..33ecd07f
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using UnityEditor;
+using VRC.Udon.Common.Interfaces;
+using VRC.Udon.ProgramSources;
+using VRC.Udon.Serialization.OdinSerializer;
+
+namespace VRC.Udon.Editor.ProgramSources
+{
+ [CustomEditor(typeof(SerializedUdonProgramAsset))]
+ public class SerializedUdonProgramAssetEditor : UnityEditor.Editor
+ {
+ private SerializedProperty _serializedProgramBytesStringSerializedProperty;
+ private SerializedProperty _serializationDataFormatSerializedProperty;
+
+ private void OnEnable()
+ {
+ _serializedProgramBytesStringSerializedProperty = serializedObject.FindProperty("serializedProgramBytesString");
+ _serializationDataFormatSerializedProperty = serializedObject.FindProperty("serializationDataFormat");
+ }
+
+ public override void OnInspectorGUI()
+ {
+ DrawSerializationDebug();
+ }
+
+ [Conditional("UDON_DEBUG")]
+ private void DrawSerializationDebug()
+ {
+ EditorGUILayout.LabelField($"DataFormat: {(DataFormat)_serializationDataFormatSerializedProperty.enumValueIndex}");
+
+ if(string.IsNullOrEmpty(_serializedProgramBytesStringSerializedProperty.stringValue))
+ {
+ return;
+ }
+
+ if(_serializationDataFormatSerializedProperty.enumValueIndex == (int)DataFormat.JSON)
+ {
+ using(new EditorGUI.DisabledScope(true))
+ {
+ EditorGUILayout.TextArea(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(_serializedProgramBytesStringSerializedProperty.stringValue)));
+ }
+ }
+ else
+ {
+ using(new EditorGUI.DisabledScope(true))
+ {
+ SerializedUdonProgramAsset serializedUdonProgramAsset = (SerializedUdonProgramAsset)target;
+ IUdonProgram udonProgram = serializedUdonProgramAsset.RetrieveProgram();
+ byte[] serializedBytes = SerializationUtility.SerializeValue(udonProgram, DataFormat.JSON, out List<UnityEngine.Object> _);
+ EditorGUILayout.TextArea(System.Text.Encoding.UTF8.GetString(serializedBytes));
+ }
+ }
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs.meta
new file mode 100644
index 00000000..e72c03a7
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/SerializedUdonProgramAssetEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e1b5b45f24b268b42826fc5c5497dc15
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram.meta
new file mode 100644
index 00000000..6fccff45
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2811cc384b684c04f9b647c597b55ff8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs
new file mode 100644
index 00000000..0045931e
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using UnityEditor;
+using UnityEngine;
+using VRC.Udon.Editor;
+using VRC.Udon.Editor.ProgramSources;
+using VRC.Udon.Editor.ProgramSources.Attributes;
+
+[assembly: UdonProgramSourceNewMenu(typeof(UdonAssemblyProgramAsset), "Udon Assembly Program Asset")]
+
+namespace VRC.Udon.Editor.ProgramSources
+{
+ [CreateAssetMenu(menuName = "VRChat/Udon/Udon Assembly Program Asset", fileName = "New Udon Assembly Program Asset")]
+ public class UdonAssemblyProgramAsset : UdonProgramAsset
+ {
+ [SerializeField]
+ protected string udonAssembly = "";
+
+ [SerializeField]
+ protected string assemblyError = null;
+
+ public delegate void AssembleDelegate(bool success, string assembly);
+ public event AssembleDelegate OnAssemble;
+
+ protected override void DrawProgramSourceGUI(UdonBehaviour udonBehaviour, ref bool dirty)
+ {
+ DrawAssemblyTextArea(!Application.isPlaying, ref dirty);
+ DrawAssemblyErrorTextArea();
+
+ base.DrawProgramSourceGUI(udonBehaviour, ref dirty);
+ }
+
+ protected override void RefreshProgramImpl()
+ {
+ AssembleProgram();
+ }
+
+ [PublicAPI]
+ protected virtual void DrawAssemblyTextArea(bool allowEditing, ref bool dirty)
+ {
+ EditorGUILayout.LabelField("Assembly Code", EditorStyles.boldLabel);
+ if(GUILayout.Button("Copy Assembly To Clipboard"))
+ {
+ EditorGUIUtility.systemCopyBuffer = udonAssembly;
+ }
+
+ EditorGUI.BeginChangeCheck();
+ using(new EditorGUI.DisabledScope(!allowEditing))
+ {
+ string newAssembly = EditorGUILayout.TextArea(udonAssembly);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ Undo.RecordObject(this, "Edit Assembly Program Code");
+ udonAssembly = newAssembly;
+ UdonEditorManager.Instance.QueueAndRefreshProgram(this);
+ }
+ }
+ }
+
+ [PublicAPI]
+ protected void DrawAssemblyErrorTextArea()
+ {
+ if(string.IsNullOrEmpty(assemblyError))
+ {
+ return;
+ }
+
+ EditorGUILayout.LabelField("Assembly Error", EditorStyles.boldLabel);
+ using(new EditorGUI.DisabledScope(true))
+ {
+ EditorGUILayout.TextArea(assemblyError);
+ }
+ }
+
+ [PublicAPI]
+ protected void AssembleProgram()
+ {
+ try
+ {
+ program = UdonEditorManager.Instance.Assemble(udonAssembly);
+ assemblyError = null;
+ OnAssemble?.Invoke(true, udonAssembly);
+ }
+ catch(Exception e)
+ {
+ program = null;
+ assemblyError = e.Message;
+ Debug.LogException(e);
+ OnAssemble?.Invoke(false, assemblyError);
+ }
+ }
+
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs.meta
new file mode 100644
index 00000000..c2d8a2b6
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAsset.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 22203902d63dec94194fefc3e155c43b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs
new file mode 100644
index 00000000..a6b6c856
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs
@@ -0,0 +1,9 @@
+using UnityEditor;
+
+namespace VRC.Udon.Editor.ProgramSources
+{
+ [CustomEditor(typeof(UdonAssemblyProgramAsset))]
+ public class UdonAssemblyProgramAssetEditor : UdonProgramAssetEditor
+ {
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs.meta
new file mode 100644
index 00000000..e7fb7fee
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3df823f3ab561fc43bcb81286e14b91d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs
new file mode 100644
index 00000000..409c2c68
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs
@@ -0,0 +1,27 @@
+using System.IO;
+using JetBrains.Annotations;
+using UnityEditor;
+using UnityEditor.Experimental.AssetImporters;
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources
+{
+ [ScriptedImporter(1, "uasm")]
+ [UsedImplicitly]
+ public class UdonAssemblyProgramAssetImporter : ScriptedImporter
+ {
+ public override void OnImportAsset(AssetImportContext ctx)
+ {
+ UdonAssemblyProgramAsset udonAssemblyProgramAsset = ScriptableObject.CreateInstance<UdonAssemblyProgramAsset>();
+ SerializedObject serializedUdonAssemblyProgramAsset = new SerializedObject(udonAssemblyProgramAsset);
+ SerializedProperty udonAssemblyProperty = serializedUdonAssemblyProgramAsset.FindProperty("udonAssembly");
+ udonAssemblyProperty.stringValue = File.ReadAllText(ctx.assetPath);
+ serializedUdonAssemblyProgramAsset.ApplyModifiedProperties();
+
+ udonAssemblyProgramAsset.RefreshProgram();
+
+ ctx.AddObjectToAsset("Imported Udon Assembly Program", udonAssemblyProgramAsset);
+ ctx.SetMainObject(udonAssemblyProgramAsset);
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs.meta
new file mode 100644
index 00000000..f3951da2
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonAssemblyProgram/UdonAssemblyProgramAssetImporter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3c0638314c289c24193b47d1c53c9fca
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram.meta
new file mode 100644
index 00000000..1f5f980a
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: fc78ce0a8f47f2642a2ed5fd39f05017
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI.meta
new file mode 100644
index 00000000..61c37e70
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d162e94e3ec124e4398f173057d3715b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView.meta
new file mode 100644
index 00000000..38782780
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 18fcefe274099824baeb479f629c1999
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields.meta
new file mode 100644
index 00000000..70a6e016
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3963ddfd2d404d449ab36d09da1a92bc
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs
new file mode 100644
index 00000000..76ab6a34
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs
@@ -0,0 +1,24 @@
+using System;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class ByteField : BaseField<byte>
+ {
+ public ByteField():base(null,null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create Char Editor and listen for changes
+ IntegerField field = new IntegerField();
+
+ field.RegisterValueChangedCallback(
+ e =>
+ value = Convert.ToByte(e.newValue));
+
+ Add(field);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs.meta
new file mode 100644
index 00000000..06d12e3c
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ByteField.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c66f97d5a5a38cb478fd4df11ece7be7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs
new file mode 100644
index 00000000..1850cbfc
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs
@@ -0,0 +1,24 @@
+using UnityEngine.UIElements;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class CharField : BaseField<char>
+ {
+ public CharField():base(null,null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create Char Editor and listen for changes
+ TextField field = new TextField
+ {
+ maxLength = 1
+ };
+ field.RegisterValueChangedCallback(
+ e =>
+ value = e.newValue.ToCharArray()[0]);
+
+ Add(field);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs.meta
new file mode 100644
index 00000000..f4388e8c
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/CharField.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 602ffcf431b3e4f41a18bd868751439a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs
new file mode 100644
index 00000000..d5d708b0
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs
@@ -0,0 +1,23 @@
+using System;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class DecimalField : BaseField<decimal>
+ {
+ public DecimalField():base(null,null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create Char Editor and listen for changes
+ DoubleField field = new DoubleField();
+ field.RegisterValueChangedCallback(
+ e =>
+ value = Convert.ToDecimal(e.newValue));
+
+ Add(field);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs.meta
new file mode 100644
index 00000000..feae7a68
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/DecimalField.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e79144dad56140a7bcd0d9f945153784
+timeCreated: 1632419615 \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs
new file mode 100644
index 00000000..51349917
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs
@@ -0,0 +1,24 @@
+using UnityEngine.UIElements;
+using UIElements = UnityEditor.UIElements;
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class LayerMaskField : BaseField<LayerMask>
+ {
+ public LayerMaskField() : base(null,null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create LayerMask Editor and listen for changes
+ UIElements.LayerMaskField field = new UIElements.LayerMaskField();
+ field.RegisterValueChangedCallback(e =>
+ {
+ this.value = e.newValue;
+ });
+
+ Add(field);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs.meta
new file mode 100644
index 00000000..67438a23
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/LayerMaskField.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 42d9183d7c7ce67448d1e010456e36f9
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs
new file mode 100644
index 00000000..13561b7c
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs
@@ -0,0 +1,24 @@
+using UnityEngine.UIElements;
+using UnityEditor.UIElements;
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class QuaternionField : BaseField<Quaternion>
+ {
+ public QuaternionField() :base(null, null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create Vector4 Editor and listen for changes
+ Vector4Field field = new Vector4Field();
+ field.RegisterValueChangedCallback(
+ e =>
+ value = new Quaternion(e.newValue.x, e.newValue.y, e.newValue.z, e.newValue.w)
+ );
+ Add(field);
+ }
+
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs.meta
new file mode 100644
index 00000000..fefc9cf3
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/QuaternionField.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1c96407d2b7698c4c8a0476efa2c765d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs
new file mode 100644
index 00000000..f0fa54df
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs
@@ -0,0 +1,23 @@
+using System;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class SByteField : BaseField<sbyte>
+ {
+ public SByteField():base(null,null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create Char Editor and listen for changes
+ IntegerField field = new IntegerField();
+ field.RegisterValueChangedCallback(
+ e =>
+ value = Convert.ToSByte(e.newValue));
+
+ Add(field);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs.meta
new file mode 100644
index 00000000..2e4769b0
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/SByteField.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 42747856e1884be6b1112c8838963662
+timeCreated: 1632419007 \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs
new file mode 100644
index 00000000..9d3a1140
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs
@@ -0,0 +1,23 @@
+using System;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class ShortField : BaseField<short>
+ {
+ public ShortField():base(null,null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create Char Editor and listen for changes
+ IntegerField field = new IntegerField();
+ field.RegisterValueChangedCallback(
+ e =>
+ value = Convert.ToInt16(e.newValue));
+
+ Add(field);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs.meta
new file mode 100644
index 00000000..72da7dfd
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/ShortField.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: bcda1561abdb40c69f9eeb9211bd3e3d
+timeCreated: 1632419462 \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs
new file mode 100644
index 00000000..77f5d696
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs
@@ -0,0 +1,23 @@
+using System;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class UnsignedIntegerField : BaseField<uint>
+ {
+ public UnsignedIntegerField():base(null,null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create Char Editor and listen for changes
+ IntegerField field = new IntegerField();
+ field.RegisterValueChangedCallback(
+ e =>
+ value = Convert.ToUInt32(e.newValue));
+
+ Add(field);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs.meta
new file mode 100644
index 00000000..516eafbb
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedIntegerField.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0b91b14ff1e24276863f173d0e9c760c
+timeCreated: 1632419145 \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs
new file mode 100644
index 00000000..81298eb1
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs
@@ -0,0 +1,23 @@
+using System;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class UnsignedLongField : BaseField<ulong>
+ {
+ public UnsignedLongField():base(null,null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create Char Editor and listen for changes
+ IntegerField field = new IntegerField();
+ field.RegisterValueChangedCallback(
+ e =>
+ value = Convert.ToUInt64(e.newValue));
+
+ Add(field);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs.meta
new file mode 100644
index 00000000..653f1a3b
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedLongField.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 1d1c8af690a94ef0a670e5d321733414
+timeCreated: 1632419341 \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs
new file mode 100644
index 00000000..6eac945f
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs
@@ -0,0 +1,23 @@
+using System;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class UnsignedShortField : BaseField<ushort>
+ {
+ public UnsignedShortField():base(null,null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create Char Editor and listen for changes
+ IntegerField field = new IntegerField();
+ field.RegisterValueChangedCallback(
+ e =>
+ value = Convert.ToUInt16(e.newValue));
+
+ Add(field);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs.meta
new file mode 100644
index 00000000..058ae560
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/UnsignedShortField.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 1387a4616a8f4c87bd0d55d2ffc021c8
+timeCreated: 1632419528 \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs
new file mode 100644
index 00000000..f6fb8307
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs
@@ -0,0 +1,23 @@
+using UnityEngine.UIElements;
+using System;
+using VRC.SDKBase;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public class VRCUrlField : BaseField<VRCUrl>
+ {
+ public VRCUrlField():base(null,null)
+ {
+ // Set up styling
+ AddToClassList("UdonValueField");
+
+ // Create Text Editor and listen for changes
+ TextField field = new TextField(50, false, false, Char.MinValue);
+ field.RegisterValueChangedCallback(
+ e =>
+ value = new VRCUrl(e.newValue)
+ );
+ Add(field);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs.meta
new file mode 100644
index 00000000..28d6b842
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Fields/VRCUrlField.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: eeb751ae1c234a04291c5039626f3470
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements.meta
new file mode 100644
index 00000000..4c7d4a8f
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: efe54a45d15688542911081f5876ca68
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs
new file mode 100644
index 00000000..55179e41
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs
@@ -0,0 +1,81 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEngine.UIElements;
+#else
+using UnityEngine.Experimental.UIElements;
+#endif
+using System;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView;
+
+#if UNITY_2019_3_OR_NEWER
+namespace UnityEditor.Experimental.GraphView
+#else
+namespace UnityEditor.Experimental.UIElements.GraphView
+#endif
+{
+ public static class GraphElementExtension
+ {
+
+ public static void Reload(this GraphElement element)
+ {
+ var evt = new Event()
+ {
+ type = EventType.ExecuteCommand,
+ commandName = UdonGraphCommands.Reload
+ };
+ using (var e = ExecuteCommandEvent.GetPooled(evt))
+ {
+ element.SendEvent(e);
+ }
+ }
+
+ public static void Compile(this GraphElement element)
+ {
+ var evt = new Event()
+ {
+ type = EventType.ExecuteCommand,
+ commandName = UdonGraphCommands.Compile
+ };
+ using (var e = ExecuteCommandEvent.GetPooled(evt))
+ {
+ element.SendEvent(e);
+ }
+ }
+
+ public static string GetUid(this GraphElement element)
+ {
+#if UNITY_2019_3_OR_NEWER
+ return element.viewDataKey;
+#else
+ return element.persistenceKey;
+#endif
+ }
+
+ public static void MarkDirty()
+ {
+ if (!EditorApplication.isPlaying)
+ {
+ EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
+ }
+ }
+
+ public static Vector2 GetSnappedPosition(Vector2 position)
+ {
+ // don't snap at 0 size
+ var snap = Settings.GridSnapSize;
+ if (snap == 0) return position;
+
+ position.x = (float)Math.Round(position.x / snap) * snap;
+ position.y = (float)Math.Round(position.y / snap) * snap;
+
+ return position;
+ }
+
+ public static Rect GetSnappedRect(Rect rect)
+ {
+ rect.position = GetSnappedPosition(rect.position);
+ return rect;
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs.meta
new file mode 100644
index 00000000..ebc4db20
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/GraphElementExtension.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 469db50616185d04e8a46dcd75db12d2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs
new file mode 100644
index 00000000..437334c0
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs
@@ -0,0 +1,86 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEngine.UIElements;
+#else
+using UnityEngine.Experimental.UIElements;
+#endif
+using System;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonArrayEditor : VisualElement
+ {
+ private IArrayProvider _inspector;
+ private Button _editArrayButton;
+ private Action<object> _setValueCallback;
+ private Type _type;
+ private object _value;
+ private bool _inspectorOpen = false;
+
+ public UdonArrayEditor(Type t, Action<object> valueChangedAction, object value)
+ {
+ _setValueCallback = valueChangedAction;
+ _value = value;
+ _type = t.GetElementType();
+
+ _editArrayButton = new Button(EditArray)
+ {
+ text = "Edit",
+ name = "array-editor",
+ };
+
+ Add(_editArrayButton);
+ }
+
+ private void EditArray()
+ {
+ if (_inspector == null)
+ {
+ // Create it new
+ Type typedArrayInspector = (typeof(UdonArrayInspector<>)).MakeGenericType(_type);
+ _inspector = (Activator.CreateInstance(typedArrayInspector, null, _value) as IArrayProvider);
+
+ AddInspector();
+ _inspectorOpen = true;
+ _editArrayButton.text = "Save";
+ return;
+ }
+ else
+ {
+ // Update Values when 'Save' is clicked
+ if(_inspectorOpen)
+ {
+ // Update Values
+ var values = _inspector.GetValues();
+ _setValueCallback(values);
+
+ // Remove Inspector
+ _inspector.RemoveFromHierarchy();
+
+ // Update Button Text
+ _editArrayButton.text = "Edit";
+ _inspectorOpen = false;
+ return;
+ }
+ else
+ {
+ // Inspector exists, it's just removed
+ _inspectorOpen = true;
+ AddInspector();
+ _editArrayButton.text = "Save";
+ }
+ }
+ }
+
+ private void AddInspector()
+ {
+ if (parent.GetType() == typeof(UdonPort))
+ {
+ parent.parent.Add(_inspector as VisualElement);
+ }
+ else
+ {
+ Add(_inspector as VisualElement);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs.meta
new file mode 100644
index 00000000..135a415d
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7f257a6eeae213a4db991d486cace003
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs
new file mode 100644
index 00000000..446950cc
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs
@@ -0,0 +1,156 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEngine.UIElements;
+using UnityEditor.UIElements;
+#else
+using UnityEngine.Experimental.UIElements;
+using UnityEditor.Experimental.UIElements;
+#endif
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonArrayInspector<T> : VisualElement, IArrayProvider
+ {
+ private ScrollView _scroller;
+ private List<VisualElement> _fields = new List<VisualElement>();
+ private IntegerField _sizeField;
+ private Action<object> _setValueCallback;
+
+ public UdonArrayInspector(Action<object> valueChangedAction, object value)
+ {
+ _setValueCallback = valueChangedAction;
+
+ AddToClassList("input-inspector");
+ var resizeContainer = new VisualElement()
+ {
+ name = "resize-container",
+ };
+ resizeContainer.Add(new Label("size"));
+
+ _sizeField = new IntegerField();
+ _sizeField.isDelayed = true;
+ #if UNITY_2019_3_OR_NEWER
+ _sizeField.RegisterValueChangedCallback((evt) =>
+ #else
+ _sizeField.OnValueChanged((evt) =>
+ #endif
+ {
+ ResizeTo(evt.newValue);
+ });
+ resizeContainer.Add(_sizeField);
+ Add(resizeContainer);
+
+ _scroller = new ScrollView()
+ {
+ name = "array-scroll",
+ };
+ Add(_scroller);
+
+ if (value == null)
+ {
+ // Can't show if array is empty
+ _sizeField.value = 0;
+ return;
+ }
+ else
+ {
+ IEnumerable<T> values = (value as IEnumerable<T>);
+ if (values == null)
+ {
+ Debug.LogError($"Couldn't convert {value} to {typeof(T).ToString()} Array");
+ return;
+ }
+
+ // Populate fields and their values from passed-in array
+ _fields = new List<VisualElement>();
+ foreach (var item in values)
+ {
+ var field = GetValueField(item);
+ _fields.Add(field);
+
+ _scroller.contentContainer.Add(field);
+ }
+
+ _sizeField.value = values.Count();
+ }
+
+ }
+
+ private void ResizeTo(int newValue)
+ {
+ _sizeField.value = newValue;
+
+ // Create from scratch if currentFields are null
+ if(_fields == null)
+ {
+ Debug.Log($"Creating from Scratch");
+ _fields = new List<VisualElement>();
+ for (int i = 0; i < newValue; i++)
+ {
+ var field = GetValueField(null);
+ _fields.Add(field);
+ _scroller.contentContainer.Add(field as VisualElement);
+ }
+ return;
+ }
+
+ // Shrink list if new value is less than old one
+ if(newValue < _fields.Count)
+ {
+ for (int i = _fields.Count - 1; i >= newValue; i--)
+ {
+ (_fields[i] as VisualElement).RemoveFromHierarchy();
+ _fields.RemoveAt(i);
+ }
+ MarkDirtyRepaint();
+ return;
+ }
+
+ // Expand list if new value is more than old one.
+ if(newValue > _fields.Count)
+ {
+ int numberToAdd = newValue - _fields.Count;
+ for (int i = 0; i < numberToAdd; i++)
+ {
+ var field = GetValueField(null);
+ if (field == null)
+ {
+ Debug.LogWarning($"Sorry, can't edit object of type {typeof(T).ToString()} yet.");
+ return;
+ }
+ _fields.Add(field);
+
+ _scroller.contentContainer.Add(field as VisualElement);
+ }
+ MarkDirtyRepaint();
+ return;
+ }
+ }
+
+ private VisualElement GetValueField(object value)
+ {
+ return UdonFieldFactory.CreateField(typeof(T), value, _setValueCallback);
+ }
+
+ public object GetValues()
+ {
+ var result = new List<T>();
+ for (int i = 0; i < _fields.Count; i++)
+ {
+ var f = (_fields[i] as INotifyValueChanged<T>);
+ result.Add(f.value);
+ }
+ return result.ToArray();
+ }
+
+ }
+
+ public interface IArrayProvider
+ {
+ object GetValues();
+ void RemoveFromHierarchy(); // in VisualElement
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs.meta
new file mode 100644
index 00000000..1a535cb2
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonArrayInspector.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f4f0ade55ae13b6468a765826f1f2540
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs
new file mode 100644
index 00000000..78ff9926
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs
@@ -0,0 +1,220 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+using UnityEngine.UIElements;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements.StyleSheets;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+using UnityEngine.Experimental.UIElements;
+using UnityEngine.Experimental.UIElements.StyleSheets;
+#endif
+using System;
+using UnityEditor;
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonComment : UdonGraphElement, IUdonGraphElementDataProvider
+ {
+ private VisualElement _mainContainer;
+ private Label _label;
+ private TextField _textField;
+ private CustomData _customData = new CustomData();
+ private UdonGraph _graph;
+ public UdonGroup group;
+
+ // Called from Context menu and Reload
+ public static UdonComment Create(string value, Rect position, UdonGraph graph)
+ {
+ var comment = new UdonComment("", graph);
+
+ // make sure rect size is not 0
+ position.width = position.width > 0 ? position.width : 128;
+ position.height = position.height > 0 ? position.height : 40;
+
+ comment._customData.layout = position;
+ comment._customData.title = value;
+
+ comment.UpdateFromData();
+ graph.MarkSceneDirty();
+
+ return comment;
+ }
+
+ public static UdonComment Create(UdonGraphElementData elementData, UdonGraph graph)
+ {
+ var comment = new UdonComment(elementData.jsonData, graph);
+
+ comment.UpdateFromData();
+ graph.MarkSceneDirty();
+
+ return comment;
+ }
+
+ private UdonComment(string jsonData, UdonGraph graph)
+ {
+ title = "Comment";
+ name = "comment";
+ _graph = graph;
+
+ capabilities |= Capabilities.Selectable | Capabilities.Movable | Capabilities.Deletable |
+ Capabilities.Resizable;
+ pickingMode = PickingMode.Ignore;
+
+ type = UdonGraphElementType.UdonComment;
+
+ if(!string.IsNullOrEmpty(jsonData))
+ {
+ EditorJsonUtility.FromJsonOverwrite(jsonData, _customData);
+ }
+
+ _mainContainer = new VisualElement();
+ _mainContainer.StretchToParentSize();
+ _mainContainer.AddToClassList("mainContainer");
+ Add(_mainContainer);
+
+ _label = new Label();
+ _label.RegisterCallback<MouseDownEvent>(OnLabelClick);
+ _mainContainer.Add(_label);
+
+ _textField = new TextField(1000, true, false, '*');
+ _textField.isDelayed = true;
+
+ // Support IME
+ _textField.RegisterCallback<FocusInEvent>(evt =>{ Input.imeCompositionMode = IMECompositionMode.On;});
+ _textField.RegisterCallback<FocusOutEvent>(evt =>
+ {
+ SetText(_textField.text);
+ Input.imeCompositionMode = IMECompositionMode.Auto;
+ SwitchToEditMode(false);
+ });
+
+#if UNITY_2019_3_OR_NEWER
+ _textField.RegisterValueChangedCallback((evt) =>
+#else
+ _textField.OnValueChanged((evt) =>
+#endif
+ {
+ SetText(evt.newValue);
+ SwitchToEditMode(false);
+ });
+ }
+
+ private void SaveNewData()
+ {
+ _graph.SaveGraphElementData(this);
+ }
+
+ private void UpdateFromData()
+ {
+ if(_customData != null)
+ {
+ layer = _customData.layer;
+ if(string.IsNullOrEmpty(_customData.uid))
+ {
+ _customData.uid = Guid.NewGuid().ToString();
+ }
+
+ uid = _customData.uid;
+
+ SetPosition(_customData.layout);
+ SetText(_customData.title);
+ }
+ }
+#if UNITY_2019_3_OR_NEWER
+ protected override void OnCustomStyleResolved(ICustomStyle style)
+ {
+ base.OnCustomStyleResolved(style);
+#else
+ protected override void OnStyleResolved(ICustomStyle style)
+ {
+ base.OnStyleResolved(style);
+#endif
+ // Something is forcing style! Resetting a few things here, grrr.
+
+ this.style.borderBottomWidth = 1;
+
+ var resizer = this.Q(null, "resizer");
+ if(resizer != null)
+ {
+ resizer.style.paddingTop = 0;
+ resizer.style.paddingLeft = 0;
+ }
+ }
+
+ public override void SetPosition(Rect newPos)
+ {
+ newPos = GraphElementExtension.GetSnappedRect(newPos);
+ base.SetPosition(newPos);
+ }
+
+ public override void UpdatePresenterPosition()
+ {
+ base.UpdatePresenterPosition();
+ _customData.layout = GraphElementExtension.GetSnappedRect(GetPosition());
+ SaveNewData();
+ if (group != null)
+ {
+ group.SaveNewData();
+ }
+ }
+
+ private double lastClickTime;
+ private const double doubleClickSpeed = 0.5;
+
+ private void OnLabelClick(MouseDownEvent evt)
+ {
+ var newTime = EditorApplication.timeSinceStartup;
+ if(newTime - lastClickTime < doubleClickSpeed)
+ {
+ SwitchToEditMode(true);
+ }
+
+ lastClickTime = newTime;
+ }
+
+ private void SwitchToEditMode(bool switchingToEdit)
+ {
+ if (switchingToEdit)
+ {
+ _mainContainer.Remove(_label);
+ _textField.value = _label.text;
+ _mainContainer.Add(_textField);
+ _textField.delegatesFocus = true;
+ _textField.Focus();
+ }
+ else
+ {
+ _mainContainer.Remove(_textField);
+ _mainContainer.Add(_label);
+ }
+
+ MarkDirtyRepaint();
+ }
+
+ public void SetText(string value)
+ {
+ Undo.RecordObject(_graph.graphProgramAsset, "Rename Comment");
+ value = value.TrimEnd();
+ _customData.title = value;
+ _label.text = value;
+ SaveNewData();
+ MarkDirtyRepaint();
+ }
+
+ public UdonGraphElementData GetData()
+ {
+ return new UdonGraphElementData(UdonGraphElementType.UdonComment, uid,
+ EditorJsonUtility.ToJson(_customData));
+ }
+
+ public class CustomData
+ {
+ public string uid;
+ public Rect layout;
+ public string title = "Comment";
+ public int layer;
+ public Color elementTypeColor;
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs.meta
new file mode 100644
index 00000000..2840e341
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonComment.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7e5916b8dd19e4445a9156a457b82ee4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs
new file mode 100644
index 00000000..a6419359
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs
@@ -0,0 +1,48 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+#endif
+using System;
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonGraphElement : GraphElement
+ {
+#if UNITY_2019_3_OR_NEWER
+ public string uid { get => viewDataKey; set => viewDataKey = value; }
+#else
+ public string uid { get => persistenceKey; set => persistenceKey = value; }
+#endif
+ internal UdonGraphElementType type = UdonGraphElementType.GraphElement;
+
+ internal UdonGraphElement()
+ {
+ }
+ }
+
+ public interface IUdonGraphElementDataProvider
+ {
+ UdonGraphElementData GetData();
+ }
+
+ [Serializable]
+ public class GraphRect
+ {
+ public float x;
+ public float y;
+ public float width;
+ public float height;
+
+ public GraphRect(Rect input)
+ {
+ this.x = Mathf.Round(input.x);
+ this.y = Mathf.Round(input.y);
+ this.width = Mathf.Round(input.width);
+ this.height = Mathf.Round(input.height);
+ }
+
+ public Rect rect => new Rect(this.x, this.y, this.width, this.height);
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs.meta
new file mode 100644
index 00000000..1137de20
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGraphElement.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ba3ecc4c46929404d8c2ec920743b823
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs
new file mode 100644
index 00000000..d6c91a73
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs
@@ -0,0 +1,213 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+using UnityEngine.UIElements;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+using UnityEngine.Experimental.UIElements;
+#endif
+using System;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonGroup : Group, IUdonGraphElementDataProvider
+ {
+ public string uid { get => _uid; set => _uid = value; }
+ private CustomData _customData = new CustomData();
+ private UdonGraph _graph;
+ private string _uid;
+ private const int GROUP_LAYER = -1;
+
+ public static UdonGroup Create(string value, Rect position, UdonGraph graph)
+ {
+ var group = new UdonGroup("", graph);
+
+ group.uid = Guid.NewGuid().ToString();
+
+ // make sure rect size is not 0
+ position.width = position.width > 0 ? position.width : 128;
+ position.height = position.height > 0 ? position.height : 128;
+
+ group._customData.uid = group.uid;
+ group._customData.layout = position;
+ group._customData.title = value;
+
+ return group;
+ }
+
+ // Called in Reload > RestoreElementFromData
+ public static UdonGroup Create(UdonGraphElementData elementData, UdonGraph graph)
+ {
+ return new UdonGroup(elementData.jsonData, graph);
+ }
+
+ // current order of operations issue when creating a group from the context menu means this isn't set until first save. This allows us to force it.
+ public void UpdateDataId()
+ {
+ _customData.uid = uid;
+ }
+
+ // Build a Group from jsonData, save to userData
+ private UdonGroup(string jsonData, UdonGraph graph)
+ {
+ title = "Group";
+ _graph = graph;
+ layer = GROUP_LAYER;
+
+ if (!string.IsNullOrEmpty(jsonData))
+ {
+ EditorJsonUtility.FromJsonOverwrite(jsonData, _customData);
+ }
+
+ // listen for changes on child elements
+ RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
+ }
+
+ private void OnGeometryChanged(GeometryChangedEvent evt)
+ {
+ _customData.layout = GraphElementExtension.GetSnappedRect(GetPosition());
+ }
+
+ public void Initialize()
+ {
+ if (_customData != null)
+ {
+ // Propagate data to useful places
+ title = _customData.title;
+ layer = _customData.layer;
+ if (string.IsNullOrEmpty(_customData.uid))
+ {
+ _customData.uid = Guid.NewGuid().ToString();
+ }
+
+ uid = _customData.uid;
+
+ // Add all elements from graph to self
+ var childUIDs = _customData.containedElements;
+ if (childUIDs.Count > 0)
+ {
+ foreach (var item in childUIDs)
+ {
+ GraphElement element = _graph.GetElementByGuid(item);
+ if (element != null)
+ {
+ if (ContainsElement(element)) continue;
+ AddElement(element);
+ if (element is UdonComment c)
+ {
+ c.group = this;
+ }
+ else if (element is UdonNode n)
+ {
+ n.group = this;
+ }
+ }
+ }
+ }
+ else
+ {
+ // No children, so restore the saved position
+ SetPosition(_customData.layout);
+ }
+ }
+ }
+
+ public override void SetPosition(Rect newPos)
+ {
+ newPos = GraphElementExtension.GetSnappedRect(newPos);
+ base.SetPosition(newPos);
+ }
+
+ public void SaveNewData()
+ {
+ _graph.SaveGraphElementData(this);
+ }
+
+ // Save data to asset after new position set
+ public override void UpdatePresenterPosition()
+ {
+ base.UpdatePresenterPosition();
+ Rect layout = GraphElementExtension.GetSnappedRect(GetPosition());
+ base.SetPosition(layout);
+ SaveNewData();
+ }
+
+ // Save data to asset after rename
+ protected override void OnGroupRenamed(string oldName, string newName)
+ {
+ // limit name to 100 characters
+ title = newName.Substring(0, Mathf.Min(newName.Length, 100));
+ _customData.title = title;
+ SaveNewData();
+ }
+
+ protected override void OnElementsAdded(IEnumerable<GraphElement> elements)
+ {
+ base.OnElementsAdded(elements);
+ foreach (var element in elements)
+ {
+ if (!_customData.containedElements.Contains(element.GetUid()))
+ {
+ _customData.containedElements.Add(element.GetUid());
+ }
+
+ // Set group variable on UdonNodes
+ if (element is UdonNode)
+ {
+ (element as UdonNode).group = this;
+ element.BringToFront();
+ }
+
+ if (element is UdonComment)
+ {
+ (element as UdonComment).group = this;
+ }
+ }
+ SaveNewData();
+ }
+
+ protected override void OnElementsRemoved(IEnumerable<GraphElement> elements)
+ {
+ base.OnElementsRemoved(elements);
+ foreach (var element in elements)
+ {
+ if (_customData.containedElements.Contains(element.GetUid()))
+ {
+ _customData.containedElements.Remove(element.GetUid());
+ if (element is UdonNode)
+ {
+ (element as UdonNode).group = null;
+ }
+ else if (element is UdonComment)
+ {
+ (element as UdonComment).group = null;
+ }
+ }
+ }
+
+ SaveNewData();
+ }
+
+ public override bool AcceptsElement(GraphElement element, ref string reasonWhyNotAccepted)
+ {
+ return base.AcceptsElement(element, ref reasonWhyNotAccepted);
+ }
+
+ public UdonGraphElementData GetData()
+ {
+ return new UdonGraphElementData(UdonGraphElementType.UdonGroup, uid, EditorJsonUtility.ToJson(_customData));
+ }
+
+ public class CustomData
+ {
+ public string uid;
+ public Rect layout;
+ public List<string> containedElements = new List<string>();
+ public string title;
+ public int layer;
+ public Color elementTypeColor;
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs.meta
new file mode 100644
index 00000000..291451b5
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonGroup.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1b8045222a10ce04b815642b9cd5ca17
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs
new file mode 100644
index 00000000..c6fec3e9
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs
@@ -0,0 +1,57 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+#endif
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonMinimap : MiniMap, IUdonGraphElementDataProvider
+ {
+ private CustomData _customData = new CustomData();
+ private UdonGraph _graph;
+
+ public UdonMinimap(UdonGraph graph)
+ {
+ _graph = graph;
+
+ name = "UdonMap";
+ maxWidth = 200;
+ maxHeight = 100;
+ anchored = false;
+ SetPosition(_customData.layout);
+ }
+
+ public void SetVisible(bool value)
+ {
+ visible = value;
+ _customData.visible = value;
+ _graph.SaveGraphElementData(this);
+ }
+
+ public override void UpdatePresenterPosition()
+ {
+ _customData.layout = GetPosition();
+ _graph.SaveGraphElementData(this);
+ }
+
+ public UdonGraphElementData GetData()
+ {
+ return new UdonGraphElementData(UdonGraphElementType.Minimap, this.GetUid(), JsonUtility.ToJson(_customData));
+ }
+
+ internal void LoadData(UdonGraphElementData data)
+ {
+ JsonUtility.FromJsonOverwrite(data.jsonData, _customData);
+ SetPosition(_customData.layout);
+ visible = _customData.visible;
+ }
+
+ public class CustomData
+ {
+ public bool visible = true;
+ public Rect layout = new Rect(new Vector2(10, 20), Vector2.zero);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs.meta
new file mode 100644
index 00000000..dc27107b
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonMinimap.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b006d67642298f04e895b6709ef12429
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs
new file mode 100644
index 00000000..c690b6af
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs
@@ -0,0 +1,1093 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+using EditorGV = UnityEditor.Experimental.GraphView;
+using EngineUI = UnityEngine.UIElements;
+using EditorUI = UnityEditor.UIElements;
+using UnityEngine.UIElements;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+using EditorGV = UnityEditor.Experimental.UIElements.GraphView;
+using EngineUI = UnityEngine.Experimental.UIElements;
+using EditorUI = UnityEditor.Experimental.UIElements;
+using UnityEngine.Experimental.UIElements;
+#endif
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes;
+using VRC.Udon.Graph;
+using VRC.Udon.Graph.Interfaces;
+using VRC.Udon.Serialization;
+using VRC.Udon.Serialization.OdinSerializer.Utilities;
+using Random = UnityEngine.Random;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonNode : Node, IEdgeConnectorListener
+ {
+ // name is inherited from parent VisualElement class
+ public Type type;
+ public GameObject gameObject;
+ protected UdonGraph _graphView;
+ private EditorUI.PopupField<string> _popup;
+ public UdonNodeDefinition definition;
+ public UdonNodeData data;
+ public Dictionary<int, UdonPort> portsIn;
+ public Dictionary<int, UdonPort> portsOut;
+ public List<UdonPort> portsFlowIn;
+ public List<UdonPort> portsFlowOut;
+ private INodeRegistry _registry;
+ public UdonGroup group;
+
+ // Overload handling
+ private IList<UdonNodeDefinition> overloadDefinitions;
+
+ private readonly Dictionary<UdonNodeDefinition, string> _optionNameCache =
+ new Dictionary<UdonNodeDefinition, string>();
+
+ private readonly Dictionary<UdonNodeDefinition, string> _cleanerOptionNameCache =
+ new Dictionary<UdonNodeDefinition, string>();
+
+
+ public bool IsVariableNode => _variableNodeType != VariableNodeType.None;
+
+ public UdonGraph Graph
+ {
+ get => _graphView;
+ private set { }
+ }
+
+ public INodeRegistry Registry
+ {
+ get => _registry;
+ private set { }
+ }
+
+ private readonly string[] _specialFlows =
+ {
+ "Block",
+ "Branch",
+ "For",
+ "Foreach",
+ "While",
+ "Is_Valid",
+ };
+
+ protected static readonly Dictionary<string, Type> DefinitionToTypeLookup = new Dictionary<string, Type>()
+ {
+ {"VRCUdonCommonInterfacesIUdonEventReceiver.__GetProgramVariable__SystemString__SystemObject", typeof(GetOrSetProgramVariableNode)},
+ {"VRCUdonCommonInterfacesIUdonEventReceiver.__SetProgramVariable__SystemString_SystemObject__SystemVoid", typeof(GetOrSetProgramVariableNode)},
+ {"VRCUdonCommonInterfacesIUdonEventReceiver.__GetProgramVariableType__SystemString__SystemType", typeof(GetOrSetProgramVariableNode)},
+ {"VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEvent__SystemString__SystemVoid", typeof(SendCustomEventNode)},
+ {"VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEventDelayedSeconds__SystemString_SystemSingle_VRCUdonCommonEnumsEventTiming__SystemVoid", typeof(SendCustomEventNode)},
+ {"VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEventDelayedFrames__SystemString_SystemInt32_VRCUdonCommonEnumsEventTiming__SystemVoid", typeof(SendCustomEventNode)},
+ {"VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomNetworkEvent__VRCUdonCommonInterfacesNetworkEventTarget_SystemString__SystemVoid", typeof(SendCustomEventNode)},
+ {"Set_ReturnValue", typeof(SetReturnValueNode)},
+ {"Set_Variable", typeof(SetVariableNode)},
+ };
+
+#if UNITY_2019_3_OR_NEWER
+ public string uid
+ {
+ get => viewDataKey;
+ set => viewDataKey = value;
+ }
+#else
+ public string uid { get => persistenceKey; set => persistenceKey = value; }
+#endif
+
+ // Called when creating from Asset, calls the CreateNode method below
+ public static UdonNode CreateNode(UdonNodeData nodeData, UdonGraph view)
+ {
+ UdonNodeDefinition definition = UdonEditorManager.Instance.GetNodeDefinition(nodeData.fullName);
+ if (definition == null)
+ {
+ Debug.LogError($"Cannot create node {nodeData.fullName} because there is no matching Node Definition");
+ return null;
+ }
+
+ return CreateNode(definition, view, nodeData);
+ }
+
+ // Always called when creating UdonNode
+ public static UdonNode CreateNode(UdonNodeDefinition definition, UdonGraph view, UdonNodeData nodeData = null)
+ {
+ Type type = typeof(UdonNode);
+ // overwrite type with target type if it exists
+ if (DefinitionToTypeLookup.TryGetValue(definition.fullName, out Type childType))
+ {
+ type = childType;
+ }
+ UdonNode node = Activator.CreateInstance(type, definition, view, nodeData) as UdonNode;
+ node?.Initialize();
+ return node;
+ }
+
+ private bool skipSubtitle = false;
+
+ private Label subtitle;
+ // Constructor is protected to force all paths through Static factory method except for child classes
+ public UdonNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null)
+ {
+ _graphView = view;
+ definition = nodeDefinition;
+ Undo.RecordObject(view.graphProgramAsset, "Add UdonNode");
+ var registry = UdonGraphExtensions.GetRegistryForDefinition(nodeDefinition);
+ if(registry != null)
+ {
+ this._registry = registry;
+ }
+ else
+ {
+ Debug.LogWarning($"Couldn't find registry for {nodeDefinition.fullName}");
+ }
+
+ VisualElement titleContainer = new VisualElement()
+ {
+ name = "title-container",
+ };
+ this.Q("title").Insert(0, titleContainer);
+
+ titleContainer.Add(this.Q("title-label"));
+
+ subtitle = new Label("")
+ {
+ name = "subtitle",
+ };
+ skipSubtitle = (
+ _specialFlows.Contains(definition.fullName)
+ || definition.fullName.EndsWith("et_Variable")
+ || definition.fullName.StartsWithCached("Const_")
+ );
+
+ if (!skipSubtitle)
+ {
+ titleContainer.Insert(0, subtitle);
+ }
+
+ name = definition.fullName;
+ elementTypeColor = Random.ColorHSV(0.5f, 0.6f, 0.1f, 0.2f, 0.8f, 0.9f);
+
+ // Null is a type here, so handle it special
+ if (nodeDefinition.type == null)
+ {
+ AddToClassList("null");
+ }
+ else
+ {
+ AddToClassList(definition.type.Namespace);
+ AddToClassList(definition.type.Name);
+ }
+
+ if (nodeDefinition.fullName.Contains('_'))
+ {
+ AddToClassList(definition.fullName.Substring(0, nodeDefinition.fullName.IndexOf('_')));
+ }
+
+ // Create or validate nodeData
+ if (nodeData == null)
+ {
+ data = _graphView.graphData.AddNode(definition.fullName);
+ PopulateDefaultValues();
+ ValidateNodeData();
+ }
+ else
+ {
+ data = nodeData;
+ ValidateNodeData();
+ SetPosition(new Rect(data.position.x, data.position.y, 0, 0));
+ }
+
+ uid = data.uid;
+
+ // Fill in all fields, etc and add to the graph view
+ if (UdonGraphExtensions.ShouldShowDocumentationLink(definition))
+ {
+ DrawHelpButton();
+ }
+
+ // Show overloads for nodes EXCEPT type, those have too many entries and break Unity UI
+ if (!nodeDefinition.fullName.StartsWith("Type_"))
+ {
+ RefreshOverloadPopup();
+ }
+
+ AddToClassList("UdonNode");
+
+ LayoutPorts();
+ }
+
+ public virtual void Initialize()
+ {
+ RefreshTitle();
+ _graphView.MarkSceneDirty();
+ }
+
+ private string GetTargetVariableUid()
+ {
+ string result = "";
+ if (IsVariableNode && this.data.nodeValues.Length > 0)
+ {
+ string[] parts = data.nodeValues[0].stringValue.Split('|');
+ if (parts.Length > 1)
+ {
+ result = parts[1];
+ }
+ }
+ return result;
+ }
+
+ public void RefreshTitle()
+ {
+ if (IsVariableNode)
+ {
+ string uid = GetTargetVariableUid();
+ if (!string.IsNullOrWhiteSpace(uid))
+ {
+ string variableName = _graphView.GetVariableName(uid);
+ if (!string.IsNullOrWhiteSpace(variableName))
+ {
+ switch (_variableNodeType)
+ {
+ case VariableNodeType.Set:
+ title = $"Set {variableName}";
+ break;
+ case VariableNodeType.Change:
+ title = $"{variableName} Change";
+ break;
+ case VariableNodeType.Get:
+ default:
+ title = variableName;
+ break;
+ }
+ return;
+ }
+ }
+ }
+ // Set Title
+ var displayTitle = UdonGraphExtensions.PrettyString(definition.name).FriendlyNameify();
+ if (displayTitle == "Const_VRCUdonCommonInterfacesIUdonEventReceiver")
+ {
+ displayTitle = "UdonBehaviour";
+ }
+ else if(displayTitle == "==" || displayTitle == "!=" || displayTitle == "+")
+ {
+ displayTitle = $"{definition.type.Name} {displayTitle}";
+ }
+
+ if (displayTitle.StartsWith("Op "))
+ displayTitle = displayTitle.Replace("Op ", "");
+
+ title = displayTitle;
+
+ AddToClassList(title.Replace(" ", "").ToLowerFirstChar());
+
+ if (definition == null)
+ {
+ Debug.LogWarning($"Definition for {this.name} is null");
+ return;
+ }
+
+ string className = definition.name.Split(' ').FirstOrDefault().Split('_').FirstOrDefault();
+ AddToClassList(className);
+
+ if (!skipSubtitle)
+ {
+ if (definition.fullName.StartsWith("Event_"))
+ {
+ subtitle.text = "Event";
+ }
+ // make Constructor nodes readable
+ else if (definition.name == "ctor")
+ {
+ subtitle.text = definition.type.Name;
+ title = "Constructor";
+ }
+ else
+ {
+ subtitle.text = className;
+ // temp title shenanigans
+ int firstSplit = definition.fullName.IndexOf("__") + 2;
+ if (firstSplit > 1)
+ {
+ int lastSplit = definition.fullName.IndexOf("__", firstSplit);
+ int stringLength = (lastSplit > -1)
+ ? lastSplit - firstSplit
+ : definition.fullName.Length - firstSplit;
+ string line2 = definition.fullName.Substring(firstSplit, stringLength).Replace("_", " ")
+ .UppercaseFirst();
+ if (line2.StartsWith("Op "))
+ {
+ line2 = line2.Replace("Op ", "");
+ subtitle.text = definition.type.Name;
+ }
+
+ title = line2;
+ }
+ else
+ {
+ //TODO: handle class names not found
+ //Debug.Log($"Couldn't find classname for {nodeDefinition.fullName}");
+ }
+ }
+ }
+ }
+
+ private void DrawHelpButton()
+ {
+ Button helpButton = new Button(ShowNodeDocs)
+ {
+ name = "help-button",
+ };
+ helpButton.Add(new TextElement()
+ {
+ name = "icon",
+ text = "?"
+ });
+ titleButtonContainer.Add(helpButton);
+ }
+
+ private void ShowNodeDocs()
+ {
+ string url = UdonGraphExtensions.GetDocumentationLink(definition);
+ if (!string.IsNullOrEmpty(url))
+ {
+ Help.BrowseURL(url);
+ }
+ }
+
+ public override void SetPosition(Rect newPos)
+ {
+ newPos.position = GraphElementExtension.GetSnappedPosition(newPos.position);
+ base.SetPosition(newPos);
+ data.position = newPos.position;
+ }
+
+ public override void UpdatePresenterPosition()
+ {
+ base.UpdatePresenterPosition();
+ if (group != null)
+ {
+ group.SaveNewData();
+ }
+ }
+
+ public void RefreshOverloadPopup()
+ {
+ // Get overloads, draw them if we have more than one signature for this method
+ overloadDefinitions = CacheOverloads();
+ if (overloadDefinitions != null && overloadDefinitions.Count > 1)
+ {
+ // Get index of currently selected (could cache this on node instead)
+ // TODO: switch to just reading this from Popup, which probably stores it
+ int currentIndex = 0;
+ for (int i = 0; i < overloadDefinitions.Count; i++)
+ {
+ if (overloadDefinitions.ElementAt(i).fullName != name)
+ {
+ continue;
+ }
+
+ currentIndex = i;
+ break;
+ }
+
+ // Build dropdown list
+ List<string> options = new List<string>();
+ for (int i = 0; i < overloadDefinitions.Count; i++)
+ {
+ UdonNodeDefinition nodeDefinition = overloadDefinitions.ElementAt(i);
+ if (!_optionNameCache.TryGetValue(nodeDefinition, out string optionName))
+ {
+ optionName = nodeDefinition.fullName;
+ // don't add overload types that take pointers, not supported
+ string[] splitOptionName = optionName.Split(new[] { "__" }, StringSplitOptions.None);
+ if (splitOptionName.Length >= 3)
+ {
+ optionName = $"({splitOptionName[2].Replace("_", ", ")})";
+ }
+
+ optionName = optionName.FriendlyNameify();
+ _optionNameCache.Add(nodeDefinition, optionName);
+ }
+
+ if (!_cleanerOptionNameCache.TryGetValue(nodeDefinition, out string cleanerOptionName))
+ {
+ cleanerOptionName =
+ optionName.Replace("UnityEngine", "").Replace("System", "").Replace("Variable_", "");
+ _cleanerOptionNameCache.Add(nodeDefinition, cleanerOptionName);
+ }
+
+ options.Add(cleanerOptionName);
+ // optionName is what was used as the tooltip. Do we need the tooltip?
+ }
+
+ // Clear out old one
+ if (inputContainer.Contains(_popup))
+ {
+ inputContainer.Remove(_popup);
+ }
+
+ _popup = new EditorUI.PopupField<string>(options, currentIndex);
+#if UNITY_2019_3_OR_NEWER
+ _popup.RegisterValueChangedCallback(
+#else
+ _popup.OnValueChanged(
+#endif
+ (e) =>
+ {
+ // TODO - store data in the dropdown and use formatListItemCallback?
+ SetNewFullName(overloadDefinitions.ElementAt(_popup.index).fullName);
+ });
+ inputContainer.Add(_popup);
+ }
+ }
+
+ private void SetNewFullName(string newFullName)
+ {
+ data.fullName = newFullName;
+ definition = UdonEditorManager.Instance.GetNodeDefinition(data.fullName);
+ data.Resize(definition.Inputs.Count);
+ // Todo: see if we can get rid of this reload. Tried ValidateNodeData,LayoutPorts,RestoreConnections but noodles were left hanging
+ this.Reload();
+ }
+
+ private List<UdonNodeDefinition> CacheOverloads()
+ {
+ string baseIdentifier = name;
+ string[] splitBaseIdentifier = baseIdentifier.Split(new[] { "__" }, StringSplitOptions.None);
+ if (splitBaseIdentifier.Length >= 2)
+ {
+ baseIdentifier = $"{splitBaseIdentifier[0]}__{splitBaseIdentifier[1]}__";
+ }
+
+ if (baseIdentifier.StartsWithCached("Const_"))
+ {
+ return null;
+ }
+
+ if (baseIdentifier.StartsWithCached("Type_"))
+ {
+ baseIdentifier = "Type_";
+ }
+
+ if (baseIdentifier.StartsWithCached("Variable_"))
+ {
+ baseIdentifier = "Variable_";
+ }
+
+ // This used to be cached on graph instead of calculated per-node
+ // TODO: cache this somewhere, maybe UdonEditorManager? Is that worth it for performance?
+ IEnumerable<UdonNodeDefinition> matchingNodeDefinitions =
+ UdonEditorManager.Instance.GetNodeDefinitions(baseIdentifier);
+
+ var result = new List<UdonNodeDefinition>();
+ foreach (var definition in matchingNodeDefinitions)
+ {
+ // don't add definitions with pointer parameters, not supported in Udon
+ if (!definition.fullName.Contains('*'))
+ {
+ result.Add(definition);
+ }
+ }
+
+ return result;
+ }
+
+ internal void RestoreConnections()
+ {
+ RestoreInputs();
+ RestoreFlows();
+ }
+
+ private void RestoreFlows()
+ {
+ for (int i = 0; i < data.flowUIDs.Length; i++)
+ {
+ // skip if flow uid is empty
+ string nodeUID = data.flowUIDs[i];
+ if (string.IsNullOrEmpty(nodeUID))
+ {
+ continue;
+ }
+
+ // Find connected node via Graph
+ UdonNode connectedNode = _graphView.GetNodeByGuid(nodeUID) as UdonNode;
+ if (connectedNode == null)
+ {
+ Debug.Log($"Couldn't find node with GUID {nodeUID}, clearing data");
+ data.flowUIDs[i] = "";
+ continue;
+ }
+
+ // Trying to move a Block's flow that was left at the end to the beginning
+ if (portsFlowOut != null && i >= portsFlowOut.Count)
+ {
+ Debug.LogWarning(
+ $"Trying to restore flow to {connectedNode.name} from a non-existent port, skipping");
+
+ for (int j = 0; j < data.flowUIDs.Length; j++)
+ {
+ bool didRestoreFlow = false;
+ if (string.IsNullOrEmpty(data.flowUIDs[j]))
+ {
+ data.flowUIDs[j] = data.flowUIDs[i];
+ data.flowUIDs[i] = "";
+ didRestoreFlow = true;
+ }
+
+ if (didRestoreFlow)
+ {
+ RestoreFlows();
+ }
+ }
+
+ continue;
+ }
+
+ UdonPort sourcePort = null;
+ // Edge case, but its possible that this is null in broken graphs
+ // Skip if we can't find the source port
+ if (portsFlowOut != null)
+ {
+ sourcePort = portsFlowOut.Count > 1 ? portsFlowOut[i] : portsFlowOut.FirstOrDefault();
+ if (sourcePort == null)
+ {
+ Debug.LogError($"Failed to find output flow port for node {uid}");
+ // clear the flow uid, user will have to reconnect by hand
+ data.flowUIDs[i] = "";
+ continue;
+ }
+ }
+ else
+ {
+ Debug.LogError($"Failed to find output flow port for node {uid}");
+ // clear the flow uid, user will have to reconnect by hand
+ data.flowUIDs[i] = "";
+ continue;
+ }
+
+
+ UdonPort destPort = null;
+ // Edge case, but its possible that this is null in broken graphs
+ if(connectedNode.portsFlowIn != null)
+ {
+ destPort = connectedNode.portsFlowIn.FirstOrDefault();
+ if (destPort == null)
+ {
+ Debug.LogError($"Failed to find input flow port node node {nodeUID}");
+ // clear the flow uid, user will have to reconnect by hand
+ data.flowUIDs[i] = "";
+ continue;
+ }
+ }
+ else
+ {
+ Debug.LogError($"Failed to find input flow port node node {nodeUID}");
+ // clear the flow uid, user will have to reconnect by hand
+ data.flowUIDs[i] = "";
+ continue;
+ }
+
+ // Passed the tests! ready to connect
+ var edge = sourcePort.ConnectTo(destPort);
+ edge.AddToClassList("flow");
+ _graphView.AddElement(edge);
+ }
+
+ }
+
+ private void RestoreInputs()
+ {
+ for (int i = 0; i < definition.Inputs.Count; i++)
+ {
+ // Skip to next input if we don't have a node to check at this index
+ if (data.nodeUIDs.Length <= i)
+ {
+ continue;
+ }
+
+ // Skip to next input if we have a bad node reference
+ if (string.IsNullOrEmpty(data.nodeUIDs[i]))
+ {
+ continue;
+ }
+
+ // get otherIndex. not 100% sure what this refers to yet, maybe a port index?
+ string[] splitUID = data.nodeUIDs[i].Split('|');
+ string nodeUID = splitUID[0];
+ int otherIndex = 0;
+ if (splitUID.Length > 1)
+ {
+ otherIndex = int.Parse(splitUID[1]);
+ }
+
+ // Skip if we don't have a good uid for the other node
+ if (string.IsNullOrEmpty(nodeUID))
+ {
+ continue;
+ }
+
+ // Find connected node via Graph
+ UdonNode connectedNode = _graphView.GetNodeByGuid(nodeUID) as UdonNode;
+ if (connectedNode == null)
+ {
+ Debug.Log($"Couldn't find node with GUID {nodeUID}");
+ data.nodeUIDs[i] = "";
+ continue;
+ }
+
+ // No matching port for this data, skip
+ if (portsIn == null) continue;
+ if (!portsIn.TryGetValue(i, out UdonPort destPort))
+ {
+ Debug.LogError($"Failed to find input data slot (index {i}) for node {uid} {data.fullName}");
+ continue;
+ }
+
+ // Copied from Legacy, not sure what conditions would cause this
+ if (otherIndex < 0 || connectedNode?.portsOut.Keys.Count <= otherIndex)
+ {
+ otherIndex = 0;
+ }
+
+ // skip if we can't find the sourcePort - comment better once you understand what this is exactly
+ if (connectedNode == null || !connectedNode.portsOut.TryGetValue(otherIndex, out UdonPort sourcePort))
+ {
+ Debug.LogError($"Failed to find output data slot for node {nodeUID}");
+ continue;
+ }
+
+ // Passed the tests! ready to connect
+ var edge = sourcePort.ConnectTo(destPort);
+ _graphView.AddElement(edge);
+ }
+ }
+
+ // Legacy, haven't gone through yet
+ void ValidateNodeData()
+ {
+ // set data to this graph
+ data.SetGraph(_graphView.graphData);
+
+ for (int i = 0; i < data.nodeValues.Length; i++)
+ {
+ if (definition.Inputs.Count <= i)
+ {
+ continue;
+ }
+
+ Type expectedType = definition.Inputs[i].type;
+
+ // Skip over if the value is null and that's ok
+ if (data.nodeValues[i] == null)
+ {
+ if (expectedType == null || Nullable.GetUnderlyingType(expectedType) != null)
+ {
+ continue;
+ }
+ else
+ {
+ data.nodeValues[i] = SerializableObjectContainer.Serialize(default, expectedType);
+ continue;
+ }
+ }
+
+ object value = data.nodeValues[i].Deserialize();
+ if (value == null)
+ {
+ if (expectedType == null || Nullable.GetUnderlyingType(expectedType) != null)
+ {
+ // type is nullable, leave it alone
+ continue;
+ }
+ else
+ {
+ // not a nullable type - set a default
+ data.nodeValues[i] = SerializableObjectContainer.Serialize(default, expectedType);
+ }
+ }
+
+ if (!expectedType.IsInstanceOfType(value))
+ {
+ data.nodeValues[i] = SerializableObjectContainer.Serialize(null, expectedType);
+ }
+ }
+ }
+
+ void PopulateDefaultValues()
+ {
+ // No default values so I'm just...making them?
+ int count = definition.Inputs.Count;
+
+ data.nodeValues = new SerializableObjectContainer[count];
+ data.nodeUIDs = new string[count];
+ for (int i = 0; i < count; i++)
+ {
+ object value = definition.defaultValues.Count > i ? definition.defaultValues[i] : default;
+ data.nodeValues[i] = SerializableObjectContainer.Serialize(value, definition.Inputs[i].type);
+ }
+ }
+
+ private enum VariableNodeType
+ {
+ Get,
+ Set,
+ None,
+ Change,
+ };
+
+ private VariableNodeType _variableNodeType = VariableNodeType.None;
+
+
+ public virtual void LayoutPorts()
+ {
+ ClearPorts();
+ SetupFlowPorts();
+
+ // Don't setup in ports for Get_Variable node types, instead add variable popup
+ if (name.CompareTo("Get_Variable") == 0)
+ {
+ _variableNodeType = VariableNodeType.Get;
+ RefreshVariablePopup();
+ }
+ else if (name.CompareTo("Event_OnVariableChange") == 0)
+ {
+ _variableNodeType = VariableNodeType.Change;
+ }
+ else
+ {
+ // Add Variable popup and in-ports for Set_Variable
+ if (name.CompareTo("Set_Variable") == 0)
+ {
+ _variableNodeType = VariableNodeType.Set;
+ RefreshVariablePopup();
+ }
+
+ SetupInPorts();
+ }
+
+ SetupOutPorts();
+
+ RefreshExpandedState();
+ RefreshPorts();
+ }
+
+ public void ClearPorts()
+ {
+ portsFlowIn?.ForEach(port => port.RemoveFromHierarchy());
+ portsFlowOut?.ForEach(port => port.RemoveFromHierarchy());
+ portsIn?.Values.ForEach(port => port.RemoveFromHierarchy());
+ portsOut?.Values.ForEach(port => port.RemoveFromHierarchy());
+ }
+
+ private EditorUI.PopupField<string> _variablePopupField;
+ // TODO: Test this again after we have the new graph serializing the addition of nodes
+ public void RefreshVariablePopup()
+ {
+ if (_variableNodeType == VariableNodeType.None)
+ {
+ Debug.LogError($"Not Creating Variable Pop-Up for Non Variable Node {data.fullName}");
+ }
+
+ // Legacy method of determining currently selected index
+ // TODO: upgrade this logic path from the legacy method of determining Variable indices
+ // Get Variable nodes only have one value, get it and deserialize
+
+ var value = data.nodeValues[0].Deserialize();
+
+ var options = _graphView.GetVariableNames.Where(t => !t.StartsWith("__")).ToList();
+
+ // Get value of selected node in rather roundabout way
+ int originalIndex = _graphView.GetVariableNodes
+ .IndexOf(_graphView.GetVariableNodes.FirstOrDefault(v => v.uid == (string)value));
+
+ // Allow OnVariableChange events to start with just any existing index
+ if (_variableNodeType == VariableNodeType.Change && originalIndex == -1) originalIndex = 0;
+
+ int currentIndex = _graphView.GetVariableNames.FindIndex(s => s == _graphView.GetVariableNames[originalIndex]);
+
+ if (currentIndex < 0)
+ {
+ Debug.LogWarning($"Node {name} didn't have a variable assigned, removing");
+ _graphView.RemoveNodeAndData(data);
+ return;
+ }
+
+ // Create popup, set current value and set function to update data when it's changed.
+ if (_variablePopupField == null)
+ {
+ // First time creating, just add it
+ _variablePopupField = new EditorUI.PopupField<string>(options, currentIndex);
+ inputContainer.Add(_variablePopupField);
+ }
+ else
+ {
+ // Remaking it - remove the old one, at the new one at its previous location
+ int index = inputContainer.IndexOf(_variablePopupField);
+ _variablePopupField.RemoveFromHierarchy();
+ _variablePopupField = new EditorUI.PopupField<string>(options, currentIndex);
+ inputContainer.Insert(index, _variablePopupField);
+ }
+#if UNITY_2019_3_OR_NEWER
+ _variablePopupField.RegisterValueChangedCallback(
+#else
+ _variablePopupField.OnValueChanged(
+#endif
+ (e) =>
+ {
+ // Ensure we've selected an existing variable
+ if (_variablePopupField.index < options.Count)
+ {
+ int trueIndex = _graphView.GetVariableNames.FindIndex(s => s == _variablePopupField.text);
+
+ // not currently using event value, which is variable name. Instead using legacy method of comparing index to graph variable nodes array index
+ string newUid = _graphView.GetVariableNodes[trueIndex].uid;
+ // Get Variable nodes only have one entry, so index is 0 below
+ SetNewValue(newUid, 0);
+
+ this.Reload(); // Didn't want to do this, but can't get flows to restore otherwise Ideally, we call RefreshTitle(), LayoutPorts(), RestoreConnections(), RestoreFlows()
+ }
+ });
+
+ string startingUid = _graphView.GetVariableNodes[originalIndex].uid;
+ SetNewValue(startingUid, 0);
+ }
+
+ public void SetNewValue(object newValue, int index, Type inType = null)
+ {
+ data.nodeValues[index] = SerializableObjectContainer.Serialize(newValue, inType);
+ }
+
+ private void SetupOutPorts()
+ {
+ portsOut = new Dictionary<int, UdonPort>();
+ for (int i = 0; i < definition.Outputs.Count; i++)
+ {
+ var item = definition.Outputs[i];
+
+ // Convert object type to variable type for Get_Variable nodes, or run them through the SlotTypeConverter for all other nodes
+ Type type = (_variableNodeType == VariableNodeType.Get || _variableNodeType == VariableNodeType.Change)
+ ? GetTypeForDefinition(definition)
+ : UdonGraphExtensions.SlotTypeConverter(item.type, definition.fullName);
+
+ string label = UdonGraphExtensions.FriendlyTypeName(type).FriendlyNameify();
+ if (label == "IUdonEventReceiver")
+ {
+ label = "UdonBehaviour";
+ }
+
+ if (item.name != null) label = $"{label} {item.name}";
+ UdonPort port = (UdonPort) UdonPort.Create(label, Direction.Output, this, type, data, i);
+ outputContainer.Add(port);
+ portsOut[i] = port;
+ }
+ }
+
+ private void SetupInPorts()
+ {
+ portsIn = new Dictionary<int, UdonPort>();
+
+ // Expand node data to hold values for all inputs
+ data.Resize(definition.Inputs.Count);
+
+ int startIndex = 0;
+ // Skip first input for Set_Variable since that's the eventName which is set via dropdown
+ if (name.CompareTo("Set_Variable") == 0)
+ {
+ startIndex = 1;
+ }
+ // Skip first input for Set_ReturnValue since that's the special variable
+ if (name.CompareTo("Set_ReturnValue") == 0)
+ {
+ startIndex = 1;
+ }
+
+ // Skip inputs for Null and This nodes
+ if (name.Contains("Const_Null") || name.Contains("Const_This"))
+ {
+ return;
+ }
+
+ for (int index = startIndex; index < definition.Inputs.Count; index++)
+ {
+ UdonNodeParameter input = definition.Inputs[index];
+ string label = "";
+ // TODO: Ask Cubed what this does? Or figure it out.
+ if (definition.Inputs.Count > index && index >= 0)
+ {
+ label = definition.Inputs[index].name;
+ }
+
+ if (label == "IUdonEventReceiver")
+ {
+ label = "UdonBehaviour";
+ }
+
+ label = label.FriendlyNameify();
+ string typeName = UdonGraphExtensions.FriendlyTypeName(input.type);
+
+ // skip over types with pointers. Should remove these from included overloads in the first place!
+ if (typeName.Contains('*'))
+ {
+ continue;
+ }
+
+ // Convert object type to variable type for Set_Variable nodes, or run them through the SlotTypeConverter for all other nodes
+ Type type = (_variableNodeType == VariableNodeType.Set && index == 1)
+ ? type = GetTypeForDefinition(definition)
+ : UdonGraphExtensions.SlotTypeConverter(input.type, definition.fullName);
+
+ if (_variableNodeType == VariableNodeType.Set && index == 2)
+ {
+ AddToClassList("send-change");
+ }
+
+ // not 100% sure if I should use label or typeName here
+ UdonPort p = UdonPort.Create(label, Direction.Input, this, type, data, index) as UdonPort;
+ inputContainer.Add(p);
+ portsIn.Add(index, p);
+ }
+ }
+
+ private Type GetTypeForDefinition(UdonNodeDefinition udonNodeDefinition)
+ {
+ string targetUid = data.nodeValues[0].Deserialize().ToString();
+ UdonNodeData varData = _graphView.GetVariableNodes.Where(n => n.uid == targetUid).FirstOrDefault();
+ if (varData != null)
+ {
+ var targetDefinition = UdonEditorManager.Instance.GetNodeDefinition(varData.fullName);
+ if (targetDefinition != null)
+ {
+ return UdonGraphExtensions.SlotTypeConverter(targetDefinition.type, udonNodeDefinition.fullName);
+ }
+ }
+
+ // if we fail, return generic object type
+ return typeof(object);
+ }
+
+ private void SetupFlowPorts()
+ {
+ if (definition.flow)
+ {
+ portsFlowIn = new List<UdonPort>();
+ portsFlowOut = new List<UdonPort>();
+
+ string label = "";
+
+ int inFlowIndex = -1;
+ int outFlowIndex = -1;
+ // don't add input flow for events, they're called from above
+ if (!definition.fullName.StartsWith("Event_"))
+ {
+ label = definition.inputFlowNames.Count > 0 ? definition.inputFlowNames[0] : "";
+ AddFlowPort(Direction.Input, label, ++inFlowIndex);
+ }
+
+ // add output flow
+ label = definition.outputFlowNames.Count > 0 ? definition.outputFlowNames[0] : "";
+ AddFlowPort(Direction.Output, label, ++outFlowIndex);
+ if (_specialFlows.Contains(definition.fullName))
+ {
+ label = definition.outputFlowNames.Count > 1 ? definition.outputFlowNames[1] : "";
+ AddFlowPort(Direction.Output, label, ++outFlowIndex);
+ }
+
+ // Add the number of output flows we need for a Block
+ if (definition.fullName == "Block")
+ {
+ data.flowUIDs = data.flowUIDs.Where(f => !string.IsNullOrEmpty(f)).ToArray();
+ int connectedFlows = data.flowUIDs.Length;
+ if (connectedFlows > 1)
+ {
+ for (int i = 0; i < connectedFlows - 1; i++)
+ {
+ AddFlowPort(Direction.Output, "", ++outFlowIndex);
+ }
+ }
+ }
+ }
+ }
+
+ private void AddFlowPort(Direction d, string label, int index)
+ {
+ UdonPort p = (UdonPort) UdonPort.Create(label, d, this, null, data, index);
+ p.AddToClassList("flow");
+ if(d == Direction.Input)
+ {
+ inputContainer.Add(p);
+ portsFlowIn.Add(p);
+ }
+ else
+ {
+ outputContainer.Add(p);
+ portsFlowOut.Add(p);
+ }
+ }
+
+ private bool HasRecursiveFlow(Port fromSlot, Port toSlot)
+ {
+ // No need to check connections to value slots
+ if (toSlot.portType != null) return false;
+
+ // Check out ports of node being connected TO
+ foreach (var port in (toSlot.node as UdonNode).portsFlowOut)
+ {
+ // if any of its ports connect to fromSlot, it's recursive. using foreach for convenience, should be just one edge
+ foreach (var edge in port.connections)
+ {
+ // if this connection goes to the node that started this all, then it's recursion
+ if(edge.input.node == fromSlot.node)
+ {
+ return true;
+ }
+
+ // Need to run this recursively to check all ports
+ if(HasRecursiveFlow(fromSlot, edge.input))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ #region IEdgeConnectorListener
+
+ public void OnDrop(EditorGV.GraphView graphView, Edge edge)
+ {
+ if (edge.output != null && edge.input != null && !HasRecursiveFlow(edge.output, edge.input))
+ {
+ edge.output.Connect(edge);
+ edge.input.Connect(edge);
+ graphView.AddElement(edge);
+
+ // Reload block nodes after new connections
+ if(definition.fullName == "Block")
+ {
+ RestoreFlows();
+ }
+ }
+ }
+
+ public void OnDropOutsidePort(Edge edge, Vector2 position)
+ {
+ if (!Settings.SearchOnNoodleDrop) return;
+
+ if (edge.output != null && edge.output.portType != null)
+ {
+ _graphView.OpenPortSearch(edge.output.portType, position, edge.output as UdonPort, Direction.Input);
+ }
+ else if (edge.input != null && edge.input.portType != null)
+ {
+ _graphView.OpenPortSearch(edge.input.portType, position, edge.input as UdonPort, Direction.Output);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs.meta
new file mode 100644
index 00000000..426e8041
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNode.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: dcd657bc1dcf357448d27bcfa8c5dc36
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes.meta
new file mode 100644
index 00000000..99b3dc3d
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 50be104b42303f24fa6166d0c5b542f1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs
new file mode 100644
index 00000000..1521f75c
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs
@@ -0,0 +1,30 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+using EngineUI = UnityEngine.UIElements;
+using EditorUI = UnityEditor.UIElements;
+#else
+using EditorGV = UnityEditor.Experimental.UIElements.GraphView;
+using EngineUI = UnityEngine.Experimental.UIElements;
+using EditorUI = UnityEditor.Experimental.UIElements;
+#endif
+using VRC.Udon.Graph;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes
+{
+ public class GetOrSetProgramVariableNode : UdonNode
+ {
+ private EditorUI.PopupField<string> _programVariablePopup;
+
+ public GetOrSetProgramVariableNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null) :
+ base(nodeDefinition, view, nodeData)
+ {
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _programVariablePopup =
+ this.GetProgramPopup(UdonNodeExtensions.ProgramPopupType.Variables, _programVariablePopup);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs.meta
new file mode 100644
index 00000000..243a80e5
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/GetOrSetProgramVariableNode.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: cbfa6b1c2cf44feca09853837fc740bb
+timeCreated: 1623890922 \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs
new file mode 100644
index 00000000..1d8b75ef
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs
@@ -0,0 +1,28 @@
+#if UNITY_2019_3_OR_NEWER
+using EditorGV = UnityEditor.Experimental.GraphView;
+using EngineUI = UnityEngine.UIElements;
+using EditorUI = UnityEditor.UIElements;
+#else
+using EditorGV = UnityEditor.Experimental.UIElements.GraphView;
+using EngineUI = UnityEngine.Experimental.UIElements;
+using EditorUI = UnityEditor.Experimental.UIElements;
+#endif
+using VRC.Udon.Graph;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes
+{
+ public class SendCustomEventNode : UdonNode
+ {
+ private EditorUI.PopupField<string> _eventNamePopup;
+
+ public SendCustomEventNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null) : base(nodeDefinition, view, nodeData)
+ {
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _eventNamePopup = this.GetProgramPopup(UdonNodeExtensions.ProgramPopupType.Events, _eventNamePopup);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs.meta
new file mode 100644
index 00000000..a71b401c
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SendCustomEventNode.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8164fc2c5c5b43428503cf064e8b53f0
+timeCreated: 1624411228 \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs
new file mode 100644
index 00000000..30691476
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs
@@ -0,0 +1,42 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+using EditorGV = UnityEditor.Experimental.GraphView;
+using EngineUI = UnityEngine.UIElements;
+using EditorUI = UnityEditor.UIElements;
+using UnityEngine.UIElements;
+#else
+using EditorGV = UnityEditor.Experimental.UIElements.GraphView;
+using EngineUI = UnityEngine.Experimental.UIElements;
+using EditorUI = UnityEditor.Experimental.UIElements;
+#endif
+using System.Linq;
+using UnityEngine;
+using VRC.Udon.Graph;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes
+{
+ public class SetReturnValueNode : UdonNode
+ {
+ public SetReturnValueNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null) : base(nodeDefinition, view, nodeData)
+ {
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ string returnVariable = UdonBehaviour.ReturnVariableName;
+ string uuid = null;
+
+ if (!_graphView.GetVariableNames.Contains(returnVariable))
+ uuid = _graphView.AddNewVariable("Variable_SystemObject", returnVariable, false);
+ else
+ uuid = _graphView.GetVariableNodes.FirstOrDefault(n => (string)n.nodeValues[1].Deserialize() == returnVariable)?.uid;
+
+ if (!string.IsNullOrWhiteSpace(uuid))
+ SetNewValue(uuid, 0);
+ else
+ Debug.LogError("Could not find return value name!");
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs.meta
new file mode 100644
index 00000000..6c3ac32b
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetReturnValueNode.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 6e65118bfe2d43f1ad2412dba47c21ee
+timeCreated: 1623892831 \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs
new file mode 100644
index 00000000..97176a9e
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs
@@ -0,0 +1,25 @@
+using UnityEngine;
+using UnityEngine.UIElements;
+using VRC.Udon.Graph;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes
+{
+ public class SetVariableNode : UdonNode
+ {
+ public SetVariableNode(UdonNodeDefinition nodeDefinition, UdonGraph view, UdonNodeData nodeData = null) : base(nodeDefinition, view, nodeData)
+ {
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ UdonPort sendChangePort = portsIn[2];
+ var toggle = sendChangePort.Q<Toggle>();
+ if (toggle != null)
+ {
+ sendChangePort.Insert(0, toggle);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs.meta
new file mode 100644
index 00000000..21ef0abe
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/SetVariableNode.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fd9209fd8a363ee42a49a2afaeb35805
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs
new file mode 100644
index 00000000..c650e567
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs
@@ -0,0 +1,164 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+using EditorGV = UnityEditor.Experimental.GraphView;
+using EngineUI = UnityEngine.UIElements;
+using EditorUI = UnityEditor.UIElements;
+using UnityEngine.UIElements;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+using EditorGV = UnityEditor.Experimental.UIElements.GraphView;
+using EngineUI = UnityEngine.Experimental.UIElements;
+using EditorUI = UnityEditor.Experimental.UIElements;
+using UnityEngine.Experimental.UIElements;
+#endif
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+using VRC.Udon.Common;
+using VRC.Udon.Compiler.Compilers;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView.UdonNodes
+{
+ public static class UdonNodeExtensions
+ {
+
+ #region Methods for creating popup selectors from Program variables / event
+ public static readonly HashSet<string> InternalEventNames = new HashSet<string>()
+ {
+ "_start", "_update", "_lateUpdate", "_fixedUpdate", "onAnimatorIk", "_onAnimatorMove", "_onBecameInvisible", "_onBecameVisible",
+ "_onPlayerCollisionEnter", "_onCollisionEnter", "_onCollisionEnter2D", "_onPlayerCollisionExit", "_onCollisionExit", "_onCollisionExit2D", "_onPlayerCollisionStay", "_onCollisionStay", "_onCollisionStay2D",
+ "_onPlayerTriggerEnter", "_onTriggerEnter", "_onTriggerEnter2D", "_onPlayerTriggerExit", "_onTriggerExit", "_onTriggerExit2D", "_onPlayerTriggerStay", "_onTriggerStay", "_onTriggerStay2D",
+ "_onDestroy", "_onDisable", "_onDrawGizmos", "_onDrawGizmosSelected", "_onEnable", "_onJointBreak", "_onJointBreak2D", "_onMouseDown", "_onMouseDrag", "_onMouseEnter", "_onMouseExit", "_onMouseOver", "_onMouseUp", "_onMouseUpAsButton",
+ "_onPlayerParticleCollision", "_onParticleCollision", "_onParticleTrigger", "_onPostRender", "_onPreCull", "_onPreRender", "_onRenderImage", "_onRenderObject", "_onTransformChildrenChanged", "_onTransformParentChanged", "_onValidate", "_onWillRenderObject",
+ "_interact", "_onDrop", "_onPickup", "_onPickupUseDown", "_onPickupUseUp", "_onPreSerialization", "_onPostSerialization", "_onDeserialization", "_onVideoEnd", "_onVideoPause", "_onVideoPlay", "_onVideoStart", "_midiNoteOn", "_midiNoteOff", "_midiControlChange",
+ "_onOwnershipRequest", "_onNetworkReady", "_onOwnershipTransferred", "_onPlayerJoined", "_onPlayerLeft", "_onSpawn", "_onStationEntered", "_onStationExited",
+ };
+
+ public enum ProgramPopupType
+ {
+ Variables, Events
+ }
+
+ private static List<string> GetCustomEventsFromAsset(AbstractSerializedUdonProgramAsset asset)
+ {
+ // don't return internal event names or VariableChange events
+ return asset.RetrieveProgram().EntryPoints.GetExportedSymbols().Where(e =>
+ !InternalEventNames.Contains(e) && !e.StartsWithCached(VariableChangedEvent.EVENT_PREFIX)).ToList();
+ }
+
+ public static EditorUI.PopupField<string> GetProgramPopup(this UdonNode node, ProgramPopupType popupType, EditorUI.PopupField<string> _eventNamePopup)
+ {
+ string PLACEHOLDER = "----";
+ string MISSING = "MISSING! Was";
+
+ List<string> _options = new List<string>(){PLACEHOLDER};
+ var data = node.data;
+
+ bool unavailable = true;
+ if(data.nodeUIDs.Length < 1 || string.IsNullOrEmpty(data.nodeUIDs[0]))
+ {
+ switch (popupType)
+ {
+ case ProgramPopupType.Events:
+ _options = GetCustomEventsFromAsset(node.Graph.graphProgramAsset.SerializedProgramAsset);
+ break;
+ case ProgramPopupType.Variables:
+ node.Graph.RefreshVariables(false);
+ _options = new List<string>(node.Graph.GetVariableNames).Where(x=>!x.StartsWithCached(VariableChangedEvent.OLD_VALUE_PREFIX)).ToList();
+ break;
+ }
+ unavailable = _options.Count == 0;
+ _options.Insert(0, PLACEHOLDER);
+ }
+ else if(data.InputNodeAtIndex(0)?.fullName == "Get_Variable")
+ {
+ // So much work to get the underlying node referenced by a variable. Would be nice to have a method for this.
+ var parts = data.nodeUIDs[0].Split('|');
+ if (parts.Length < 1) return null;
+
+ string targetId = parts[0];
+
+ var variableGetterNode = node.Graph.graphData.FindNode(targetId);
+ if (variableGetterNode == null || variableGetterNode.nodeValues.Length < 1) return null;
+
+ string variableId = variableGetterNode.nodeValues[0].Deserialize() as string;
+ if (string.IsNullOrWhiteSpace(variableId)) return null;
+
+ string variableName = node.Graph.GetVariableName(variableId);
+ if (string.IsNullOrWhiteSpace(variableName)) return null;
+
+ if (node.Graph.udonBehaviour != null && node.Graph.udonBehaviour.publicVariables.TryGetVariableValue(variableName, out UdonBehaviour ub))
+ {
+ if (ub != null)
+ {
+ switch (popupType)
+ {
+ case ProgramPopupType.Events:
+ _options = GetCustomEventsFromAsset(ub.programSource.SerializedProgramAsset);
+ break;
+ case ProgramPopupType.Variables:
+ _options = ub.programSource?.SerializedProgramAsset?.RetrieveProgram()?.SymbolTable
+ .GetSymbols().Where(s => !s.StartsWithCached(UdonGraphCompiler.INTERNAL_VARIABLE_PREFIX) && !s.StartsWithCached(VariableChangedEvent.OLD_VALUE_PREFIX)).ToList();
+ break;
+ }
+ _options.Insert(0, PLACEHOLDER);
+ unavailable = false;
+ }
+ }
+ }
+
+
+ int currentIndex = 0;
+ int targetNodeValueIndex = node.data.fullName.Contains("SendCustomNetworkEvent") ? 2 : 1;
+ string targetVarName = data.nodeValues[targetNodeValueIndex].Deserialize() as string;
+ if (targetVarName != null && targetVarName.StartsWithCached(MISSING)) targetVarName = null;
+
+ // If we have a target variable name:
+ if (!string.IsNullOrWhiteSpace(targetVarName))
+ {
+ if (_options.Contains(targetVarName))
+ {
+ currentIndex = _options.IndexOf(targetVarName);
+ }
+ else
+ {
+ _options[0] = unavailable ? targetVarName : $"{MISSING} {targetVarName}";
+ }
+ }
+
+ if (_eventNamePopup == null)
+ {
+ _eventNamePopup = new EditorUI.PopupField<string>(_options, currentIndex);
+ _eventNamePopup.name = popupType == ProgramPopupType.Events ? "EventNamePopup" : "VariablePopup";
+ var eventNamePort = node.inputContainer.Q(null, popupType == ProgramPopupType.Events ? "eventName" : "symbolName");
+ eventNamePort?.Add(_eventNamePopup);
+ if (unavailable)
+ {
+ _eventNamePopup.SetEnabled(false);
+ }
+ }
+ else
+ {
+ // Remaking it - remove the old one, at the new one at its previous location
+ int index = node.inputContainer.IndexOf(_eventNamePopup);
+ _eventNamePopup.RemoveFromHierarchy();
+ _eventNamePopup = new EditorUI.PopupField<string>(_options, currentIndex);
+ node.inputContainer.Insert(index, _eventNamePopup);
+ }
+#if UNITY_2019_3_OR_NEWER
+ _eventNamePopup.RegisterValueChangedCallback(
+#else
+ _eventNamePopup.OnValueChanged(
+#endif
+ (e) =>
+ {
+ node.SetNewValue(e.newValue.CompareTo(PLACEHOLDER) == 0 ? "" : e.newValue.ToString(), targetNodeValueIndex);
+ // Todo: update text field directly and save instead of calling Reload
+ node.Reload();
+ });
+
+ return _eventNamePopup;
+ }
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs.meta
new file mode 100644
index 00000000..93534521
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonNodes/UdonNodeExtensions.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 00b770580aae40ffb9f6a1d898b52269
+timeCreated: 1625678192 \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs
new file mode 100644
index 00000000..8b808489
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs
@@ -0,0 +1,450 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+using UnityEngine.UIElements;
+using EditorUI = UnityEditor.UIElements;
+using EngineUI = UnityEngine.UIElements;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+using UnityEngine.Experimental.UIElements;
+using EditorUI = UnityEditor.Experimental.UIElements;
+using EngineUI = UnityEngine.Experimental.UIElements;
+#endif
+using System;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using VRC.Udon.Graph;
+using VRC.Udon.Serialization;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ [Serializable]
+ public class UdonPort : Port
+ {
+ public string FullName;
+ private UdonNodeData _udonNodeData;
+ private int _nodeValueIndex;
+
+ private VisualElement _inputField;
+ private VisualElement _inputFieldTypeLabel;
+
+ private IArrayProvider _inspector;
+
+ protected UdonPort(Orientation portOrientation, Direction portDirection, Capacity portCapacity, Type type) :
+ base(portOrientation, portDirection, portCapacity, type)
+ {
+ }
+
+ public static Port Create(string portName, Direction portDirection, IEdgeConnectorListener connectorListener,
+ Type type, UdonNodeData data, int index, Orientation orientation = Orientation.Horizontal)
+ {
+
+ Capacity capacity = Capacity.Single;
+ if (portDirection == Direction.Input && type == null || portDirection == Direction.Output && type != null)
+ {
+ capacity = Capacity.Multi;
+ }
+
+ UdonPort port = new UdonPort(orientation, portDirection, capacity, type)
+ {
+ m_EdgeConnector = new EdgeConnector<Edge>(connectorListener),
+ };
+
+ port.portName = portName;
+ port._udonNodeData = data;
+ port._nodeValueIndex = index;
+
+ port.SetupPort();
+ return port;
+ }
+
+ public int GetIndex()
+ {
+ return _nodeValueIndex;
+ }
+
+ private bool _isSendChangePort = false;
+
+ private void SetupPort()
+ {
+ _isSendChangePort = portName == "sendChange";
+ if (_isSendChangePort)
+ {
+ m_EdgeConnector = null;
+ }
+ else
+ {
+ this.AddManipulator(m_EdgeConnector);
+ }
+
+ tooltip = UdonGraphExtensions.FriendlyTypeName(portType);
+
+ FullName = _udonNodeData.fullName;
+
+ if (portType == null || direction == Direction.Output)
+ {
+ return;
+ }
+
+ if (TryGetValueObject(out object result, portType))
+ {
+ var field = UdonFieldFactory.CreateField(
+ portType,
+ result,
+ newValue => SetNewValue(newValue)
+ );
+
+ if (field != null)
+ {
+ SetupField(field);
+ }
+ }
+
+ if (_udonNodeData.fullName.StartsWithCached("Const"))
+ {
+ RemoveConnectorAndLabel();
+ }
+ else if (_udonNodeData.fullName.StartsWithCached("Set_Variable") && _nodeValueIndex == 2)
+ {
+ _isSendChangePort = true;
+ RemoveConnector();
+ AddToClassList("send-change");
+ }
+
+ AddToClassList(portName);
+
+ UpdateLabel(connected);
+ }
+
+ // Made its own method for now as we have issues auto-converting between string and char in a TextField
+ // TODO: refactor SetupField so we can do just the field.value part separately to combine with this
+ private VisualElement SetupCharField()
+ {
+ TextField field = new TextField();
+ field.AddToClassList("portField");
+ if (TryGetValueObject(out object result))
+ {
+ field.value = UdonGraphExtensions.UnescapeLikeALiteral((char) result);
+ }
+
+ field.isDelayed = true;
+
+ // Special handling for escaping char value
+#if UNITY_2019_3_OR_NEWER
+ field.RegisterValueChangedCallback(
+#else
+ field.OnValueChanged(
+#endif
+ e =>
+ {
+ if (e.newValue[0] == '\\' && e.newValue.Length > 1)
+ {
+ SetNewValue(UdonGraphExtensions.EscapeLikeALiteral(e.newValue.Substring(0, 2)));
+ }
+ else
+ {
+ SetNewValue(e.newValue[0]);
+ }
+ });
+ _inputField = field;
+
+ // Add label, shown when input is connected. Not shown by default
+ var friendlyName = UdonGraphExtensions.FriendlyTypeName(typeof(char)).FriendlyNameify();
+ var label = new Label(friendlyName);
+ _inputFieldTypeLabel = label;
+
+ return _inputField;
+ }
+
+ private void SetupField(VisualElement field)
+ {
+ // Custom Event fields need their event names sanitized after input and their connectors removed
+ if (_udonNodeData.fullName.CompareTo("Event_Custom") == 0)
+ {
+ var tfield = (TextField) field;
+#if UNITY_2019_3_OR_NEWER
+ tfield.RegisterValueChangedCallback(
+#else
+ tfield.OnValueChanged(
+#endif
+ (e) =>
+ {
+ string newValue = e.newValue.SanitizeVariableName();
+ tfield.value = newValue;
+ SetNewValue(newValue);
+ });
+ RemoveConnectorAndLabel();
+ }
+
+ // Add label, shown when input is connected. Not shown by default
+ var friendlyName = UdonGraphExtensions.FriendlyTypeName(portType).FriendlyNameify();
+ var label = new Label(friendlyName);
+ _inputFieldTypeLabel = label;
+ field.AddToClassList("portField");
+
+ _inputField = field;
+ Add(_inputField);
+ }
+
+ private void RemoveConnectorAndLabel()
+ {
+ RemoveConnector();
+ this.Q(null, "connectorText")?.RemoveFromHierarchy();
+ }
+
+ private void RemoveConnector()
+ {
+ this.Q("connector")?.RemoveFromHierarchy();
+ }
+
+#pragma warning disable 0649 // variable never assigned
+ private Button _editArrayButton;
+
+ private void EditArray(Type elementType)
+ {
+ // Update Values when 'Save' is clicked
+ if (_inspector != null)
+ {
+ // Update Values
+ SetNewValue(_inspector.GetValues());
+
+ // Remove Inspector
+ _inspector.RemoveFromHierarchy();
+ _inspector = null;
+
+ // Update Button Text
+ _editArrayButton.text = "Edit";
+ return;
+ }
+
+ // Otherwise set up the inspector
+ _editArrayButton.text = "Save";
+
+ // Get value object, null is ok
+ TryGetValueObject(out object value);
+
+ // Create it new
+ Type typedArrayInspector = (typeof(UdonArrayInspector<>)).MakeGenericType(elementType);
+ _inspector = (Activator.CreateInstance(typedArrayInspector, value) as IArrayProvider);
+
+ parent.Add(_inspector as VisualElement);
+ }
+
+ // Update elements on connect
+ public override void Connect(Edge edge)
+ {
+ AddToClassList("connected");
+ base.Connect(edge);
+
+ Undo.RecordObject(((UdonNode)node).Graph.graphProgramAsset, "Connect Edge");
+
+ // The below logic is just for Output ports
+ if (edge.input.Equals(this)) return;
+
+ // hide field, show label
+ var input = ((UdonPort) edge.input);
+ input.UpdateLabel(true);
+
+ if (IsReloading())
+ {
+ return;
+ }
+
+ // update data
+ if (portType == null)
+ {
+ // We are a flow port
+ SetFlowUID(((UdonNode) input.node).uid);
+ this.Compile();
+ }
+ else
+ {
+ // We are a value port, we need to send our info over to the OTHER node
+ string myNodeUid = ((UdonNode) node).uid;
+ input.SetDataFromNewConnection($"{myNodeUid}|{_nodeValueIndex}", input.GetIndex());
+ }
+
+ if (_isSendChangePort)
+ {
+ DisconnectAll();
+ this.Reload();
+ }
+ }
+
+ public override void OnStopEdgeDragging()
+ {
+ base.OnStopEdgeDragging();
+
+ if (edgeConnector?.edgeDragHelper?.draggedPort == this)
+ {
+ if (capacity == Capacity.Single && connections.Count() > 0)
+ {
+ // This port could only have one connection. Fixed in Reserialize, need to reload to show the change
+ this.Reload();
+ }
+ }
+ else
+ {
+ this.Reload();
+ }
+ }
+
+ private void SetFlowUID(string newValue)
+ {
+ if (_udonNodeData.flowUIDs.Length <= _nodeValueIndex)
+ {
+ // If we don't have space for this flow value, create a new array
+ // TODO: handle this elsewhere?
+ var newFlowArray = new string[_nodeValueIndex + 1];
+ for (int i = 0; i < _udonNodeData.flowUIDs.Length; i++)
+ {
+ newFlowArray[i] = _udonNodeData.flowUIDs[i];
+ }
+
+ _udonNodeData.flowUIDs = newFlowArray;
+
+ _udonNodeData.flowUIDs.SetValue(newValue, _nodeValueIndex);
+ }
+ else
+ {
+ _udonNodeData.flowUIDs.SetValue(newValue, _nodeValueIndex);
+ }
+ }
+
+ public bool IsReloading()
+ {
+ if (node is UdonNode)
+ {
+ return ((UdonNode) node).Graph.IsReloading;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ public void SetDataFromNewConnection(string uidAndPort, int index)
+ {
+ // can't do this for Reg stack nodes yet so skipping for demo
+ if (_udonNodeData == null) return;
+
+ if (_udonNodeData.nodeUIDs.Length <= _nodeValueIndex)
+ {
+ Debug.Log("Couldn't set it");
+ }
+ else
+ {
+ _udonNodeData.nodeUIDs.SetValue(uidAndPort, index);
+ }
+ }
+
+ // Update elements on disconnect
+ public override void Disconnect(Edge edge)
+ {
+ RemoveFromClassList("connected");
+ if (node == null) return;
+ Undo.RecordObject(((UdonNode)node).Graph.graphProgramAsset, "Connect Edge");
+ base.Disconnect(edge);
+
+ // hide label, show field
+ if (direction == Direction.Input)
+ {
+ UpdateLabel(false);
+ }
+
+ if (IsReloading())
+ {
+ return;
+ }
+
+ // update data
+ if (direction == Direction.Output && portType == null)
+ {
+ // We are a flow port
+ SetFlowUID("");
+ this.Compile();
+ }
+ else if (direction == Direction.Input && portType != null)
+ {
+ // Direction is input
+ // We are a value port
+ SetDataFromNewConnection("", GetIndex());
+ }
+ }
+
+ public void UpdateLabel(bool isConnected)
+ {
+ // Port has a 'connected' bool but it doesn't seem to update, so passing 'isConnected' for now
+
+ if (isConnected)
+ {
+ if (_inputField != null && Contains(_inputField))
+ {
+ _inputField.RemoveFromHierarchy();
+ }
+
+ if (_inputFieldTypeLabel != null && !Contains(_inputFieldTypeLabel))
+ {
+ Add(_inputFieldTypeLabel);
+ }
+
+ if (_editArrayButton != null && Contains(_editArrayButton))
+ {
+ _editArrayButton.RemoveFromHierarchy();
+ }
+ }
+ else
+ {
+ if (_inputField != null && !Contains(_inputField))
+ {
+ Add(_inputField);
+ }
+
+ if (_inputFieldTypeLabel != null && Contains(_inputFieldTypeLabel))
+ {
+ _inputFieldTypeLabel.RemoveFromHierarchy();
+ }
+
+ if (_editArrayButton != null && !Contains(_editArrayButton))
+ {
+ Add(_editArrayButton);
+ }
+ }
+ }
+
+ private bool TryGetValueObject(out object result, Type type = null)
+ {
+ // Initialize out object
+ result = null;
+
+ // get container from node values
+ SerializableObjectContainer container = _udonNodeData.nodeValues[_nodeValueIndex];
+
+ // Null check, failure
+ if (container == null)
+ return false;
+
+ // Deserialize into result, return failure on null
+ result = container.Deserialize();
+
+ // Strings will deserialize as null, that's ok
+ if (type == null || type == typeof(string))
+ {
+ return true;
+ }
+ // any other type is not ok to be null
+ else if (result == null)
+ {
+ return false;
+ }
+
+ // Success - return true
+ return type.IsInstanceOfType(result);
+ }
+
+ private void SetNewValue(object newValue)
+ {
+ _udonNodeData.nodeValues[_nodeValueIndex] = SerializableObjectContainer.Serialize(newValue, portType);
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs.meta
new file mode 100644
index 00000000..e09d6a0e
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonPort.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8f83d1d3578dd28498c71a980bca86dd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs
new file mode 100644
index 00000000..79baae39
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs
@@ -0,0 +1,6 @@
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonStackNode
+ {
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs.meta
new file mode 100644
index 00000000..827649e8
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonStackNode.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1d5984c5be753da439b8a33ffbee8d36
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs
new file mode 100644
index 00000000..1b59c5d3
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs
@@ -0,0 +1,122 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEngine.UIElements;
+using UnityEditor.Experimental.GraphView;
+#else
+using UnityEngine.Experimental.UIElements;
+using UnityEditor.Experimental.UIElements.GraphView;
+#endif
+using System.Collections.Generic;
+using UnityEngine;
+using VRC.Udon.Graph;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonVariablesBlackboard : Blackboard, IUdonGraphElementDataProvider
+ {
+ private CustomData _customData = new CustomData();
+ private UdonGraph _graph;
+ private Dictionary<string, BlackboardRow> _idToRow;
+
+ public UdonVariablesBlackboard(UdonGraph graph)
+ {
+ _graph = graph;
+ title = "Variables";
+ name = "Parameters";
+ scrollable = true;
+
+ // Remove subtitle
+ var subtitle = this.Query<Label>("subTitleLabel").AtIndex(0);
+ if (subtitle != null)
+ {
+ subtitle.RemoveFromHierarchy();
+ }
+
+ // Improve resizer UI
+ style.borderBottomWidth = 1;
+
+ var resizer = this.Q(null, "resizer");
+ if (resizer != null)
+ {
+ resizer.style.paddingTop = 0;
+ resizer.style.paddingLeft = 0;
+ }
+
+ SetPosition(_customData.layout);
+
+ _idToRow = new Dictionary<string, BlackboardRow>();
+ }
+
+ public new void Clear()
+ {
+ _idToRow.Clear();
+ base.Clear();
+ }
+
+ public void AddFromData(UdonNodeData nodeData)
+ {
+ // don't add internal variables, which start with __
+ // Todo: handle all "__" variables instead, need to tell community first and let the word spread
+ string newVariableName = (string)nodeData.nodeValues[(int)UdonParameterProperty.ValueIndices.name].Deserialize();
+ if (newVariableName.StartsWithCached("__returnValue"))
+ {
+ return;
+ }
+
+ UdonNodeDefinition definition = UdonEditorManager.Instance.GetNodeDefinition(nodeData.fullName);
+ if (definition != null)
+ {
+ BlackboardRow row = new BlackboardRow(new UdonParameterField(_graph, nodeData),
+ new UdonParameterProperty(_graph, definition, nodeData));
+ contentContainer.Add(row);
+ _idToRow.Add(nodeData.uid, row);
+ }
+ this.Reload();
+ }
+
+ public void RemoveByID(string id)
+ {
+ if (_idToRow.TryGetValue(id, out BlackboardRow row))
+ {
+ Remove(row);
+ _idToRow.Remove(id);
+ }
+ }
+
+ public void SetVisible(bool value)
+ {
+ visible = value;
+ _customData.visible = value;
+ SaveData();
+ }
+
+ public override void UpdatePresenterPosition()
+ {
+ _customData.layout = GetPosition();
+ SaveData();
+ }
+
+ private void SaveData()
+ {
+ _graph.SaveGraphElementData(this);
+ }
+
+ public UdonGraphElementData GetData()
+ {
+ return new UdonGraphElementData(UdonGraphElementType.VariablesWindow, this.GetUid(), JsonUtility.ToJson(_customData));
+ }
+
+ public class CustomData {
+ public bool visible = true;
+ public Rect layout = new Rect(10, 130, 200, 150);
+ }
+
+ internal void LoadData(UdonGraphElementData data)
+ {
+ _idToRow = new Dictionary<string, BlackboardRow>();
+ JsonUtility.FromJsonOverwrite(data.jsonData, _customData);
+ SetPosition(_customData.layout);
+ this.visible = _customData.visible;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs.meta
new file mode 100644
index 00000000..ecb9b2d3
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/GraphElements/UdonVariablesBlackboard.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2d0a4730c5f61b247b27b54f280300b5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search.meta
new file mode 100644
index 00000000..e32446e5
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b713b1dddee74a340a64c7755fa1f9e6
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs
new file mode 100644
index 00000000..d29e25db
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs
@@ -0,0 +1,64 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+#endif
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+using VRC.Udon.Graph;
+using VRC.Udon.Graph.Interfaces;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+
+ public class UdonFocusedSearchWindow : UdonSearchWindowBase
+ {
+
+ public INodeRegistry targetRegistry;
+ internal List<SearchTreeEntry> _fullRegistry;
+
+ #region ISearchWindowProvider
+
+ public override List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
+ {
+
+ _fullRegistry = new List<SearchTreeEntry>();
+
+ Texture2D icon = EditorGUIUtility.FindTexture("cs Script Icon");
+
+ var registryName = GetSimpleNameForRegistry(targetRegistry);
+ _fullRegistry.Add(new SearchTreeGroupEntry(new GUIContent($"{registryName} Search"), 0));
+
+ // add Registry Level
+ AddEntriesForRegistry(_fullRegistry, targetRegistry, 1, true);
+
+ return _fullRegistry;
+ }
+
+ public override bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context)
+ {
+ if (entry.userData is UdonNodeDefinition definition && !_graphView.IsDuplicateEventNode(definition.fullName))
+ {
+ _graphView.AddNodeFromSearch(definition, GetGraphPositionFromContext(context));
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ // TODO: move this to Extension
+ private string GetSimpleNameForRegistry(INodeRegistry registry)
+ {
+ string registryName = registry.ToString().Replace("NodeRegistry", "").FriendlyNameify();
+ registryName = registryName.Substring(registryName.LastIndexOf(".") + 1);
+ registryName = registryName.Replace("UnityEngine", "");
+ return registryName;
+ }
+
+ #endregion
+
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs.meta
new file mode 100644
index 00000000..6bbeda5f
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFocusedSearchWindow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6581176c97993bb40976acff208bd0b1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs
new file mode 100644
index 00000000..0660019b
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs
@@ -0,0 +1,113 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+#endif
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+using VRC.Udon.Graph;
+using VRC.Udon.Graph.Interfaces;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+
+ public class UdonFullSearchWindow : UdonSearchWindowBase
+ {
+
+ static private List<SearchTreeEntry> _slowRegistryCache;
+ #region ISearchWindowProvider
+
+ override public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
+ {
+ if (!skipCache && (_slowRegistryCache != null && _slowRegistryCache.Count > 0)) return _slowRegistryCache;
+
+ _slowRegistryCache = new List<SearchTreeEntry>
+ {
+ new SearchTreeGroupEntry(new GUIContent("Full Search"), 0)
+ };
+
+ var topRegistries = UdonEditorManager.Instance.GetTopRegistries();
+
+ Texture2D icon = null;
+ var groupEntries = new Dictionary<string, SearchTreeGroupEntry>();
+ foreach (var topRegistry in topRegistries)
+ {
+ string topName = topRegistry.Key.Replace("NodeRegistry", "");
+
+ if (topName != "Udon")
+ {
+ _slowRegistryCache.Add(new SearchTreeGroupEntry(new GUIContent(topName), 1));
+ }
+
+ // get all registries, save into registryName > INodeRegistry Lookup
+ var subRegistries = new Dictionary<string, INodeRegistry>();
+ foreach (KeyValuePair<string, INodeRegistry> registry in topRegistry.Value.OrderBy(s => s.Key))
+ {
+ string baseRegistryName = registry.Key.Replace("NodeRegistry", "").FriendlyNameify().ReplaceFirst(topName, "");
+ string registryName = baseRegistryName.UppercaseFirst();
+ subRegistries.Add(registryName, registry.Value);
+ }
+
+ // Go through each registry entry and add the top-level registry and associated array registry
+ foreach (KeyValuePair<string, INodeRegistry> regEntry in subRegistries)
+ {
+ INodeRegistry registry = regEntry.Value;
+ string registryName = regEntry.Key;
+
+ int level = 2;
+ // Special cases for Udon sub-levels, added at top
+ if (topName == "Udon")
+ {
+ level = 1;
+ if (registryName == "Event" || registryName == "Type")
+ {
+ registryName = $"{registryName}s";
+ }
+ }
+
+ if (!registryName.EndsWith("[]"))
+ {
+ // add Registry Level
+ var groupEntry = new SearchTreeGroupEntry(new GUIContent(registryName, icon), level) { userData = registry };
+ _slowRegistryCache.Add(groupEntry);
+ }
+
+ // Check for Array Type first
+ string regArrayType = $"{registryName}[]";
+ if (subRegistries.TryGetValue(regArrayType, out INodeRegistry arrayRegistry))
+ {
+ // we have a matching subRegistry, add that next
+ var arrayLevel = level + 1;
+ var arrayGroupEntry = new SearchTreeGroupEntry(new GUIContent(regArrayType, icon), arrayLevel) { userData = registry };
+ _slowRegistryCache.Add(arrayGroupEntry);
+
+ // Add all array entries
+ AddEntriesForRegistry(_slowRegistryCache, arrayRegistry, arrayLevel + 1);
+ }
+
+ AddEntriesForRegistry(_slowRegistryCache, registry, level + 1, true);
+
+ }
+ }
+ return _slowRegistryCache;
+ }
+
+ public override bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context)
+ {
+ // checking type so we can support selecting registries as well
+ if (entry.userData is UdonNodeDefinition definition && !_graphView.IsDuplicateEventNode(definition.fullName))
+ {
+ _graphView.AddNodeFromSearch(definition, GetGraphPositionFromContext(context));
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ #endregion
+
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs.meta
new file mode 100644
index 00000000..135a1da8
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonFullSearchWindow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b721120e6c1d320448a55fe87a7de824
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs
new file mode 100644
index 00000000..e7279762
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs
@@ -0,0 +1,124 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+#endif
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using VRC.Udon.Graph;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+
+ public class UdonPortSearchWindow : UdonSearchWindowBase
+ {
+
+ internal List<SearchTreeEntry> _fullRegistry;
+
+ #region ISearchWindowProvider
+
+ public Type typeToSearch;
+ public UdonPort startingPort;
+ public Direction direction;
+
+ public class VariableInfo
+ {
+ public string uid;
+ public bool isGetter;
+
+ public VariableInfo(string uid, bool isGetter)
+ {
+ this.uid = uid;
+ this.isGetter = isGetter;
+ }
+ }
+
+ override public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
+ {
+ _fullRegistry = new List<SearchTreeEntry>
+ {
+ new SearchTreeGroupEntry(new GUIContent($"{direction.ToString()} Search"), 0)
+ };
+
+ var defsToAdd = new Dictionary<string, List<UdonNodeDefinition>>();
+ var registries = UdonEditorManager.Instance.GetNodeRegistries();
+ foreach (var item in registries)
+ {
+ var definitions = item.Value.GetNodeDefinitions().ToList();
+
+ var registryName = item.Key.FriendlyNameify().Replace("NodeRegistry", "");
+ defsToAdd.Add(registryName, new List<UdonNodeDefinition>());
+
+ foreach (var def in definitions)
+ {
+ var collection = direction == Direction.Input ? def.Inputs : def.Outputs;
+ if(collection.Any(p=>p.type == typeToSearch))
+ {
+ defsToAdd[registryName].Add(def);
+ }
+ }
+ }
+
+ var variables = _graphView.GetVariableNodes;
+
+ // Add Getters and Setters for matched variable types
+ Texture2D icon = EditorGUIUtility.FindTexture("GameManager Icon");
+ string typeToSearchSimple = typeToSearch.ToString().Replace(".", "");
+ foreach (var item in variables)
+ {
+ string variableSimpleName = item.fullName.Replace("Variable_", "");
+ string getOrSet = direction == Direction.Output ? "Get" : "Set";
+ if(variableSimpleName == typeToSearchSimple)
+ {
+ string customVariableName = item.nodeValues[1].Deserialize().ToString();
+ _fullRegistry.Add(new SearchTreeEntry(new GUIContent($"{getOrSet} {customVariableName}", icon))
+ {
+ level = 1,
+ userData = new VariableInfo(item.uid, direction == Direction.Output),
+ });
+ }
+ }
+
+ foreach (var item in defsToAdd)
+ {
+ // Skip empty lists
+ if (item.Value.Count == 0) continue;
+
+ _fullRegistry.Add(new SearchTreeGroupEntry(new GUIContent(item.Key), 1));
+ AddEntries(_fullRegistry, item.Value, 2);
+ }
+
+ return _fullRegistry;
+ }
+
+ public override bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context)
+ {
+ var position = GetGraphPositionFromContext(context) - new Vector2(140, 0);
+ // checking type so we can support selecting registries as well
+ if (entry.userData is UdonNodeDefinition definition && !_graphView.IsDuplicateEventNode(definition.fullName))
+ {
+ var node = _graphView.AddNodeFromSearch(definition, position);
+ _graphView.ConnectNodeTo(node, startingPort, direction, typeToSearch);
+ return true;
+ }
+ else if(entry.userData is VariableInfo data)
+ {
+ UdonNode node = _graphView.MakeVariableNode(data.uid, position, data.isGetter ? GraphView.UdonGraph.VariableNodeType.Getter : GraphView.UdonGraph.VariableNodeType.Setter );
+ _graphView.AddElement(node);
+ _graphView.ConnectNodeTo(node, startingPort, direction, typeToSearch);
+ _graphView.RefreshVariables(true);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ #endregion
+
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs.meta
new file mode 100644
index 00000000..e5ff6867
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonPortSearchWindow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e94c084f399869b42a21244fd07778c4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs
new file mode 100644
index 00000000..67a19eb2
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs
@@ -0,0 +1,183 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+#endif
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using VRC.Udon.Graph;
+using VRC.Udon.Graph.Interfaces;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonRegistrySearchWindow : UdonSearchWindowBase
+ {
+ private UdonSearchManager _searchManager;
+ private static List<SearchTreeEntry> _registryCache;
+ private List<(string, string)> _shortcutRegistries = new List<(string, string)>()
+ {
+ ("UnityEngine","Debug"),
+ ("Udon","Special"),
+ ("Udon","Type"),
+ ("VRC", "UdonCommonInterfacesIUdonEventReceiver")
+ };
+
+ private HashSet<string> _hiddenRegistries = new HashSet<string>()
+ {
+ };
+
+ public void Initialize(UdonGraphWindow editorWindow, UdonGraph graphView, UdonSearchManager manager)
+ {
+ base.Initialize(editorWindow, graphView);
+ _searchManager = manager;
+ }
+
+ #region ISearchWindowProvider
+
+ override public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
+ {
+ if (!skipCache && (_registryCache != null && _registryCache.Count > 0)) return _registryCache;
+
+ _registryCache = new List<SearchTreeEntry>();
+
+ Texture2D icon = EditorGUIUtility.FindTexture("cs Script Icon");
+ _registryCache.Add(new SearchTreeGroupEntry(new GUIContent("Quick Search"), 0));
+
+ var topRegistriesLookup = new Dictionary<string, List<KeyValuePair<string, INodeRegistry>>>();
+ foreach (var entry in UdonEditorManager.Instance.GetTopRegistries())
+ {
+ topRegistriesLookup.Add(entry.Key, new List<KeyValuePair<string, INodeRegistry>>(entry.Value));
+ }
+
+ // Add shortcut registries
+ foreach (var item in _shortcutRegistries)
+ {
+ if (topRegistriesLookup.TryGetValue(item.Item1, out var searchRegistry))
+ {
+ string subRegistryName = $"{item.Item1}{item.Item2}NodeRegistry";
+ var subRegistry = searchRegistry.FindAll(r => r.Key == subRegistryName);
+ if (subRegistry.Count == 1)
+ {
+ topRegistriesLookup.Add(item.Item2, subRegistry);
+ }
+ else
+ {
+ Debug.LogWarning($"Could not find sub-registry {subRegistryName}");
+ }
+ }
+ }
+
+ // Combine Events into special top-level element
+ var vrcEvents = topRegistriesLookup["VRC"].Find(r=>r.Key == "VRCEventNodeRegistry").Value.GetNodeDefinitions();
+ var udonEvents = topRegistriesLookup["Udon"].Find(r => r.Key == "UdonEventNodeRegistry").Value.GetNodeDefinitions();
+ var allEvents = vrcEvents.Concat(udonEvents);
+ var eventRegistry = new EventRegistry();
+ foreach (var item in allEvents)
+ {
+ eventRegistry.definitions.Add(item);
+ }
+ topRegistriesLookup.Add("Events", new List<KeyValuePair<string, INodeRegistry>>() { new KeyValuePair<string, INodeRegistry>("Events", eventRegistry) });
+
+ // Build lookup from newly organized list
+ var topRegistries = topRegistriesLookup.OrderBy(s => s.Key);
+ foreach (var topRegistry in topRegistries)
+ {
+ string topName = topRegistry.Key.Replace("NodeRegistry", "");
+
+ // Handle Shortcut registries with only 1 top-level registry listed
+ if (topRegistry.Value.Count == 1)
+ {
+ string registryName = UdonGraphExtensions.FriendlyNameify(topName);
+ _registryCache.Add(new SearchTreeGroupEntry(new GUIContent(registryName)) { level = 1, userData = topRegistry.Value });
+ AddEntriesForRegistry(_registryCache, topRegistry.Value.First().Value, 2);
+ continue;
+ }
+
+ // Skip empty 'Udon' top level
+ if (topName != "Udon")
+ {
+ _registryCache.Add(new SearchTreeGroupEntry(new GUIContent(topName), 1));
+ }
+
+ foreach (KeyValuePair<string, INodeRegistry> registry in topRegistry.Value.OrderBy(s => s.Key))
+ {
+ string baseRegistryName = registry.Key.Replace("NodeRegistry", "").FriendlyNameify().ReplaceFirst(topName, "");
+ string registryName = baseRegistryName.UppercaseFirst();
+
+ // Plural-ize Event->Events and Type->Types
+ if (topName == "Udon" && (registryName == "Event" || registryName == "Type"))
+ {
+ registryName = $"{registryName}s";
+ }
+ else
+ {
+ // add Registry Level
+ if (registryName.StartsWithCached("Object") || registryName.StartsWithCached("Type"))
+ {
+ registryName = $"{topName}.{registryName}";
+ }
+
+ // skip certain registries
+ if (_hiddenRegistries.Contains(registryName))
+ {
+ continue;
+ }
+
+ _registryCache.Add(new SearchTreeEntry(new GUIContent(registryName, icon, $"{topName}.{registryName}")) { level = 2, userData = registry.Value });
+ }
+ }
+ }
+ return _registryCache;
+ }
+
+ public override bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context)
+ {
+ // checking type so we can support selecting registries as well
+ if (entry.userData is INodeRegistry)
+ {
+ _searchManager.QueueOpenFocusedSearch(entry.userData as INodeRegistry, context.screenMousePosition);
+ return true;
+ }
+ else if (entry.userData is UdonNodeDefinition definition && !_graphView.IsDuplicateEventNode(definition.fullName))
+ {
+ _graphView.AddNodeFromSearch(definition, GetGraphPositionFromContext(context));
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ #endregion
+
+ public class EventRegistry : INodeRegistry
+ {
+ public List<UdonNodeDefinition> definitions = new List<UdonNodeDefinition>();
+
+ public UdonNodeDefinition GetNodeDefinition(string identifier)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IEnumerable<UdonNodeDefinition> GetNodeDefinitions()
+ {
+ return definitions;
+ }
+
+ public IEnumerable<UdonNodeDefinition> GetNodeDefinitions(string baseIdentifier)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Dictionary<string, INodeRegistry> GetNodeRegistries()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs.meta
new file mode 100644
index 00000000..e0a73c89
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonRegistrySearchWindow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6a6c453fae11b5349a33399e258d1578
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs
new file mode 100644
index 00000000..49df2467
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs
@@ -0,0 +1,119 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+#endif
+using System;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using VRC.Udon.Graph.Interfaces;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonSearchManager
+ {
+ private UdonGraph _view;
+ private UdonGraphWindow _window;
+
+ // Search Windows
+ private UdonFocusedSearchWindow _focusedSearchWindow;
+ private UdonRegistrySearchWindow _registrySearchWindow;
+ private UdonFullSearchWindow _fullSearchWindow;
+ public UdonVariableTypeWindow _variableSearchWindow;
+ public UdonPortSearchWindow _portSearchWindow;
+
+ public UdonSearchManager(UdonGraph view, UdonGraphWindow window)
+ {
+ _view = view;
+ _window = window;
+
+ SetupSearchTypes();
+
+ view.nodeCreationRequest += OnRequestNodeCreation;
+ }
+
+ private void SetupSearchTypes()
+ {
+ if (_registrySearchWindow == null)
+ _registrySearchWindow = ScriptableObject.CreateInstance<UdonRegistrySearchWindow>();
+ _registrySearchWindow.Initialize(_window, _view, this);
+
+ if (_fullSearchWindow == null)
+ _fullSearchWindow = ScriptableObject.CreateInstance<UdonFullSearchWindow>();
+ _fullSearchWindow.Initialize(_window, _view);
+
+ if (_focusedSearchWindow == null)
+ _focusedSearchWindow = ScriptableObject.CreateInstance<UdonFocusedSearchWindow>();
+ _focusedSearchWindow.Initialize(_window, _view);
+
+ if (_variableSearchWindow == null)
+ _variableSearchWindow = ScriptableObject.CreateInstance<UdonVariableTypeWindow>();
+ _variableSearchWindow.Initialize(_window, _view);
+
+ if (_portSearchWindow == null)
+ _portSearchWindow = ScriptableObject.CreateInstance<UdonPortSearchWindow>();
+ _portSearchWindow.Initialize(_window, _view);
+ }
+
+ private void OnRequestNodeCreation(NodeCreationContext context)
+ {
+ // started on empty space
+ if (context.target == null)
+ {
+ // If we have a node selected (but not set as context.target because that's a container for new nodes to go into), search within that node's registry
+ if (Settings.SearchOnSelectedNodeRegistry && _view.selection.Count > 0 && _view.selection.First() is UdonNode)
+ {
+ _focusedSearchWindow.targetRegistry = (_view.selection.First() as UdonNode).Registry;
+ SearchWindow.Open(new SearchWindowContext(context.screenMousePosition, 360, 360), _focusedSearchWindow);
+ }
+ else
+ {
+ // Create Search Window that only searches Top-Level Registries
+ SearchWindow.Open(new SearchWindowContext(context.screenMousePosition, 360, 360), _registrySearchWindow);
+ }
+ }
+ else if (context.target is UdonGraph)
+ {
+ // Slightly hacky method to figure out that we want a full-search window
+ SearchWindow.Open(new SearchWindowContext(context.screenMousePosition, 360, 360), _fullSearchWindow);
+ }
+ }
+
+ public void OpenVariableSearch(Vector2 screenMousePosition)
+ {
+ // offset search window to appear next to mouse
+ screenMousePosition.x += 140;
+ screenMousePosition.y += 0;
+ SearchWindow.Open(new SearchWindowContext(screenMousePosition, 360, 360), _variableSearchWindow);
+ }
+
+ public void OpenPortSearch(Type type, Vector2 screenMousePosition, UdonPort port, Direction direction)
+ {
+ // offset search window to appear next to mouse
+ screenMousePosition = _portSearchWindow._editorWindow.position.position + screenMousePosition;
+ screenMousePosition.x += 140;
+ screenMousePosition.y += 0;
+ _portSearchWindow.typeToSearch = type;
+ _portSearchWindow.startingPort = port;
+ _portSearchWindow.direction = direction;
+ SearchWindow.Open(new SearchWindowContext(screenMousePosition, 360, 360), _portSearchWindow);
+ }
+
+ private Vector2 _searchWindowPosition;
+ public void QueueOpenFocusedSearch(INodeRegistry registry, Vector2 position)
+ {
+ _searchWindowPosition = position;
+ _focusedSearchWindow.targetRegistry = registry;
+ EditorApplication.update += TryOpenFocusedSearch;
+ }
+
+ private void TryOpenFocusedSearch()
+ {
+ if (SearchWindow.Open(new SearchWindowContext(_searchWindowPosition, 360, 360), _focusedSearchWindow))
+ {
+ EditorApplication.update -= TryOpenFocusedSearch;
+ }
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs.meta
new file mode 100644
index 00000000..4f5b97c8
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e5a10bb1987c27944bd08a88119b2844
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs
new file mode 100644
index 00000000..0108480c
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs
@@ -0,0 +1,195 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+using UnityEngine.UIElements;
+using UnityEditor.UIElements;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+using UnityEngine.Experimental.UIElements;
+using UnityEditor.Experimental.UIElements;
+#endif
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using VRC.Udon.Graph;
+using VRC.Udon.Graph.Interfaces;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonSearchWindowBase : ScriptableObject, ISearchWindowProvider
+ {
+ // Reference to actual Graph View
+ internal UdonGraph _graphView;
+ private List<SearchTreeEntry> _exampleLookup;
+ internal UdonGraphWindow _editorWindow;
+ protected bool skipCache = false;
+
+ private readonly HashSet<string> nodesToSkip = new HashSet<string>()
+ {
+ "Get_Variable",
+ "Set_Variable",
+ "Comment",
+ "Event_OnVariableChange",
+ };
+
+ public virtual void Initialize(UdonGraphWindow editorWindow, UdonGraph graphView)
+ {
+ _editorWindow = editorWindow;
+ _graphView = graphView;
+ }
+
+ #region ISearchWindowProvider
+
+ public virtual List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
+ {
+ if (!skipCache && ( _exampleLookup != null && _exampleLookup.Count > 0)) return _exampleLookup;
+
+ _exampleLookup = new List<SearchTreeEntry>();
+
+ Texture2D icon = EditorGUIUtility.FindTexture("cs Script Icon");
+ _exampleLookup.Add(new SearchTreeGroupEntry(new GUIContent("Create Node"), 0));
+
+ return _exampleLookup;
+ }
+
+ public virtual bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context)
+ {
+ return true;
+ }
+
+ #endregion
+
+ internal Vector2 GetGraphPositionFromContext(SearchWindowContext context)
+ {
+#if UNITY_2019_3_OR_NEWER
+ var windowRoot = _editorWindow.rootVisualElement;
+#else
+ var windowRoot = _editorWindow.GetRootVisualContainer();
+#endif
+ var windowMousePosition = windowRoot.ChangeCoordinatesTo(windowRoot.parent,
+ context.screenMousePosition - _editorWindow.position.position);
+ var graphMousePosition = _graphView.contentViewContainer.WorldToLocal(windowMousePosition);
+ return graphMousePosition;
+ }
+
+ internal void AddEntries(List<SearchTreeEntry> cache, IEnumerable<UdonNodeDefinition> definitions, int level,
+ bool stripToLastDot = false)
+ {
+ Texture2D icon = AssetPreview.GetMiniTypeThumbnail(typeof(GameObject));
+ Texture2D iconGetComponents = EditorGUIUtility.FindTexture("d_ViewToolZoom");
+ Texture2D iconOther = new Texture2D(1, 1);
+ iconOther.SetPixel(0,0, new Color(0,0,0,0));
+ iconOther.Apply();
+ Dictionary<string, UdonNodeDefinition> baseNodeDefinition = new Dictionary<string, UdonNodeDefinition>();
+
+ foreach (UdonNodeDefinition nodeDefinition in definitions.OrderBy(
+ s => UdonGraphExtensions.PrettyFullName(s)))
+ {
+ string baseIdentifier = nodeDefinition.fullName;
+ string[] splitBaseIdentifier = baseIdentifier.Split(new[] { "__" }, StringSplitOptions.None);
+ if (splitBaseIdentifier.Length >= 2)
+ {
+ baseIdentifier = $"{splitBaseIdentifier[0]}__{splitBaseIdentifier[1]}";
+ }
+
+ if (baseNodeDefinition.ContainsKey(baseIdentifier))
+ {
+ continue;
+ }
+
+ baseNodeDefinition.Add(baseIdentifier, nodeDefinition);
+ }
+
+ var nodesOfGetComponentType = new List<SearchTreeEntry>();
+ var nodesOfOtherType = new List<SearchTreeEntry>();
+
+ // add all subTypes
+ foreach (KeyValuePair<string, UdonNodeDefinition> nodeDefinitionsEntry in baseNodeDefinition)
+ {
+ string nodeName = UdonGraphExtensions.PrettyBaseName(nodeDefinitionsEntry.Key);
+ nodeName = nodeName.UppercaseFirst();
+ nodeName = nodeName.Replace("_", " ");
+ if (stripToLastDot)
+ {
+ int lastDotIndex = nodeName.LastIndexOf('.');
+ nodeName = nodeName.Substring(lastDotIndex + 1);
+ }
+
+ // Skip some nodes that should be added in other ways (variables and comments)
+ if (nodeName.StartsWithCached("Variable") || nodesToSkip.Contains(nodeDefinitionsEntry.Key))
+ {
+ continue;
+ }
+
+ if (nodeName.StartsWithCached("Object"))
+ {
+ nodeName = $"{nodeDefinitionsEntry.Value.type.Namespace}.{nodeName}";
+ }
+
+ if (nodeNamesGetComponentType.Contains(nodeName))
+ {
+ nodesOfGetComponentType.Add(new SearchTreeEntry(new GUIContent(nodeName, iconGetComponents)) { level = level+1, userData = nodeDefinitionsEntry.Value });
+ continue;
+ }
+
+ // Only put 'Equals' in the 'Other' category if this definition is not an Enum
+ if (nodeNamesOtherType.Contains(nodeName) || nodeName == "Equals" && !nodeDefinitionsEntry.Value.type.IsEnum)
+ {
+ nodesOfOtherType.Add(new SearchTreeEntry(new GUIContent(nodeName, iconOther)) { level = level+1, userData = nodeDefinitionsEntry.Value });
+ continue;
+ }
+
+ cache.Add(new SearchTreeEntry(new GUIContent(nodeName, icon)) { level = level, userData = nodeDefinitionsEntry.Value });
+ }
+
+ // add getComponents level
+ if (nodesOfGetComponentType.Count > 0)
+ {
+ cache.Add(new SearchTreeGroupEntry(new GUIContent("GetComponents"), level));
+ foreach (var entry in nodesOfGetComponentType)
+ {
+ cache.Add(entry);
+ }
+ }
+
+ // add other level
+ if (nodesOfOtherType.Count > 0)
+ {
+ cache.Add(new SearchTreeGroupEntry(new GUIContent("Other"), level));
+ foreach (var entry in nodesOfOtherType)
+ {
+ cache.Add(entry);
+ }
+ }
+ }
+
+ private static HashSet<string> nodeNamesGetComponentType = new HashSet<string>()
+ {
+ "GetComponent",
+ "GetComponentInChildren",
+ "GetComponentInParent",
+ "GetComponents",
+ "GetComponentsInChildren",
+ "GetComponentsInParent",
+ };
+
+ private static HashSet<string> nodeNamesOtherType = new HashSet<string>()
+ {
+ "Equality",
+ "GetHashCode",
+ "GetInstanceID",
+ "GetType",
+ "Implicit",
+ "Inequality",
+ "Tostring",
+ };
+
+ // adds all entries so we can use this for regular and array registries
+ internal void AddEntriesForRegistry(List<SearchTreeEntry> cache, INodeRegistry registry, int level,
+ bool stripToLastDot = false)
+ {
+ AddEntries(cache, registry.GetNodeDefinitions(), level, stripToLastDot);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs.meta
new file mode 100644
index 00000000..3bae0009
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonSearchWindowBase.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d825ed3ba6aa7f14294e73efefc217d0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs
new file mode 100644
index 00000000..31f07a5c
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs
@@ -0,0 +1,52 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+#endif
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonVariableTypeWindow : UdonSearchWindowBase
+ {
+ internal List<SearchTreeEntry> _fullRegistry;
+
+ #region ISearchWindowProvider
+
+ override public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
+ {
+ if (!skipCache && _fullRegistry != null) return _fullRegistry;
+
+ _fullRegistry = new List<SearchTreeEntry>();
+
+ _fullRegistry.Add(new SearchTreeGroupEntry(new GUIContent("Variable Type Search"), 0));
+
+ var definitions = UdonEditorManager.Instance.GetNodeDefinitions("Variable_").ToList().OrderBy(n=>n.name);
+ foreach (var definition in definitions)
+ {
+ _fullRegistry.Add(new SearchTreeEntry(new GUIContent(UdonGraphExtensions.FriendlyTypeName(definition.type).FriendlyNameify()))
+ {
+ level = 1,
+ userData = definition.fullName,
+ });
+ }
+
+ return _fullRegistry;
+ }
+
+ override public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context)
+ {
+ if(_graphView == null)
+ {
+ return false;
+ }
+ _graphView.AddNewVariable((string)entry.userData);
+ return true;
+ }
+
+ #endregion
+
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs.meta
new file mode 100644
index 00000000..70109177
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/Search/UdonVariableTypeWindow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 16fc7a7a059deeb458fdcdf719b467a4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs
new file mode 100644
index 00000000..3899a163
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Linq.Expressions;
+using UnityEngine;
+using VRC.Udon.Common.Interfaces;
+
+// nicked from GraphProcessor project
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram
+{
+ public static class TypeExtension
+ {
+ public static bool IsReallyAssignableFrom(this Type type, Type otherType)
+ {
+ if (type == null && otherType != null) return false;
+ if (otherType == null && type != null) return false;
+
+ if (type == otherType)
+ return true;
+ if (type.IsAssignableFrom(otherType))
+ return true;
+ if (otherType.IsAssignableFrom(type))
+ return true;
+ if (type == typeof(IUdonEventReceiver) && otherType == typeof(Component))
+ return true;
+
+ return false;
+ }
+
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs.meta
new file mode 100644
index 00000000..fdb7364b
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/TypeExtension.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e2f2300f99ce0ea4a8d9a20b464384df
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs
new file mode 100644
index 00000000..23060dd9
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs
@@ -0,0 +1,180 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+#else
+using UnityEditor.Experimental.UIElements;
+using UnityEngine.Experimental.UIElements;
+#endif
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using UnityEngine;
+using VRC.SDKBase;
+using VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView;
+using Object = UnityEngine.Object;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public static class UdonFieldFactory
+ {
+ static readonly Dictionary<Type, Type> fieldDrawers = new Dictionary<Type, Type>();
+
+ static readonly MethodInfo createFieldMethod =
+ typeof(UdonFieldFactory).GetMethod("CreateFieldSpecific", BindingFlags.Static | BindingFlags.Public);
+
+ static UdonFieldFactory()
+ {
+ AddDrawer(typeof(bool), typeof(Toggle));
+ AddDrawer(typeof(int), typeof(IntegerField));
+ AddDrawer(typeof(uint), typeof(UnsignedIntegerField));
+ AddDrawer(typeof(long), typeof(LongField));
+ AddDrawer(typeof(ulong), typeof(UnsignedLongField));
+ AddDrawer(typeof(short), typeof(ShortField));
+ AddDrawer(typeof(ushort), typeof(UnsignedShortField));
+ AddDrawer(typeof(float), typeof(FloatField));
+ AddDrawer(typeof(double), typeof(DoubleField));
+ AddDrawer(typeof(string), typeof(TextField));
+ AddDrawer(typeof(Bounds), typeof(BoundsField));
+ AddDrawer(typeof(Color), typeof(ColorField));
+ AddDrawer(typeof(Vector2), typeof(Vector2Field));
+ AddDrawer(typeof(Vector2Int), typeof(Vector2IntField));
+ AddDrawer(typeof(Vector3), typeof(Vector3Field));
+ AddDrawer(typeof(Vector3Int), typeof(Vector3IntField));
+ AddDrawer(typeof(Vector4), typeof(Vector4Field));
+ AddDrawer(typeof(AnimationCurve), typeof(CurveField));
+ AddDrawer(typeof(Enum), typeof(EnumField));
+ AddDrawer(typeof(Gradient), typeof(GradientField));
+ AddDrawer(typeof(Object), typeof(ObjectField));
+ AddDrawer(typeof(Rect), typeof(RectField));
+ AddDrawer(typeof(RectInt), typeof(RectIntField));
+ AddDrawer(typeof(char), typeof(CharField));
+ AddDrawer(typeof(byte), typeof(ByteField));
+ AddDrawer(typeof(sbyte), typeof(SByteField));
+ AddDrawer(typeof(decimal), typeof(DecimalField));
+ AddDrawer(typeof(Quaternion), typeof(QuaternionField));
+ AddDrawer(typeof(LayerMask), typeof(LayerMaskField));
+ AddDrawer(typeof(VRCUrl), typeof(VRCUrlField));
+ }
+
+ static void AddDrawer(Type fieldType, Type drawerType)
+ {
+ var iNotifyType = typeof(INotifyValueChanged<>).MakeGenericType(fieldType);
+
+ if (!iNotifyType.IsAssignableFrom(drawerType))
+ {
+ Debug.LogWarning("The custom field drawer " + drawerType +
+ " does not implements INotifyValueChanged< " + fieldType + " >");
+ return;
+ }
+
+ fieldDrawers[fieldType] = drawerType;
+ }
+
+ public static INotifyValueChanged<T> CreateField<T>(T value)
+ {
+ return CreateField(value != null ? value.GetType() : typeof(T)) as INotifyValueChanged<T>;
+ }
+
+ public static VisualElement CreateField(Type t)
+ {
+ Type drawerType;
+
+ fieldDrawers.TryGetValue(t, out drawerType);
+
+ if (drawerType == null)
+ drawerType = fieldDrawers.FirstOrDefault(kp => kp.Key.IsReallyAssignableFrom(t)).Value;
+
+ if (drawerType == null)
+ {
+ return null;
+ }
+
+ object field;
+
+ if (drawerType == typeof(EnumField))
+ {
+ field = new EnumField(Activator.CreateInstance(t) as Enum);
+ }
+ else
+ {
+ try
+ {
+ field = Activator.CreateInstance(drawerType,
+ BindingFlags.CreateInstance |
+ BindingFlags.Public |
+ BindingFlags.NonPublic |
+ BindingFlags.Instance |
+ BindingFlags.OptionalParamBinding, null,
+ new object[] {Type.Missing}, CultureInfo.CurrentCulture);
+ }
+ catch
+ {
+ field = Activator.CreateInstance(drawerType,
+ BindingFlags.CreateInstance |
+ BindingFlags.Public |
+ BindingFlags.NonPublic |
+ BindingFlags.Instance |
+ BindingFlags.OptionalParamBinding, null,
+ new object[] { }, CultureInfo.CurrentCulture);
+ }
+ }
+
+ // For mutiline
+ if (field is TextField textField)
+ textField.multiline = true;
+
+ return field as VisualElement;
+ }
+
+ public static INotifyValueChanged<T> CreateFieldSpecific<T>(T value, Action<object> onValueChanged)
+ {
+ var fieldDrawer = CreateField<T>(value);
+
+ if (fieldDrawer == null)
+ return null;
+
+ fieldDrawer.value = value;
+ if (onValueChanged != null)
+ {
+#if UNITY_2019_3_OR_NEWER
+ fieldDrawer.RegisterValueChangedCallback(
+#else
+ fieldDrawer.OnValueChanged(
+#endif
+ (e) => onValueChanged(e.newValue));
+ }
+
+ return fieldDrawer as INotifyValueChanged<T>;
+ }
+
+ public static VisualElement CreateField(Type fieldType, object value, Action<object> onValueChanged)
+ {
+ if (typeof(Enum).IsAssignableFrom(fieldType))
+ fieldType = typeof(Enum);
+
+ if (fieldType.IsArray)
+ {
+ return new UdonArrayEditor(fieldType, onValueChanged, value);
+ }
+
+ VisualElement field = null;
+
+ try
+ {
+ var createFieldSpecificMethod = createFieldMethod.MakeGenericMethod(fieldType);
+ field = createFieldSpecificMethod.Invoke(null, new object[] {value, onValueChanged}) as VisualElement;
+
+ // delay textFields
+ if (field is TextField) ((TextField) field).isDelayed = true;
+ }
+ catch (Exception e)
+ {
+ Debug.LogError(e);
+ }
+
+ return field;
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs.meta
new file mode 100644
index 00000000..d4980abf
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonFieldFactory.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: dfca99fb2e099d84da6c504cd521aa70
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs
new file mode 100644
index 00000000..d4ff35d6
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs
@@ -0,0 +1,1392 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.Experimental.GraphView;
+using UnityGraph = UnityEditor.Experimental.GraphView;
+using UnityEngine.UIElements;
+using MenuAction = UnityEngine.UIElements.DropdownMenuAction;
+#else
+using UnityEditor.Experimental.UIElements.GraphView;
+using UnityGraph = UnityEditor.Experimental.UIElements.GraphView;
+using UnityEngine.Experimental.UIElements;
+using MenuAction = UnityEngine.Experimental.UIElements.DropdownMenu.MenuAction;
+#endif
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using VRC.Udon.Graph;
+using VRC.Udon.Serialization;
+
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public static class UdonGraphCommands
+ {
+ public const string Reload = "Reload";
+ public const string Compile = "Compile";
+ }
+
+ public class UdonGraph : UnityGraph.GraphView
+ {
+ private GridBackground _background;
+ private UdonMinimap _map;
+ private UdonVariablesBlackboard _blackboard;
+
+ // copied over from Legacy.UdonGraph,
+ public UdonGraphProgramAsset graphProgramAsset;
+ public UdonBehaviour udonBehaviour;
+
+ public UdonGraphData graphData
+ {
+ get => graphProgramAsset.graphData;
+ set
+ {
+ graphProgramAsset.graphData = value;
+ EditorUtility.SetDirty(graphProgramAsset);
+ }
+ }
+
+ // Tracking variables
+ private List<UdonNodeData> _variableNodes = new List<UdonNodeData>();
+ private ImmutableList<string> _variableNames;
+
+ private Vector2 lastMousePosition;
+ private VisualElement mouseTipContainer;
+ private TextElement mouseTip;
+ private Vector2 mouseTipOffset = new Vector2(20, -22);
+
+ private UdonSearchManager _searchManager;
+
+ private bool _reloading = false;
+
+ private bool _dragging = false;
+
+ public bool IsReloading => _reloading;
+
+ // Enable stuff from NodeGraphProcessor
+ private UdonGraphWindow _window;
+
+ public ImmutableList<string> GetVariableNames
+ {
+ get => _variableNames;
+ private set { }
+ }
+
+ public List<UdonNodeData> GetVariableNodes
+ {
+ get => _variableNodes;
+ private set { }
+ }
+
+ public bool IsReservedName(string name)
+ {
+ return name.StartsWith("__");
+ }
+
+ public UdonGraph(UdonGraphWindow window)
+ {
+ _window = window;
+
+ this.StretchToParentSize();
+ SetupBackground();
+ SetupMap();
+ SetupBlackboard();
+ SetupZoom(0.2f, 3);
+ SetupDragAndDrop();
+
+ this.AddManipulator(new ContentDragger());
+ this.AddManipulator(new SelectionDragger());
+ this.AddManipulator(new RectangleSelector());
+
+ mouseTipContainer = new VisualElement()
+ {
+ name = "mouse-tip-container",
+ };
+ Add(mouseTipContainer);
+ mouseTip = new TextElement()
+ {
+ name = "mouse-tip",
+ visible = true,
+ };
+ SetMouseTip("");
+ mouseTipContainer.Add(mouseTip);
+
+ // This event is used to send commands from updated port fields
+ RegisterCallback<ExecuteCommandEvent>(OnExecuteCommand);
+
+ // Save last known mouse position for better pasting. Is there a performance hit for this?
+ RegisterCallback<MouseMoveEvent>(OnMouseMove);
+ RegisterCallback<KeyDownEvent>(OnKeyDown);
+
+ _searchManager = new UdonSearchManager(this, window);
+
+ graphViewChanged = OnViewChanged;
+ serializeGraphElements = OnSerializeGraphElements;
+ unserializeAndPaste = OnUnserializeAndPaste;
+ canPasteSerializedData = CheckCanPaste;
+ viewTransformChanged = OnViewTransformChanged;
+ }
+
+ private void OnViewTransformChanged(UnityGraph.GraphView graphView)
+ {
+ graphProgramAsset.viewTransform.position = this.viewTransform.position;
+ graphProgramAsset.viewTransform.scale = this.viewTransform.scale.x;
+ EditorUtility.SetDirty(graphProgramAsset);
+ }
+
+ private bool CheckCanPaste(string pasteData)
+ {
+ UdonNodeData[] copiedNodeDataArray;
+ try
+ {
+ copiedNodeDataArray = JsonUtility
+ .FromJson<SerializableObjectContainer.ArrayWrapper<UdonNodeData>>(
+ UdonGraphExtensions.UnZipString(pasteData))
+ .value;
+ }
+ catch
+ {
+ //oof ouch that's not valid data
+ return false;
+ }
+
+ return true;
+ }
+
+ public void Initialize(UdonGraphProgramAsset asset, UdonBehaviour udonBehaviour)
+ {
+ if (graphProgramAsset != null)
+ {
+ SaveGraphToDisk();
+ }
+
+ graphProgramAsset = asset;
+ if (udonBehaviour != null)
+ {
+ this.udonBehaviour = udonBehaviour;
+ }
+
+ graphData = new UdonGraphData(graphProgramAsset.GetGraphData());
+
+ DoDelayedReload();
+ EditorApplication.update += DelayedRestoreViewFromData;
+
+ // When pressing ctrl-s, we save the graph
+ EditorSceneManager.sceneSaved += _ => SaveGraphToDisk();
+ }
+
+ private void DelayedRestoreViewFromData()
+ {
+ EditorApplication.update -= DelayedRestoreViewFromData;
+ //Todo: restore from saved data instead of FrameAll
+#if UNITY_2019_3_OR_NEWER
+ FrameAll();
+#else
+ UpdateViewTransform(graphProgramAsset.viewTransform.position,
+ Vector3.one * graphProgramAsset.viewTransform.scale);
+ contentViewContainer.MarkDirtyRepaint();
+#endif
+ }
+
+ public UdonNode AddNodeFromSearch(UdonNodeDefinition definition, Vector2 position)
+ {
+ UdonNode node = UdonNode.CreateNode(definition, this);
+ AddElement(node);
+
+ node.SetPosition(new Rect(position, Vector2.zero));
+ node.Select(this, false);
+
+ return node;
+ }
+
+ public void ConnectNodeTo(UdonNode node, UdonPort startingPort, UnityGraph.Direction direction, Type typeToSearch)
+ {
+ // Find port to connect to
+ var collection = direction == Direction.Input ? node.portsIn : node.portsOut;
+ UdonPort endPort = collection.FirstOrDefault(p => p.Value.portType == typeToSearch).Value;
+ // If found, add edge and serialize the connection in the programAsset
+ if(endPort != null)
+ {
+ // Important not to create and add this edge, we'll restore it below instead
+ startingPort.ConnectTo(endPort);
+ (startingPort.node as UdonNode).RestoreConnections();
+ (endPort.node as UdonNode).RestoreConnections();
+ Compile();
+ }
+ }
+
+ public void SaveGraphToDisk()
+ {
+ if (graphProgramAsset == null)
+ return;
+
+ EditorUtility.SetDirty(graphProgramAsset);
+ }
+
+ private void OnKeyDown(KeyDownEvent evt)
+ {
+ if (evt.target == this && evt.keyCode == KeyCode.Tab)
+ {
+ var screenPosition = GUIUtility.GUIToScreenPoint(evt.originalMousePosition);
+ nodeCreationRequest(new NodeCreationContext() { screenMousePosition = screenPosition, target = this });
+ evt.StopImmediatePropagation();
+ }
+ else if (evt.keyCode == KeyCode.A && evt.ctrlKey)
+ {
+ // Select every graph element
+ ClearSelection();
+ foreach (var element in graphElements.ToList())
+ {
+ AddToSelection(element);
+ }
+ }
+ else if (evt.keyCode == KeyCode.G && evt.shiftKey)
+ {
+ Undo.RecordObject(graphProgramAsset, "Changed Name");
+ graphProgramAsset.graphData.name = Guid.NewGuid().ToString();
+ }
+ }
+
+ public bool GetBlackboardVisible()
+ {
+ return _blackboard.visible;
+ }
+
+ public bool GetMinimapVisible()
+ {
+ return _map.visible;
+ }
+
+ public void ToggleShowVariables(bool value)
+ {
+ _blackboard.SetVisible(value);
+ }
+
+ public void ToggleShowMiniMap(bool value)
+ {
+ _map.SetVisible(value);
+ }
+
+ public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
+ {
+ if (evt.target is UnityGraph.GraphView || evt.target is UdonNode)
+ {
+ // Create a Group, enclosing any selected nodes
+ evt.menu.AppendAction("Create Group", (m) =>
+ {
+ UdonGroup group = UdonGroup.Create("Group", GetRectFromMouse(), this);
+ Undo.RecordObject(graphProgramAsset, "Add Group");
+ AddElement(group);
+ group.UpdateDataId();
+
+ foreach (ISelectable item in selection)
+ {
+ if (item is UdonNode)
+ {
+ group.AddElement(item as UdonNode);
+ }
+ else if(item is UdonComment)
+ {
+ group.AddElement(item as UdonComment);
+ }
+ }
+ group.Initialize();
+ SaveGraphElementData(group);
+ }, MenuAction.AlwaysEnabled);
+ var selectedItems = selection.Where(i=>i is UdonNode || i is UdonComment).ToList();
+ if (selectedItems.Count > 0)
+ {
+ evt.menu.AppendAction("Remove From Group", (m) =>
+ {
+ Undo.RecordObject(graphProgramAsset, "Remove Items from Group");
+ int count = selectedItems.Count;
+ for (int i = count - 1; i >=0; i--)
+ {
+ if(selectedItems.ElementAt(i) is UdonNode)
+ {
+ UdonNode node = selectedItems.ElementAt(i) as UdonNode;
+ if (node.group != null)
+ {
+ node.group.RemoveElement(node);
+ }
+ }
+ else if (selectedItems.ElementAt(i) is UdonComment)
+ {
+ UdonComment comment = selectedItems.ElementAt(i) as UdonComment;
+ if (comment.group != null)
+ {
+ comment.group.RemoveElement(comment);
+ }
+ }
+ }
+
+ }, MenuAction.AlwaysEnabled);
+ }
+
+ // Create a Comment
+ evt.menu.AppendAction("Create Comment", (m) =>
+ {
+ UdonComment comment = UdonComment.Create("Comment", GetRectFromMouse(), this);
+ Undo.RecordObject(graphProgramAsset, "Add Comment");
+ AddElement(comment);
+ }, MenuAction.AlwaysEnabled);
+
+ evt.menu.AppendSeparator();
+ }
+
+ base.BuildContextualMenu(evt);
+ }
+
+ private Rect GetRectFromMouse()
+ {
+ return new Rect(contentViewContainer.WorldToLocal(lastMousePosition), Vector2.zero);
+ }
+
+ private void OnMouseMove(MouseMoveEvent evt)
+ {
+ lastMousePosition = evt.mousePosition;
+ MoveMouseTip(lastMousePosition);
+ }
+
+ private void MoveMouseTip(Vector2 position)
+ {
+ if (mouseTipContainer.visible)
+ {
+ var newLayout = mouseTipContainer.layout;
+ newLayout.position = position + mouseTipOffset;
+#if UNITY_2019_3_OR_NEWER
+ mouseTipContainer.transform.position = newLayout.position;
+#else
+ mouseTipContainer.layout = newLayout;
+#endif
+ }
+ }
+
+ public bool IsDuplicateEventNode(string fullName, string uid = "")
+ {
+ if (fullName.StartsWith("Event_") &&
+ (fullName != "Event_Custom")
+ )
+ {
+ if (this.Query(fullName).ToList().Count > 0)
+ {
+ Debug.LogWarning(
+ $"Can't create more than one {fullName} node, try managing your flow with a Block node instead!");
+ return true;
+ }
+ }
+ else if(fullName.StartsWith(Common.VariableChangedEvent.EVENT_PREFIX))
+ {
+ bool isDuplicate = graphData.EventNodes.Any(d =>
+ d.nodeValues.Length > 0 && d.nodeValues[0].Deserialize().ToString() == uid);
+ if (isDuplicate)
+ {
+ Debug.LogWarning(
+ $"Can't create more than one Change Event for {GetVariableName(uid)}, try managing your flow with a Block node instead!");
+ }
+ return isDuplicate;
+ }
+
+ return false;
+ }
+
+ private string OnSerializeGraphElements(IEnumerable<GraphElement> selection)
+ {
+ Bounds bounds = new Bounds();
+ bool startedBounds = false;
+ List<UdonNodeData> nodeData = new List<UdonNodeData>();
+ List<UdonNodeData> variables = new List<UdonNodeData>();
+ foreach (var item in selection)
+ {
+ // Only serializing UdonNode for now
+ if (item is UdonNode)
+ {
+ UdonNode node = (UdonNode)item;
+ // Calculate bounding box to enclose all items
+ if (!startedBounds)
+ {
+ bounds = new Bounds(node.data.position, Vector3.zero);
+ startedBounds = true;
+ }
+ else
+ {
+ bounds.Encapsulate(node.data.position);
+ }
+
+ // Handle Get/Set Variables
+ if (node.data.fullName == "Get_Variable" || node.data.fullName == "Set_Variable" || node.data.fullName == "Set_ReturnValue")
+ {
+ // make old-school get-variable node data from existing variable
+ var targetUid = node.data.nodeValues[0].Deserialize();
+ var matchingNode = GetVariableNodes.First(v => v.uid == (string)targetUid);
+ if (matchingNode != null && !variables.Contains(matchingNode))
+ {
+ variables.Add(matchingNode);
+ }
+ }
+
+ nodeData.Add(new UdonNodeData(node.data));
+ }
+ }
+
+ // Add variables at beginning of list so they get created first
+ nodeData.InsertRange(0, variables);
+
+ // Go through each item and offset its position by the center of the group (normalizes the coordinates around 0,0)
+ var offset = new Vector2(bounds.center.x, bounds.center.y);
+ foreach (UdonNodeData data in nodeData)
+ {
+ var ogPosition = data.position;
+ data.position -= offset;
+ }
+
+ string result = UdonGraphExtensions.ZipString(JsonUtility.ToJson(
+ new SerializableObjectContainer.ArrayWrapper<UdonNodeData>(nodeData.ToArray())));
+
+ return result;
+ }
+
+ private void OnUnserializeAndPaste(string operationName, string pasteData)
+ {
+ ClearSelection();
+
+ UdonNodeData[] copiedNodeDataArray;
+ // Note: CheckCanPaste already does this check but it doesn't cost much to do it twice
+ try
+ {
+ copiedNodeDataArray = JsonUtility
+ .FromJson<SerializableObjectContainer.ArrayWrapper<UdonNodeData>>(
+ UdonGraphExtensions.UnZipString(pasteData))
+ .value;
+ }
+ catch
+ {
+ //oof ouch that's not valid data
+ return;
+ }
+
+ var copiedNodeDataList = new List<UdonNodeData>();
+ // Add new variables if needed
+ for (int i = 0; i < copiedNodeDataArray.Length; i++)
+ {
+ if (copiedNodeDataArray[i].fullName.StartsWith("Variable_"))
+ {
+ if (!graphData.nodes.Exists(n => n.uid == copiedNodeDataArray[i].uid))
+ {
+ // set graph to this one in case it was pasted from somewhere else
+ copiedNodeDataArray[i].SetGraph(graphData);
+
+ // check for conflicting variable names
+ int nameIndex = (int)UdonParameterProperty.ValueIndices.name;
+ string varName = (string)copiedNodeDataArray[i].nodeValues[nameIndex].Deserialize();
+ if (GetVariableNames.Contains(varName))
+ {
+ // if we already have a variable with that name, find a new name and serialize it into the data
+ varName = GetUnusedVariableNameLike(varName);
+ copiedNodeDataArray[i].nodeValues[nameIndex] =
+ SerializableObjectContainer.Serialize(varName);
+ }
+
+ _blackboard.AddFromData(copiedNodeDataArray[i]);
+ graphData.nodes.Add(copiedNodeDataArray[i]);
+ }
+ }
+ else if(IsDuplicateEventNode(copiedNodeDataArray[i].fullName))
+ {
+ // don't add duplicate event nodes
+ }
+ else
+ {
+ copiedNodeDataList.Add(copiedNodeDataArray[i]);
+ }
+ }
+
+ // Remove duplicate events
+ RefreshVariables(false);
+
+ // copy modified list back to array
+ copiedNodeDataArray = copiedNodeDataList.ToArray();
+
+ _reloading = true;
+ var graphMousePosition = GetRectFromMouse().position;
+ List<UdonNode> pastedNodes = new List<UdonNode>();
+ Dictionary<string, string> uidMap = new Dictionary<string, string>();
+ UdonNodeData[] newNodeDataArray = new UdonNodeData[copiedNodeDataArray.Length];
+
+ for (int i = 0; i < copiedNodeDataArray.Length; i++)
+ {
+ UdonNodeData nodeData = copiedNodeDataArray[i];
+ newNodeDataArray[i] = new UdonNodeData(graphData, nodeData.fullName)
+ {
+ position = nodeData.position + graphMousePosition,
+ uid = Guid.NewGuid().ToString(),
+ nodeUIDs = new string[nodeData.nodeUIDs.Length],
+ nodeValues = nodeData.nodeValues,
+ flowUIDs = new string[nodeData.flowUIDs.Length]
+ };
+
+ uidMap.Add(nodeData.uid, newNodeDataArray[i].uid);
+ }
+
+ for (int i = 0; i < copiedNodeDataArray.Length; i++)
+ {
+ UdonNodeData nodeData = copiedNodeDataArray[i];
+ UdonNodeData newNodeData = newNodeDataArray[i];
+
+ // Set the new node to point at this graph if it came from a different one
+ newNodeData.SetGraph(graphData);
+
+ for (int j = 0; j < newNodeData.nodeUIDs.Length; j++)
+ {
+ if (uidMap.ContainsKey(nodeData.nodeUIDs[j].Split('|')[0]))
+ {
+ newNodeData.nodeUIDs[j] = uidMap[nodeData.nodeUIDs[j].Split('|')[0]];
+ }
+ }
+
+ for (int j = 0; j < newNodeData.flowUIDs.Length; j++)
+ {
+ if (uidMap.ContainsKey(nodeData.flowUIDs[j].Split('|')[0]))
+ {
+ newNodeData.flowUIDs[j] = uidMap[nodeData.flowUIDs[j].Split('|')[0]];
+ }
+ }
+
+ UdonNode udonNode = UdonNode.CreateNode(newNodeData, this);
+ if (udonNode != null)
+ {
+ graphData.nodes.Add(newNodeData);
+ AddElement(udonNode);
+ pastedNodes.Add(udonNode);
+ }
+ }
+
+ // Select all newly-pasted nodes after reload
+ foreach (var item in pastedNodes)
+ {
+ item.RestoreConnections();
+ item.BringToFront();
+ AddToSelection(item as GraphElement);
+ }
+
+ _reloading = false;
+ Compile();
+ }
+
+ // This is needed to properly clear the selection in some cases (like deleting a stack node) even though it doesn't appear to do anything
+ public override void ClearSelection()
+ {
+ base.ClearSelection();
+ }
+
+ public void MarkSceneDirty()
+ {
+ if (!EditorApplication.isPlaying)
+ {
+ EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
+ }
+ }
+
+ private GraphViewChange OnViewChanged(GraphViewChange changes)
+ {
+ bool dirty = false;
+ bool needsVariableRefresh = false;
+ // Remove node from Data when removed from Graph
+ if (!_reloading && changes.elementsToRemove != null && changes.elementsToRemove.Count > 0)
+ {
+
+ foreach (var element in changes.elementsToRemove)
+ {
+ if (element is UdonNode)
+ {
+ var nodeData = ((UdonNode)element).data;
+ RemoveNodeAndData(nodeData);
+ continue;
+ }
+
+ if (element is Edge)
+ {
+ Undo.RecordObject(graphProgramAsset, $"delete-{element.name}");
+ continue;
+ }
+
+ if (element is UdonParameterField)
+ {
+ needsVariableRefresh = true;
+
+ var pField = element as UdonParameterField;
+ if (graphData.nodes.Contains(pField.Data))
+ {
+ RemoveNodeAndData(pField.Data);
+ }
+ }
+
+ if (element is IUdonGraphElementDataProvider)
+ {
+ Undo.RecordObject(graphProgramAsset, $"delete-{element.name}");
+ var provider = (IUdonGraphElementDataProvider) element;
+ DeleteGraphElementData(provider, false);
+ RemoveElement(element);
+ }
+ }
+
+ ClearSelection();
+ dirty = true;
+ }
+
+ if (dirty)
+ {
+ MarkSceneDirty();
+ SaveGraphToDisk();
+ }
+
+ if (needsVariableRefresh)
+ {
+ RefreshVariables(true);
+ }
+
+ return changes;
+ }
+
+ public void DoDelayedCompile()
+ {
+ EditorApplication.update += DelayedCompile;
+ }
+
+ private void DelayedCompile()
+ {
+ EditorApplication.update -= DelayedCompile;
+ graphProgramAsset.RefreshProgram();
+ }
+
+ private bool _waitingToReload;
+ public void DoDelayedReload()
+ {
+ if (!_waitingToReload && !_reloading)
+ {
+ _waitingToReload = true;
+ EditorApplication.update += DelayedReload;
+ }
+ }
+
+ void DelayedReload()
+ {
+ _waitingToReload = false;
+ EditorApplication.update -= DelayedReload;
+ Reload();
+ }
+
+ private void SetupBackground()
+ {
+ _background = new GridBackground
+ {
+ name = "bg"
+ };
+ Insert(0, _background);
+ _background.StretchToParentSize();
+ }
+
+ private void SetupBlackboard()
+ {
+ _blackboard = new UdonVariablesBlackboard(this);
+
+ _blackboard.addItemRequested = BlackboardAddVariable;
+ _blackboard.editTextRequested = BlackboardEditVariableName;
+ _blackboard.SetPosition(new Rect(10, 130, 200, 150));
+ Add(_blackboard);
+ }
+
+ private void BlackboardEditVariableName(Blackboard b, VisualElement v, string newValue)
+ {
+ UdonParameterField field = (UdonParameterField) v;
+ Undo.RecordObject(graphProgramAsset, "Rename Variable");
+
+ // Sanitize value for variable name
+ string newVariableName = newValue.SanitizeVariableName();
+ newVariableName = GetUnusedVariableNameLike(newVariableName);
+ field.Data.nodeValues[(int)UdonParameterProperty.ValueIndices.name] = SerializableObjectContainer.Serialize(newVariableName);
+ field.text = newVariableName;
+
+ // Find all nodes that are getters/setters for this variable
+ // Change their title text by hand
+ nodes.ForEach((node =>
+ {
+ UdonNode udonNode = (UdonNode) node;
+ if (udonNode != null && udonNode.IsVariableNode)
+ {
+ udonNode.RefreshTitle();
+ }
+ }));
+
+ RefreshVariables(true);
+ }
+
+ private void BlackboardAddVariable(Blackboard obj)
+ {
+ var screenPosition = GUIUtility.GUIToScreenPoint(lastMousePosition);
+ _searchManager.OpenVariableSearch(screenPosition);
+ }
+
+ public void OpenPortSearch(Type type, Vector2 position, UdonPort output, Direction direction)
+ {
+ _searchManager.OpenPortSearch(type, position, output, direction);
+ }
+
+ private void SetupMap()
+ {
+ _map = new UdonMinimap(this);
+ Add(_map);
+ }
+
+ private void OnExecuteCommand(ExecuteCommandEvent evt)
+ {
+ switch (evt.commandName)
+ {
+ case UdonGraphCommands.Reload:
+ DoDelayedReload();
+ break;
+ case UdonGraphCommands.Compile:
+ Compile();
+ break;
+ default:
+ break;
+ }
+ }
+
+ public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
+ {
+ var result = ports.ToList().Where(
+ port => port.direction != startPort.direction
+ && port.node != startPort.node
+ && port.portType.IsReallyAssignableFrom(startPort.portType)
+ && (port.capacity == Port.Capacity.Multi || port.connections.Count() == 0)
+ ).ToList();
+ return result;
+ }
+
+#if UNITY_2019_3_OR_NEWER
+ private StyleSheet neonStyle = (StyleSheet) Resources.Load("UdonGraphNeonStyle");
+#endif
+
+ public void Reload()
+ {
+ if (_reloading) return;
+
+ _reloading = true;
+
+#if UNITY_2019_3_OR_NEWER
+
+ if (Settings.UseNeonStyle && !styleSheets.Contains(neonStyle))
+ {
+ styleSheets.Add(neonStyle);
+ }
+ else if (!Settings.UseNeonStyle && !styleSheets.Contains(neonStyle))
+ {
+ styleSheets.Remove(neonStyle);
+ }
+#else
+ string customStyle = "UdonGraphNeonStyle";
+ if (Settings.UseNeonStyle && !HasStyleSheetPath(customStyle))
+ {
+ AddStyleSheetPath(customStyle);
+ }
+ else if (!Settings.UseNeonStyle && HasStyleSheetPath(customStyle))
+ {
+ RemoveStyleSheetPath(customStyle);
+ }
+#endif
+ Undo.undoRedoPerformed -=
+ OnUndoRedo; //Remove old handler if present to prevent duplicates, doesn't cause errors if not present
+ Undo.undoRedoPerformed += OnUndoRedo;
+
+ // Clear out Blackboard here
+ _blackboard.Clear();
+
+ // clear existing elements, probably need to update to only clear nodes and edges
+ DeleteElements(graphElements.ToList());
+
+ RefreshVariables(false);
+
+ List<UdonNodeData> nodesToDelete = new List<UdonNodeData>();
+ // add all nodes to graph
+ for (int i = 0; i < graphData.nodes.Count; i++)
+ {
+ UdonNodeData nodeData = graphData.nodes[i];
+
+ // Check for Node type - create nodes, separate out Variables
+ if (nodeData.fullName.StartsWithCached("Variable_"))
+ {
+ _blackboard.AddFromData(nodeData);
+ }
+ else if (nodeData.fullName.StartsWithCached("Comment"))
+ {
+ // one way conversion from Comment Node > Comment Group
+ var commentString = nodeData.nodeValues[0].Deserialize();
+ if (commentString != null)
+ {
+ var comment = UdonComment.Create((string) commentString,
+ new Rect(nodeData.position, Vector2.zero), this);
+ AddElement(comment);
+ SaveGraphElementData(comment);
+ }
+
+ // Remove from data, no longer a node
+ nodesToDelete.Add(nodeData);
+ }
+ else
+ {
+ try
+ {
+ UdonNode udonNode = UdonNode.CreateNode(nodeData, this);
+ AddElement(udonNode);
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"Error Loading Node {nodeData.fullName} : {e.Message}");
+ nodesToDelete.Add(nodeData);
+ continue;
+ }
+ }
+ }
+
+ // Delete old comments and data that could not be turned into an UdonNode
+ foreach (UdonNodeData nodeData in nodesToDelete)
+ {
+ if (graphData.nodes.Remove(nodeData))
+ {
+ Debug.Log($"removed node {nodeData.fullName}");
+ }
+ }
+
+ // reconnect nodes
+ nodes.ForEach((genericNode) =>
+ {
+ UdonNode udonNode = (UdonNode)genericNode;
+ udonNode.RestoreConnections();
+ });
+
+ // Add all Graph Elements
+ if (graphProgramAsset.graphElementData != null)
+ {
+ var orderedElements = graphProgramAsset.graphElementData.ToList().OrderByDescending(e => e.type);
+ foreach (var elementData in orderedElements)
+ {
+ GraphElement element = RestoreElementFromData(elementData);
+ if (element != null)
+ {
+ AddElement(element);
+ if (element is UdonGroup group)
+ {
+ group.Initialize();
+ }
+ }
+ }
+ }
+
+ _reloading = false;
+ Compile();
+ }
+
+ // TODO: create generic to restore any supported element from UdonGraphElementData?
+ private GraphElement RestoreElementFromData(UdonGraphElementData data)
+ {
+ switch (data.type)
+ {
+ case UdonGraphElementType.GraphElement:
+ {
+ return null;
+ }
+
+ case UdonGraphElementType.UdonGroup:
+ {
+ return UdonGroup.Create(data, this);
+ }
+
+ case UdonGraphElementType.UdonComment:
+ {
+ return UdonComment.Create(data, this);
+ }
+ case UdonGraphElementType.Minimap:
+ {
+ _map.LoadData(data);
+ return null;
+ }
+ case UdonGraphElementType.VariablesWindow:
+ {
+ _blackboard.LoadData(data);
+ return null;
+ }
+ default:
+ return null;
+ }
+ }
+
+ private void OnUndoRedo()
+ {
+ Reload();
+ }
+
+ public void RefreshVariables(bool recompile = true)
+ {
+ // we want internal variables at the end of the list so they can be trivially filtered out
+ _variableNodes = graphData.nodes
+ .Where(n => n.fullName.StartsWithCached("Variable_"))
+ .Where(n => n.nodeValues.Length > 1 && n.nodeValues[1] != null)
+ .OrderBy(n => ((string)n.nodeValues[1].Deserialize()).StartsWith("__"))
+ .ToList();
+ _variableNames = ImmutableList.Create(
+ _variableNodes.Select(s => (string) s.nodeValues[1].Deserialize()).ToArray()
+ );
+
+ // Refresh variable options in popup
+ nodes.ForEach(node =>
+ {
+ if (node is UdonNode udonNode && udonNode.IsVariableNode)
+ {
+ udonNode.RefreshVariablePopup();
+ }
+ });
+
+ // We usually want to compile after a Refresh
+ if(recompile)
+ Compile();
+ DoDelayedReload();
+ }
+
+ // Returns UID of newly created variable
+ public string AddNewVariable(string typeName = "Variable_SystemString", string variableName = "",
+ bool isPublic = false)
+ {
+ // Figure out unique variable name to use
+ string newVariableName = string.IsNullOrEmpty(variableName) ? "newVariable" : variableName;
+ newVariableName = GetUnusedVariableNameLike(newVariableName);
+
+ string newVarUid = Guid.NewGuid().ToString();
+ UdonNodeData newNodeData = new UdonNodeData(graphData, typeName)
+ {
+ uid = newVarUid,
+ nodeUIDs = new string[5],
+ nodeValues = new[]
+ {
+ SerializableObjectContainer.Serialize(default),
+ SerializableObjectContainer.Serialize(newVariableName, typeof(string)),
+ SerializableObjectContainer.Serialize(isPublic, typeof(bool)),
+ SerializableObjectContainer.Serialize(false, typeof(bool)),
+ SerializableObjectContainer.Serialize("none", typeof(string))
+ },
+ position = Vector2.zero
+ };
+
+ graphData.nodes.Add(newNodeData);
+ _blackboard.AddFromData(newNodeData);
+ RefreshVariables(true);
+ return newVarUid;
+ }
+
+ public void RemoveNodeAndData(UdonNodeData nodeData)
+ {
+ Undo.RecordObject(graphProgramAsset, $"Removing {nodeData.fullName}");
+
+ if (nodeData.fullName.StartsWithCached("Variable_"))
+ {
+ var allVariableNodes = new HashSet<Node>();
+ // Find all get/set variable nodes that reference this node
+ nodes.ForEach((graphNode =>
+ {
+ UdonNode udonNode = graphNode as UdonNode;
+ if (udonNode != null && udonNode.IsVariableNode)
+ {
+ // Get variable uid and recursively remove all nodes that refer to it
+ var values = udonNode.data.nodeValues[0].stringValue.Split('|');
+ if (values.Length > 1)
+ {
+ string targetVariable = values[1];
+ if (targetVariable.CompareTo(nodeData.uid) == 0)
+ {
+ // We have a match! Delete this node
+ allVariableNodes.Add(graphNode);
+ RemoveNodeAndData(udonNode.data);
+ }
+ }
+ }
+ }));
+
+ // remove each edge connected to a Get/Set Variable node which will be deleted
+ edges.ForEach(edge =>
+ {
+ if (allVariableNodes.Contains(edge.input.node) || allVariableNodes.Contains(edge.output.node))
+ {
+ (edge.output as UdonPort)?.Disconnect(edge);
+ (edge.input as UdonPort)?.Disconnect(edge);
+ RemoveElement(edge);
+ }
+ });
+
+ // remove from existing blackboard
+ _blackboard.RemoveByID(nodeData.uid);
+ RefreshVariables(true);
+ }
+
+ UdonNode node = (UdonNode)GetNodeByGuid(nodeData.uid);
+ if (node != null)
+ {
+ node.RemoveFromHierarchy();
+ }
+
+ if (graphData.nodes.Contains(nodeData))
+ {
+ graphData.nodes.Remove(nodeData);
+ }
+ }
+
+ public void Compile()
+ {
+ UdonEditorManager.Instance.QueueAndRefreshProgram(graphProgramAsset);
+ }
+
+ private bool ShouldUpdateAsset => !IsReloading && graphProgramAsset != null;
+
+ private readonly HashSet<UdonGraphElementType> singleElementTypes = new HashSet<UdonGraphElementType>()
+ {
+ UdonGraphElementType.Minimap, UdonGraphElementType.VariablesWindow
+ };
+
+ public void SaveGraphElementData(IUdonGraphElementDataProvider provider)
+ {
+ if (ShouldUpdateAsset)
+ {
+ UdonGraphElementData newData = provider.GetData();
+ if (graphProgramAsset.graphElementData == null)
+ {
+ graphProgramAsset.graphElementData = new UdonGraphElementData[0];
+ }
+
+ int index = -1;
+ // Some elements like minimap and variables window should only ever have one entry, so find by type
+ if (singleElementTypes.Contains(newData.type))
+ {
+ index = Array.FindIndex(graphProgramAsset.graphElementData, e => e.type == newData.type);
+ }
+ // other elements can have multiples, so find by uid
+ else
+ {
+ index = Array.FindIndex(graphProgramAsset.graphElementData, e => e.uid == newData.uid);
+ }
+ if (index > -1)
+ {
+ // Update
+ graphProgramAsset.graphElementData[index] = newData;
+ }
+ else
+ {
+ // Add
+ int arrayLength = graphProgramAsset.graphElementData.Length;
+ Array.Resize(ref graphProgramAsset.graphElementData, arrayLength+1);
+ graphProgramAsset.graphElementData[arrayLength] = newData;
+ }
+ SaveGraphToDisk();
+ }
+ }
+
+ public void DeleteGraphElementData(IUdonGraphElementDataProvider provider, bool save = true)
+ {
+ int index = Array.FindIndex(graphProgramAsset.graphElementData, e => e.uid == provider.GetData().uid);
+ // remove if found
+ if (index > -1)
+ {
+ graphProgramAsset.graphElementData = graphProgramAsset.graphElementData.Where((source, i) => i != index).ToArray();
+ }
+
+ if (save)
+ {
+ SaveGraphToDisk();
+ }
+ }
+
+ #region Drag and Drop Support
+
+ private void SetupDragAndDrop()
+ {
+ RegisterCallback<DragEnterEvent>(OnDragEnter);
+ RegisterCallback<DragPerformEvent>(OnDragPerform, TrickleDown.TrickleDown);
+ RegisterCallback<DragUpdatedEvent>(OnDragUpdated);
+ RegisterCallback<DragExitedEvent>(OnDragExited);
+ RegisterCallback<DragLeaveEvent>((e)=>OnDragExited(null));
+ }
+
+ private void OnDragEnter(DragEnterEvent e)
+ {
+ OnDragEnter(e.mousePosition, e.ctrlKey, e.altKey);
+ }
+
+ private void OnDragEnter(Vector2 mousePosition, bool ctrlKey, bool altKey)
+ {
+ MoveMouseTip(mousePosition);
+
+ var dragData = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>;
+ _dragging = false;
+
+ if (dragData != null)
+ {
+ // Handle drag from exposed parameter view
+ if (dragData.OfType<UdonParameterField>().Any())
+ {
+ _dragging = true;
+ string tip = "Get Variable\n+Ctrl: Set Variable\n+Alt: On Var Change";
+ if (ctrlKey)
+ {
+ tip = "Set Variable";
+ } else if (altKey)
+ {
+ tip = "On Variable Changed";
+ }
+ SetMouseTip(tip);
+ }
+ }
+
+ if (DragAndDrop.objectReferences.Length == 1 && DragAndDrop.objectReferences[0] != null)
+ {
+ var target = DragAndDrop.objectReferences[0];
+ switch (target)
+ {
+ case GameObject g:
+ case Component c:
+ {
+ string type = GetDefinitionNameForType(target.GetType());
+ if (UdonEditorManager.Instance.GetNodeDefinition(type) != null)
+ {
+ _dragging = true;
+ }
+ break;
+ }
+ }
+ }
+
+ if (_dragging)
+ {
+ DragAndDrop.visualMode = ctrlKey ? DragAndDropVisualMode.Link : DragAndDropVisualMode.Copy;
+ }
+ }
+
+ private void OnDragUpdated(DragUpdatedEvent e)
+ {
+ if (_dragging)
+ {
+ MoveMouseTip(e.mousePosition);
+ DragAndDrop.visualMode = e.ctrlKey ? DragAndDropVisualMode.Link : DragAndDropVisualMode.Copy;
+ }
+ else
+ {
+ OnDragEnter(e.mousePosition, e.ctrlKey, e.altKey);
+ }
+ }
+
+ private void OnDragPerform(DragPerformEvent e)
+ {
+ if (!_dragging) return;
+ var graphMousePosition = this.contentViewContainer.WorldToLocal(e.mousePosition);
+ var draggedVariables = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>;
+
+ if (draggedVariables != null)
+ {
+ // Handle Drop of Variables
+ var parameters = draggedVariables.OfType<UdonParameterField>();
+ if (parameters.Any())
+ {
+ RefreshVariables(false);
+ VariableNodeType nodeType = VariableNodeType.Getter;
+ if (e.ctrlKey) nodeType = VariableNodeType.Setter;
+ else if (e.altKey) nodeType = VariableNodeType.Change;
+ foreach (var parameter in parameters)
+ {
+ UdonNode udonNode = MakeVariableNode(parameter.Data.uid, graphMousePosition, nodeType);
+ if (udonNode != null)
+ {
+ AddElement(udonNode);
+ }
+ }
+ RefreshVariables(true);
+ }
+ }
+
+ // Handle Drop of single GameObjects and Assets
+ if (DragAndDrop.objectReferences.Length == 1 && DragAndDrop.objectReferences[0] != null)
+ {
+ var target = DragAndDrop.objectReferences[0];
+ switch (target)
+ {
+ case Component c:
+ SetupDraggedObject(c, graphMousePosition);
+ break;
+
+ case GameObject g:
+ SetupDraggedObject(g, graphMousePosition);
+ break;
+ }
+ }
+
+ _dragging = false;
+ }
+
+ private void OnDragExited(DragExitedEvent e)
+ {
+ SetMouseTip("");
+ _dragging = false;
+ }
+
+ #endregion
+ public enum VariableNodeType
+ {
+ Getter,
+ Setter,
+ Return,
+ Change,
+ }
+
+ public string GetVariableName(string uid)
+ {
+ var targetNode = GetVariableNodes.Where(n => n.uid == uid).First();
+ try
+ {
+ return targetNode.nodeValues[1].Deserialize() as string;
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"Couldn't find variable name for {uid}: {e.Message}");
+ return "";
+ }
+ }
+
+ public UdonNode MakeVariableNode(string selectedUid, Vector2 graphMousePosition, VariableNodeType nodeType)
+ {
+ string definitionName = "";
+ switch (nodeType)
+ {
+ case VariableNodeType.Getter:
+ definitionName = "Get_Variable";
+ break;
+ case VariableNodeType.Setter:
+ definitionName = "Set_Variable";
+ break;
+ case VariableNodeType.Return:
+ definitionName = "Set_ReturnValue";
+ break;
+ case VariableNodeType.Change:
+ definitionName = "Event_OnVariableChange";
+ break;
+ }
+
+ if (nodeType == VariableNodeType.Change)
+ {
+ string variableName = GetVariableName(selectedUid);
+ if (!string.IsNullOrWhiteSpace(variableName))
+ {
+ string eventName = UdonGraphExtensions.GetVariableChangeEventName(variableName);
+ if (IsDuplicateEventNode(eventName, selectedUid))
+ {
+ return null;
+ }
+ }
+ }
+
+ var definition = UdonEditorManager.Instance.GetNodeDefinition(definitionName);
+ var nodeData = this.graphData.AddNode(definition.fullName);
+ nodeData.nodeValues = new SerializableObjectContainer[2];
+ nodeData.nodeUIDs = new string[1];
+ nodeData.nodeValues[0] = SerializableObjectContainer.Serialize(selectedUid);
+ nodeData.position = graphMousePosition;
+
+ Undo.RecordObject(graphProgramAsset, "Add Variable");
+ var udonNode = UdonNode.CreateNode(nodeData, this);
+ return udonNode;
+ }
+
+ public string GetUnusedVariableNameLike(string newVariableName)
+ {
+ RefreshVariables(false);
+
+ while (GetVariableNames.Contains(newVariableName))
+ {
+ char lastChar = newVariableName[newVariableName.Length - 1];
+ if(char.IsDigit(lastChar))
+ {
+ string newLastChar = (int.Parse(lastChar.ToString()) + 1).ToString();
+ newVariableName = newVariableName.Substring(0, newVariableName.Length - 1) + newLastChar;
+ }
+ else
+ {
+ newVariableName = $"{newVariableName}_1";
+ }
+ }
+
+ return newVariableName;
+ }
+
+ private void SetMouseTip(string message)
+ {
+ if (mouseTipContainer.visible)
+ {
+ mouseTip.text = message;
+ }
+ }
+
+ private void LinkAfterCompile(string variableName, object target)
+ {
+ UdonAssemblyProgramAsset.AssembleDelegate listener = null;
+ listener = (success, assembly) =>
+ {
+ if (!success) return;
+
+ //TODO: get actual variable name in case it was auto-changed on add
+ var result = udonBehaviour.publicVariables.TrySetVariableValue(variableName, target);
+ if (result)
+ {
+ graphProgramAsset.OnAssemble -= listener;
+ }
+ };
+
+ graphProgramAsset.OnAssemble += listener;
+ EditorUtility.SetDirty(graphProgramAsset);
+ AssetDatabase.SaveAssets();
+ graphProgramAsset.RefreshProgram();
+ }
+
+ private string GetDefinitionNameForType(Type t)
+ {
+ string variableType = $"Variable_{t}".SanitizeVariableName();
+ variableType = variableType.Replace("UdonBehaviour", "CommonInterfacesIUdonEventReceiver");
+ return variableType;
+ }
+
+ private void SetupDraggedObject(UnityEngine.Object o, Vector2 graphMousePosition)
+ {
+ // Ensure variable type is allowed
+
+ // create new Component variable and add to graph
+ string variableType = GetDefinitionNameForType(o.GetType());
+ string variableName = GetUnusedVariableNameLike(o.name.SanitizeVariableName());
+
+ SetMouseTip($"Made {variableName}");
+
+ string uid = AddNewVariable(variableType, variableName, true);
+ RefreshVariables(false);
+
+ object target = o;
+ // Cast component to expected type
+ if (o is Component) target = Convert.ChangeType(o, o.GetType());
+ var variableNode = MakeVariableNode(uid, graphMousePosition, UdonGraph.VariableNodeType.Getter);
+ AddElement(variableNode);
+
+ LinkAfterCompile(variableName, target);
+ }
+
+ [Serializable]
+ public class ViewTransformData
+ {
+ public Vector2 position = Vector2.zero;
+ public float scale = 1f;
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs.meta
new file mode 100644
index 00000000..62ff2be8
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraph.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9214873dab0ea8a4b91861cd5a04dae3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs
new file mode 100644
index 00000000..2a226b20
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram
+{
+ [Serializable]
+ public class UdonGraphElementData
+ {
+ public UdonGraphElementType type;
+ public string uid;
+ public string jsonData;
+
+ public UdonGraphElementData(UdonGraphElementType type, string uid, string jsonData)
+ {
+ this.type = type;
+ this.jsonData = jsonData;
+ this.uid = uid;
+ }
+ }
+
+ public enum UdonGraphElementType
+ {
+ GraphElement,
+ UdonStackNode,
+ UdonGroup,
+ UdonComment,
+ Minimap,
+ VariablesWindow,
+ };
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs.meta
new file mode 100644
index 00000000..23bdc696
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphElementData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f166d8f1c152ef34899019ab9a4fd0f2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs
new file mode 100644
index 00000000..6c0a1847
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs
@@ -0,0 +1,144 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEngine.UIElements;
+using UnityEditor.UIElements;
+#else
+using UnityEngine.Experimental.UIElements;
+using UnityEditor.Experimental.UIElements;
+#endif
+using UnityEditor;
+using UnityEngine;
+
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonGraphStatus : ToolbarToggle
+ {
+ private VisualElement _detailsContainer;
+ private UdonProgramSourceView _programSourceView;
+ private UdonGraphProgramAsset _graphAsset;
+ private TextElement _label;
+
+ public UdonGraphStatus(VisualElement detailsContainer)
+ {
+ _detailsContainer = detailsContainer;
+
+ _label = new TextElement() {text = "-", name = "Content"};
+ _label.StretchToParentSize();
+ Add(_label);
+#if UNITY_2019_3_OR_NEWER
+ this.RegisterValueChangedCallback(ShowGraphAssetDetails);
+#else
+ OnValueChanged(ShowGraphAssetDetails);
+#endif
+
+ _programSourceView = new UdonProgramSourceView();
+ }
+
+ private void ShowGraphAssetDetails(ChangeEvent<bool> changeEvent)
+ {
+ // Just remove it
+ if (!changeEvent.newValue)
+ {
+ RemoveIfContaining(_programSourceView);
+ return;
+ }
+
+ // else we need to make a new one and show it
+ if (_graphAsset == null)
+ {
+ Debug.LogError("Can't show Asset Details for a null asset.");
+ return;
+ }
+
+ RemoveIfContaining(_programSourceView);
+ _detailsContainer.Add(_programSourceView);
+ _programSourceView.BringToFront();
+ }
+
+ private void RemoveIfContaining(VisualElement element)
+ {
+ if (_detailsContainer.Contains(element))
+ {
+ _detailsContainer.Remove(element);
+ }
+ }
+
+ public void OnAssemble(bool success, string assembly)
+ {
+ if (!enabledInHierarchy || _label == null) return;
+
+ string newText;
+ Color flashColor;
+ Color targetColor;
+
+ // change visuals based on success
+ if (success)
+ {
+ newText = "OK";
+ flashColor = new Color(0, 1, 0);
+ targetColor = new Color(0.1f, 0.25f, 0.1f, alpha);
+ }
+ else
+ {
+ newText = "!";
+ flashColor = new Color(1, 0, 0);
+ targetColor = new Color(0.25f, 0.1f, 0.1f, alpha);
+ }
+
+ // Update visuals
+ _label.text = newText;
+ _label.style.backgroundColor = flashColor;
+ LerpColor(targetColor);
+ _programSourceView.SetText(assembly);
+ }
+
+ private Color targetColor;
+ private Color startColor;
+ private float lerpAmount = 0f;
+ private float alpha = 0.75f;
+
+ private void LerpColor(Color newColor)
+ {
+ startColor = _label.style.backgroundColor.value;
+ targetColor = newColor;
+ lerpAmount = 0f;
+ EditorApplication.update += UpdateColor;
+ }
+
+ private void UpdateColor()
+ {
+ lerpAmount += 0.01f;
+
+ if (lerpAmount >= 1)
+ {
+ EditorApplication.update -= UpdateColor;
+ }
+
+ if (_label != null)
+ {
+ _label.style.backgroundColor = Color.Lerp(startColor, targetColor, lerpAmount);
+ }
+ }
+
+ public override void Blur()
+ {
+ EditorApplication.update -= UpdateColor;
+ }
+
+ ~UdonGraphStatus()
+ {
+ EditorApplication.update -= UpdateColor;
+ if (_graphAsset != null)
+ {
+ _graphAsset.OnAssemble -= OnAssemble;
+ }
+ }
+
+ internal void LoadAsset(UdonGraphProgramAsset asset)
+ {
+ _graphAsset = asset;
+ _graphAsset.OnAssemble += OnAssemble;
+ _programSourceView.LoadAsset(asset);
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs.meta
new file mode 100644
index 00000000..4d7c23c0
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphStatus.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 54dd824c6c614b94183d92710efe4f5f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs
new file mode 100644
index 00000000..3bce7881
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs
@@ -0,0 +1,58 @@
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public static class Settings
+ {
+ private static string UseNeonStyleString = "UdonGraphViewSettings.UseNeonStyle";
+ private static string LastGraphGuidString = "UdonGraphViewSettings.LastGraphGuid";
+ private static string LastUdonBehaviourPathString = "UdonGraphViewSettings.LastUdonBehaviourPath";
+ private static string LastUdonBehaviourScenePathString = "UdonGraphViewSettings.LastUdonBehaviourScenePath";
+ private static string SearchOnSelectedNodeRegistryString = "UdonGraphViewSettings.SearchOnSelectedNodeRegistry";
+ private static string GridSnapSizeString = "UdonGraphViewSettings.GridSnapSize";
+ private static string SearchOnNoodleDropString = "UdonGraphViewSettings.SearchOnNoodleDrop";
+
+ public static bool UseNeonStyle
+ {
+ get { return PlayerPrefs.GetInt(UseNeonStyleString, 0) == 1; }
+ set { PlayerPrefs.SetInt(UseNeonStyleString, value ? 1 : 0); }
+ }
+
+ public static string LastGraphGuid
+ {
+ get { return PlayerPrefs.GetString(LastGraphGuidString, ""); }
+ set { PlayerPrefs.SetString(LastGraphGuidString, value); }
+ }
+
+ public static string LastUdonBehaviourPath
+ {
+ get { return PlayerPrefs.GetString(LastUdonBehaviourPathString, ""); }
+ set { PlayerPrefs.SetString(LastUdonBehaviourPathString, value); }
+ }
+
+ public static string LastUdonBehaviourScenePath
+ {
+ get { return PlayerPrefs.GetString(LastUdonBehaviourScenePathString, ""); }
+ set { PlayerPrefs.SetString(LastUdonBehaviourScenePathString, value); }
+ }
+
+ public static bool SearchOnSelectedNodeRegistry
+ {
+ get { return PlayerPrefs.GetInt(SearchOnSelectedNodeRegistryString, 1) == 1; }
+ set { PlayerPrefs.SetInt(SearchOnSelectedNodeRegistryString, value ? 1 : 0); }
+ }
+
+ public static int GridSnapSize
+ {
+ get { return PlayerPrefs.GetInt(GridSnapSizeString, 0); }
+ set { PlayerPrefs.SetInt(GridSnapSizeString, value); }
+ }
+
+ public static bool SearchOnNoodleDrop
+ {
+ get { return PlayerPrefs.GetInt(SearchOnNoodleDropString, 1) == 1; }
+ set { PlayerPrefs.SetInt(SearchOnNoodleDropString, value ? 1 : 0); }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs.meta
new file mode 100644
index 00000000..1b726b27
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphViewSettings.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 87e2044d3bcb715499ac68cc7380a9ed
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs
new file mode 100644
index 00000000..70b655c3
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs
@@ -0,0 +1,324 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEngine.UIElements;
+using UnityEditor.UIElements;
+#else
+using UnityEngine.Experimental.UIElements;
+using UnityEditor.Experimental.UIElements;
+using UnityEngine.Experimental.UIElements.StyleEnums;
+#endif
+using System.Linq;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using VRC.Udon.Graph;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonGraphWindow : EditorWindow
+ {
+ private VisualElement _rootView;
+
+ // Reference to actual Graph View
+ [SerializeField] private UdonGraph _graphView;
+
+ private UdonGraphProgramAsset _graphAsset;
+ private UdonWelcomeView _welcomeView;
+ private VisualElement _curtain;
+
+ // Toolbar and Buttons
+ private Toolbar _toolbar;
+ private Label _graphAssetName;
+ private ToolbarMenu _toolbarOptions;
+ private UdonGraphStatus _graphStatus;
+ private ToolbarButton _graphReload;
+ private ToolbarButton _graphCompile;
+ private VisualElement _updateOrderField;
+ private IntegerField _updateOrderIntField;
+
+ [MenuItem("VRChat SDK/Udon Graph")]
+ private static void ShowWindow()
+ {
+ // Get or focus the window
+ var window = GetWindow<UdonGraphWindow>("Udon Graph", true, typeof(SceneView));
+ window.titleContent = new GUIContent("Udon Graph");
+ }
+
+ private void LogPlayModeState(PlayModeStateChange state)
+ {
+ switch (state)
+ {
+ case PlayModeStateChange.EnteredEditMode:
+ if (_rootView.Contains(_curtain))
+ {
+ _curtain.RemoveFromHierarchy();
+ }
+
+ break;
+ case PlayModeStateChange.ExitingEditMode:
+ break;
+ case PlayModeStateChange.EnteredPlayMode:
+ _rootView.Add(_curtain);
+ break;
+ case PlayModeStateChange.ExitingPlayMode:
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void OnEnable()
+ {
+ EditorApplication.playModeStateChanged += LogPlayModeState;
+
+ InitializeRootView();
+
+ _curtain = new VisualElement()
+ {
+ name = "curtain",
+ };
+ _curtain.Add(new Label("Graph Locked in Play Mode"));
+
+ _welcomeView = new UdonWelcomeView();
+ _welcomeView.StretchToParentSize();
+ _rootView.Add(_welcomeView);
+
+ SetupToolbar();
+
+ Undo.undoRedoPerformed -=
+ OnUndoRedo; //Remove old handler if present to prevent duplicates, doesn't cause errors if not present
+ Undo.undoRedoPerformed += OnUndoRedo;
+
+ if (_graphAsset != null)
+ {
+ UdonBehaviour udonBehaviour = null;
+ string gPath = Settings.LastUdonBehaviourPath;
+ string sPath = Settings.LastUdonBehaviourScenePath;
+ if (!string.IsNullOrEmpty(gPath) && !string.IsNullOrEmpty(sPath))
+ {
+ var targetScene = EditorSceneManager.GetSceneByPath(sPath);
+ if (targetScene != null && targetScene.isLoaded && targetScene.IsValid())
+ {
+ var targetObject = GameObject.Find(gPath);
+ if (targetObject != null)
+ {
+ udonBehaviour = targetObject.GetComponent<UdonBehaviour>();
+ }
+ }
+ }
+
+ InitializeGraph(_graphAsset, udonBehaviour);
+ }
+ }
+
+ private void InitializeRootView()
+ {
+#if UNITY_2019_3_OR_NEWER
+ _rootView = rootVisualElement;
+ _rootView.styleSheets.Add((StyleSheet) Resources.Load("UdonGraphStyle"));
+#else
+ _rootView = this.GetRootVisualContainer();
+ _rootView.AddStyleSheetPath("UdonGraphStyle2018");
+#endif
+ }
+
+ public void InitializeGraph(UdonGraphProgramAsset graph, UdonBehaviour udonBehaviour = null)
+ {
+ this._graphAsset = graph;
+
+ InitializeWindow();
+
+ _graphView = _rootView.Children().FirstOrDefault(e => e is UdonGraph) as UdonGraph;
+ if (_graphView == null)
+ {
+ Debug.LogError("GraphView has not been added to the BaseGraph root view!");
+ return;
+ }
+
+ _graphView.Initialize(graph, udonBehaviour);
+
+ _graphStatus.LoadAsset(graph);
+ // Store GUID for this asset to settings for easy reload later
+ if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(graph, out string guid, out long localId))
+ {
+ Settings.LastGraphGuid = guid;
+ }
+
+ if (udonBehaviour != null)
+ {
+ Settings.LastUdonBehaviourPath = udonBehaviour.transform.GetHierarchyPath();
+ Settings.LastUdonBehaviourScenePath = udonBehaviour.gameObject.scene.path;
+ }
+
+ _graphAssetName.text = graph.name;
+ ShowGraphTools(true);
+ }
+
+ private void InitializeWindow()
+ {
+ if (_graphView == null)
+ {
+ _graphView = new UdonGraph(this);
+ // we could add the toolbar in here
+ }
+
+ RemoveIfContaining(_welcomeView);
+ RemoveIfContaining(_graphView);
+ _rootView.Insert(0, _graphView);
+ }
+
+ private void ReloadWelcome()
+ {
+ RemoveIfContaining(_welcomeView);
+ _rootView.Add(_welcomeView);
+ ShowGraphTools(false);
+ }
+
+ // TODO: maybe move this to GraphView since it's so connected?
+ private void SetupToolbar()
+ {
+ _toolbar = new Toolbar()
+ {
+ name = "UdonToolbar",
+ };
+ _rootView.Add(_toolbar);
+
+ _toolbar.Add(new ToolbarButton(() => { ReloadWelcome(); })
+ {text = "Welcome"});
+
+ _graphAssetName = new Label()
+ {
+ name = "assetName",
+ };
+ _toolbar.Add(_graphAssetName);
+
+#if UNITY_2019_3_OR_NEWER
+ _toolbar.Add(new ToolbarSpacer()
+ {
+ flex = true,
+ });
+#else
+ _toolbar.Add(new ToolbarFlexSpacer());
+#endif
+ _updateOrderField = new VisualElement()
+ {
+ visible = false,
+ };
+ _updateOrderField.Add(new Label("UpdateOrder"));
+ _updateOrderIntField = new IntegerField()
+ {
+ name = "UpdateOrderIntegerField",
+ value = (_graphAsset == null) ? 0 : _graphAsset.graphData.updateOrder,
+ };
+#if UNITY_2019_3_OR_NEWER
+ _updateOrderIntField.RegisterValueChangedCallback((e) =>
+#else
+ _updateOrderIntField.OnValueChanged(e =>
+#endif
+ {
+ _graphView.graphProgramAsset.graphData.updateOrder = e.newValue;
+ _updateOrderField.visible = false;
+ });
+ _updateOrderIntField.isDelayed = true;
+ _updateOrderField.Add(_updateOrderIntField);
+ _toolbar.Add(_updateOrderField);
+
+ _toolbarOptions = new ToolbarMenu
+ {
+ text = "Settings"
+ };
+ // Show Variables Window
+ _toolbarOptions.menu.AppendAction("Show Variables",
+ (m) => { _graphView.ToggleShowVariables(!_graphView.GetBlackboardVisible()); },
+ (s) => { return BoolToStatus(_graphView.GetBlackboardVisible()); });
+ // Show Minimap
+ _toolbarOptions.menu.AppendAction("Show MiniMap",
+ (m) => { _graphView.ToggleShowMiniMap(!_graphView.GetMinimapVisible()); },
+ (s) => { return BoolToStatus(_graphView.GetMinimapVisible()); });
+ _toolbarOptions.menu.AppendSeparator();
+ // Show Update Order
+ _toolbarOptions.menu.AppendAction("Show UpdateOrder", (m) =>
+ {
+#if UNITY_2019_3_OR_NEWER
+ _updateOrderField.visible = !(m.status == DropdownMenuAction.Status.Checked);
+#else
+ _updateOrderField.visible = !(m.status == DropdownMenu.MenuAction.StatusFlags.Checked);
+#endif
+ if (_updateOrderField.visible)
+ {
+ _updateOrderIntField.value = _graphAsset.graphData.updateOrder;
+ }
+
+ _updateOrderIntField.Focus();
+ _updateOrderIntField.SelectAll();
+ }, (s) => { return BoolToStatus(_updateOrderField.visible); });
+ // Search On Noodle Drop
+ _toolbarOptions.menu.AppendAction("Search on Noodle Drop",
+ (m) => { Settings.SearchOnNoodleDrop = !Settings.SearchOnNoodleDrop; },
+ (s) => { return BoolToStatus(Settings.SearchOnNoodleDrop); });
+ // Search On Selected Node
+ _toolbarOptions.menu.AppendAction("Search on Selected Node",
+ (m) => { Settings.SearchOnSelectedNodeRegistry = !Settings.SearchOnSelectedNodeRegistry; },
+ (s) => { return BoolToStatus(Settings.SearchOnSelectedNodeRegistry); });
+ _toolbar.Add(_toolbarOptions);
+
+ _graphCompile = new ToolbarButton(() =>
+ {
+ if (_graphAsset != null && _graphAsset is AbstractUdonProgramSource udonProgramSource)
+ {
+ UdonEditorManager.Instance.QueueAndRefreshProgram(udonProgramSource);
+ }
+ })
+ {text = "Compile"};
+ _toolbar.Add(_graphCompile);
+
+ _graphReload = new ToolbarButton(() => { _graphView.Reload(); })
+ {text = "Reload"};
+ _toolbar.Add(_graphReload);
+
+ _graphStatus = new UdonGraphStatus(_rootView);
+ _toolbar.Add(_graphStatus);
+
+ ShowGraphTools(false);
+ }
+
+#if UNITY_2019_3_OR_NEWER
+ private DropdownMenuAction.Status BoolToStatus(bool value)
+ {
+ return value ? DropdownMenuAction.Status.Checked : DropdownMenuAction.Status.Normal;
+ }
+#else
+ private DropdownMenu.MenuAction.StatusFlags BoolToStatus(bool value)
+ {
+ return value ? DropdownMenu.MenuAction.StatusFlags.Checked : DropdownMenu.MenuAction.StatusFlags.Normal;
+ }
+#endif
+
+ public void ShowGraphTools(bool value)
+ {
+ _graphAssetName.style.visibility = value ? Visibility.Visible : Visibility.Hidden;
+ _toolbarOptions.style.visibility = value ? Visibility.Visible : Visibility.Hidden;
+ _graphCompile.style.visibility = value ? Visibility.Visible : Visibility.Hidden;
+ _graphReload.style.visibility = value ? Visibility.Visible : Visibility.Hidden;
+ _graphStatus.style.visibility = value ? Visibility.Visible : Visibility.Hidden;
+ }
+
+ private void RemoveIfContaining(VisualElement element)
+ {
+ if (_rootView.Contains(element))
+ {
+ _rootView.Remove(element);
+ }
+ }
+
+ private void OnUndoRedo()
+ {
+ Repaint();
+ }
+
+ public UdonGraphData GetGraphDataFromAsset(UdonGraphProgramAsset asset)
+ {
+ InitializeGraph(asset);
+ return _graphView.graphData;
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs.meta
new file mode 100644
index 00000000..d20bdf69
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonGraphWindow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c6f017dc2674fec4da54a57b2655a948
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs
new file mode 100644
index 00000000..5fed928b
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs
@@ -0,0 +1,58 @@
+#if UNITY_2019_3_OR_NEWER
+using EditorUI = UnityEditor.UIElements;
+using UnityEngine.UIElements;
+using UnityEditor.Experimental.GraphView;
+using MenuAction = UnityEngine.UIElements.DropdownMenuAction;
+#else
+using EditorUI = UnityEditor.Experimental.UIElements;
+using UnityEngine.Experimental.UIElements;
+using UnityEditor.Experimental.UIElements.GraphView;
+using MenuAction = UnityEngine.Experimental.UIElements.DropdownMenu.MenuAction;
+#endif
+using UnityEditor;
+using UnityEngine;
+using VRC.Udon.Graph;
+using VRC.Udon.Serialization;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonParameterField : BlackboardField
+ {
+ private UdonGraph udonGraph;
+ private UdonNodeData nodeData;
+ public UdonNodeData Data => nodeData;
+
+ public UdonParameterField(UdonGraph udonGraph, UdonNodeData nodeData)
+ {
+ this.udonGraph = udonGraph;
+ this.nodeData = nodeData;
+
+ // Get Definition or exit early
+ UdonNodeDefinition definition = UdonEditorManager.Instance.GetNodeDefinition(nodeData.fullName);
+ if (definition == null)
+ {
+ Debug.LogWarning($"Couldn't create Parameter Field for {nodeData.fullName}");
+ return;
+ }
+
+ this.text = (string) nodeData.nodeValues[(int) UdonParameterProperty.ValueIndices.name].Deserialize();
+ this.typeText = UdonGraphExtensions.PrettyString(definition.name).FriendlyNameify();
+
+ this.AddManipulator(new ContextualMenuManipulator(BuildContextualMenu));
+
+ this.Q("icon").AddToClassList("parameter-" + definition.type);
+ this.Q("icon").visible = true;
+
+ var textField = (TextField) this.Q("textField");
+ textField.isDelayed = true;
+ }
+
+ void BuildContextualMenu(ContextualMenuPopulateEvent evt)
+ {
+ evt.menu.AppendAction("Rename", (a) => OpenTextEditor(), MenuAction.AlwaysEnabled);
+ evt.menu.AppendAction("Delete", (a) => udonGraph.RemoveNodeAndData(nodeData), MenuAction.AlwaysEnabled);
+
+ evt.StopPropagation();
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs.meta
new file mode 100644
index 00000000..56cdf576
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterField.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5dcd92112af21784ba5bf6383abab768
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs
new file mode 100644
index 00000000..aa2b82b0
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs
@@ -0,0 +1,224 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEngine.UIElements;
+using EditorUI = UnityEditor.UIElements;
+using EngineUI = UnityEngine.UIElements;
+#else
+using UnityEngine.Experimental.UIElements;
+using EditorUI = UnityEditor.Experimental.UIElements;
+using EngineUI = UnityEngine.Experimental.UIElements;
+#endif
+using System.Collections.Generic;
+using UnityEngine;
+using VRC.Udon.Graph;
+using VRC.Udon.Serialization;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonParameterProperty : VisualElement
+ {
+ protected UdonGraph graph;
+ protected UdonNodeData nodeData;
+ protected UdonNodeDefinition definition;
+
+ //public ExposedParameter parameter { get; private set; }
+
+ public Toggle isPublic { get; private set; }
+ public Toggle isSynced { get; private set; }
+ public VisualElement defaultValueContainer { get; private set; }
+ public EditorUI.PopupField<string> syncField { get; private set; }
+ private VisualElement _inputField;
+
+ public enum ValueIndices
+ {
+ value = 0,
+ name = 1,
+ isPublic = 2,
+ isSynced = 3,
+ syncType = 4
+ }
+
+ private static SerializableObjectContainer[] GetDefaultNodeValues()
+ {
+ return new[]
+ {
+ SerializableObjectContainer.Serialize("", typeof(string)),
+ SerializableObjectContainer.Serialize("newVariableName", typeof(string)),
+ SerializableObjectContainer.Serialize(false, typeof(bool)),
+ SerializableObjectContainer.Serialize(false, typeof(bool)),
+ SerializableObjectContainer.Serialize("none", typeof(string))
+ };
+ }
+
+ // 0 = Value, 1 = name, 2 = public, 3 = synced, 4 = syncType
+ public UdonParameterProperty(UdonGraph graphView, UdonNodeDefinition definition, UdonNodeData nodeData)
+ {
+ this.graph = graphView;
+ this.definition = definition;
+ this.nodeData = nodeData;
+
+ string friendlyName = UdonGraphExtensions.FriendlyTypeName(definition.type).FriendlyNameify();
+
+ // Public Toggle
+ isPublic = new Toggle
+ {
+ text = "public",
+ value = (bool) GetValue(ValueIndices.isPublic)
+ };
+#if UNITY_2019_3_OR_NEWER
+ isPublic.RegisterValueChangedCallback(
+#else
+ isPublic.OnValueChanged(
+#endif
+ e => { SetNewValue(e.newValue, ValueIndices.isPublic); });
+ Add(isPublic);
+
+ if(UdonNetworkTypes.CanSync(definition.type))
+ {
+ // Is Synced Field
+ isSynced = new Toggle
+ {
+ text = "synced",
+ value = (bool)GetValue(ValueIndices.isSynced),
+ name = "syncToggle",
+ };
+
+#if UNITY_2019_3_OR_NEWER
+ isSynced.RegisterValueChangedCallback(
+#else
+ isSynced.OnValueChanged(
+#endif
+ e =>
+ {
+ SetNewValue(e.newValue, ValueIndices.isSynced);
+ syncField.visible = e.newValue;
+ });
+
+ // Sync Field, add to isSynced
+ List<string> choices = new List<string>()
+ {
+ "none"
+ };
+
+ if(UdonNetworkTypes.CanSyncLinear(definition.type))
+ {
+ choices.Add("linear");
+ }
+
+ if(UdonNetworkTypes.CanSyncSmooth(definition.type))
+ {
+ choices.Add("smooth");
+ }
+
+ syncField = new EditorUI.PopupField<string>(choices, 0)
+ {
+ visible = isSynced.value,
+ };
+ syncField.Insert(0, new Label("smooth:"));
+
+#if UNITY_2019_3_OR_NEWER
+ syncField.RegisterValueChangedCallback(
+#else
+ syncField.OnValueChanged(
+#endif
+ e =>
+ {
+ SetNewValue(e.newValue, ValueIndices.syncType);
+ });
+
+ // Only show sync smoothing dropdown if there are choices to be made
+ if (choices.Count > 1)
+ {
+ isSynced.Add(syncField);
+ }
+
+ Add(isSynced);
+ }
+ else
+ {
+ // Cannot Sync
+ SetNewValue(false, ValueIndices.isSynced);
+ Add(new Label($"{friendlyName} cannot be synced."));
+ }
+
+ // Container to show/edit Default Value
+ defaultValueContainer = new VisualElement();
+ defaultValueContainer.Add(
+ new Label("default value") {name = "default-value-label"});
+
+ // Generate Default Value Field
+ var value = TryGetValueObject(out object result);
+ _inputField = UdonFieldFactory.CreateField(
+ definition.type,
+ result,
+ newValue => SetNewValue(newValue, ValueIndices.value)
+ );
+ if (_inputField != null)
+ {
+ defaultValueContainer.Add(_inputField);
+ Add(defaultValueContainer);
+ }
+ }
+
+ private object GetValue(ValueIndices index)
+ {
+ if ((int) index >= nodeData.nodeValues.Length)
+ {
+ Debug.LogWarning($"Can't get {index} from {definition.name} variable");
+ return null;
+ }
+
+ return nodeData.nodeValues[(int) index].Deserialize();
+ }
+
+ private bool TryGetValueObject(out object result)
+ {
+ result = null;
+
+ var container = nodeData.nodeValues[0];
+ if (container == null)
+ {
+ return false;
+ }
+
+ result = container.Deserialize();
+ if (result == null)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void SetNewValue(object newValue, ValueIndices index)
+ {
+ nodeData.nodeValues[(int) index] = SerializableObjectContainer.Serialize(newValue);
+ }
+
+ // Convenience wrapper for field types that don't need special initialization
+ private VisualElement SetupField<TField, TType>()
+ where TField : VisualElement, INotifyValueChanged<TType>, new()
+ {
+ var field = new TField();
+ return SetupField<TField, TType>(field);
+ }
+
+ // Works for any TextValueField types, needs to know fieldType and object type
+ private VisualElement SetupField<TField, TType>(TField field)
+ where TField : VisualElement, INotifyValueChanged<TType>
+ {
+ field.AddToClassList("portField");
+ if (TryGetValueObject(out object result))
+ {
+ field.value = (TType) result;
+ }
+#if UNITY_2019_3_OR_NEWER
+ field.RegisterValueChangedCallback(
+#else
+ field.OnValueChanged(
+#endif
+ (e) => SetNewValue(e.newValue, ValueIndices.value));
+ _inputField = field;
+ return _inputField;
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs.meta
new file mode 100644
index 00000000..0a095238
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonParameterProperty.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 70616b8b964e3664780fc03f65f27f4f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs
new file mode 100644
index 00000000..a0e755ae
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs
@@ -0,0 +1,60 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEngine.UIElements;
+#else
+using UnityEngine.Experimental.UIElements;
+#endif
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonProgramSourceView : VisualElement
+ {
+ private UdonProgramAsset _asset;
+
+ private VisualElement _assemblyContainer;
+ private ScrollView _scrollView;
+ private Label _assemblyHeader;
+ private TextElement _assemblyField;
+
+ public UdonProgramSourceView ()
+ {
+ // Create and add container and children to display latest Assembly
+ _assemblyContainer = new VisualElement() { name = "Container", };
+ _assemblyHeader = new Label("Udon Assembly")
+ {
+ name = "Header",
+ };
+
+ _scrollView = new ScrollView();
+
+ _assemblyField = new TextElement()
+ {
+ name = "AssemblyField",
+ };
+
+ _assemblyContainer.Add(_assemblyHeader);
+ _assemblyContainer.Add(_scrollView);
+ _assemblyContainer.Add(_assemblyField);
+ _scrollView.contentContainer.Add(_assemblyField);
+
+ Add(_assemblyContainer);
+ }
+
+ public void LoadAsset(UdonGraphProgramAsset asset)
+ {
+ _asset = asset;
+ }
+
+ public void SetText(string newValue)
+ {
+ _assemblyField.text = newValue;
+ }
+
+ public void Unload()
+ {
+ if(_asset != null)
+ {
+ _asset = null;
+ }
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs.meta
new file mode 100644
index 00000000..e8027811
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonProgramSourceView.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fddc146e8502d7b49a294b6264d66dfd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs
new file mode 100644
index 00000000..f29ad0fc
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs
@@ -0,0 +1,212 @@
+#if UNITY_2019_3_OR_NEWER
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+#else
+using UnityEditor.Experimental.UIElements;
+using UnityEngine.Experimental.UIElements;
+#endif
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class UdonWelcomeView : VisualElement
+ {
+ private Button _openLastGraphButton;
+
+ public UdonWelcomeView()
+ {
+ name = "udon-welcome";
+ this.RegisterCallback<AttachToPanelEvent>(Initialize);
+ }
+
+
+ private void Initialize(AttachToPanelEvent evt)
+ {
+ // switch event to do some UI updates instead of initialization from here on out
+ UnregisterCallback<AttachToPanelEvent>(Initialize);
+ // this.RegisterCallback<AttachToPanelEvent>(OnAttach);
+
+ // Add Header
+ Add(new TextElement()
+ {
+ name = "intro",
+ text = "Udon Graph",
+ });
+
+ Add(new TextElement()
+ {
+ name = "header-message",
+ text =
+ "The Udon Graph is your gateway to creating amazing things in VRChat.\nCheck out the Readme and UdonExampleScene in the VRChat Examples folder to get started."
+ });
+
+ var mainContainer = new VisualElement()
+ {
+ name = "main",
+ };
+
+ Add(mainContainer);
+
+ var template = EditorGUIUtility.Load("Assets/Udon/Editor/Resources/UdonChangelog.uxml") as VisualTreeAsset;
+ #if UNITY_2019_3_OR_NEWER
+ var changelog = template.CloneTree((string) null);
+ #else
+ var changelog = template.CloneTree(null);
+ #endif
+ changelog.name = "changelog";
+ mainContainer.Add(changelog);
+
+ var column2 = new VisualElement() {name = "column-2"};
+ mainContainer.Add(column2);
+
+ // Add Button for Last Graph
+ if (!string.IsNullOrEmpty(Settings.LastGraphGuid))
+ {
+ _openLastGraphButton = new Button(() =>
+ {
+ var assetPath = AssetDatabase.GUIDToAssetPath(Settings.LastGraphGuid);
+ var graphName = assetPath.Substring(assetPath.LastIndexOf("/") + 1).Replace(".asset", "");
+
+ // Find actual asset from guid
+ var asset = AssetDatabase.LoadAssetAtPath<UdonGraphProgramAsset>(assetPath);
+ if (asset != null)
+ {
+ var w = EditorWindow.GetWindow<UdonGraphWindow>("Udon Graph", true, typeof(SceneView));
+ // get reference to saved UdonBehaviour if possible
+ UdonBehaviour udonBehaviour = null;
+ string gPath = Settings.LastUdonBehaviourPath;
+ string sPath = Settings.LastUdonBehaviourScenePath;
+ if (!string.IsNullOrEmpty(gPath) && !string.IsNullOrEmpty(sPath))
+ {
+ var targetScene = EditorSceneManager.GetSceneByPath(sPath);
+ if (targetScene != null && targetScene.isLoaded && targetScene.IsValid())
+ {
+ var targetObject = GameObject.Find(gPath);
+ if (targetObject != null)
+ {
+ udonBehaviour = targetObject.GetComponent<UdonBehaviour>();
+ }
+ }
+ }
+
+ // Initialize graph with restored udonBehaviour or null if not found / not saved
+ w.InitializeGraph(asset, udonBehaviour);
+ }
+ });
+
+ UpdateLastGraphButtonLabel();
+ column2.Add(_openLastGraphButton);
+ }
+
+ var settingsTemplate =
+ EditorGUIUtility.Load("Assets/Udon/Editor/Resources/UdonSettings.uxml") as VisualTreeAsset;
+ #if UNITY_2019_3_OR_NEWER
+ var settings = settingsTemplate.CloneTree((string)null);
+ #else
+ var settings = settingsTemplate.CloneTree(null);
+ #endif
+ settings.name = "settings";
+ column2.Add(settings);
+
+ // get reference to first settings section
+ var section = settings.Q("section");
+
+ // Add Grid Snap setting
+ var gridSnapContainer = new VisualElement();
+ gridSnapContainer.AddToClassList("settings-item-container");
+ var gridSnapField = new IntegerField(3)
+ {
+ value = Settings.GridSnapSize
+ };
+#if UNITY_2019_3_OR_NEWER
+ gridSnapField.RegisterValueChangedCallback(
+#else
+ gridSnapField.OnValueChanged(
+#endif
+ e => { Settings.GridSnapSize = e.newValue; });
+ gridSnapContainer.Add(new Label("Grid Snap Size"));
+ gridSnapContainer.Add(gridSnapField);
+ section.Add(gridSnapContainer);
+ var gridSnapLabel = new Label("Snap elements to a grid as you move them. 0 for No Snapping.");
+ gridSnapLabel.AddToClassList("settings-label");
+ section.Add(gridSnapLabel);
+
+ // Add Search On Selected Node settings
+ var searchOnSelectedNode = (new Toggle()
+ {
+ text = "Focus Search On Selected Node",
+ value = Settings.SearchOnSelectedNodeRegistry,
+ });
+#if UNITY_2019_3_OR_NEWER
+ searchOnSelectedNode.RegisterValueChangedCallback(
+#else
+ searchOnSelectedNode.OnValueChanged(
+#endif
+ (toggleEvent) => { Settings.SearchOnSelectedNodeRegistry = toggleEvent.newValue; });
+ section.Add(searchOnSelectedNode);
+ var searchOnLabel =
+ new Label(
+ "Highlight a node and press Spacebar to open a Search Window focused on nodes for that type. ");
+ searchOnLabel.AddToClassList("settings-label");
+ section.Add(searchOnLabel);
+
+ // Add Search On Noodle Drop settings
+ var searchOnNoodleDrop = (new Toggle()
+ {
+ text = "Search On Noodle Drop",
+ value = Settings.SearchOnNoodleDrop,
+ });
+#if UNITY_2019_3_OR_NEWER
+ searchOnNoodleDrop.RegisterValueChangedCallback(
+#else
+ searchOnNoodleDrop.OnValueChanged(
+#endif
+(toggleEvent) => { Settings.SearchOnNoodleDrop = toggleEvent.newValue; });
+ section.Add(searchOnNoodleDrop);
+ var searchOnDropLabel =
+ new Label("Drop a noodle into empty space to search for anything that can be connected.");
+ searchOnDropLabel.AddToClassList("settings-label");
+ section.Add(searchOnDropLabel);
+
+ // Add UseNeonStyle setting
+ var useNeonStyle = (new Toggle()
+ {
+ text = "Use Neon Style",
+ value = Settings.UseNeonStyle,
+ });
+#if UNITY_2019_3_OR_NEWER
+ useNeonStyle.RegisterValueChangedCallback(
+#else
+ useNeonStyle.OnValueChanged(
+#endif
+ (toggleEvent) => { Settings.UseNeonStyle = toggleEvent.newValue; });
+ section.Add(useNeonStyle);
+ var useNeonStyleLabel =
+ new Label("Try out an experimental Neon Style. We will support User Styles in an upcoming version.");
+ useNeonStyleLabel.AddToClassList("settings-label");
+ section.Add(useNeonStyleLabel);
+ }
+
+ private void UpdateLastGraphButtonLabel()
+ {
+ if (_openLastGraphButton == null) return;
+
+ string currentButtonAssetGuid = (string) _openLastGraphButton.userData;
+ if (string.Compare(currentButtonAssetGuid, Settings.LastGraphGuid) != 0)
+ {
+ var assetPath = AssetDatabase.GUIDToAssetPath(Settings.LastGraphGuid);
+ var graphName = assetPath.Substring(assetPath.LastIndexOf("/") + 1).Replace(".asset", "");
+
+ _openLastGraphButton.userData = Settings.LastGraphGuid;
+ _openLastGraphButton.text = $"Open {graphName}";
+ }
+ }
+
+ private void OnAttach(AttachToPanelEvent evt)
+ {
+ UpdateLastGraphButtonLabel();
+ }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs.meta
new file mode 100644
index 00000000..ab3bc509
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/UdonWelcomeView.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e5786fc577943ae45953c6f54c97116b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs
new file mode 100644
index 00000000..9e04df93
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs
@@ -0,0 +1,200 @@
+
+using System.Collections.Generic;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.Video;
+#if UNITY_2019_3_OR_NEWER
+using UnityEngine.UIElements;
+using UnityEditor.UIElements;
+#else
+using UnityEngine.Experimental.UIElements;
+using UnityEditor.Experimental.UIElements;
+#endif
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView
+{
+ public class VideoPlayerElement : VisualElement
+ {
+ private VideoPlayer _player;
+ private Scene _tempScene;
+ private Toolbar _toolbar;
+ private TextElement _header;
+ private Image _videoProxy;
+
+ public VideoPlayerElement()
+ {
+ // Constructing Items
+ _header = new TextElement()
+ {
+ text = "Using Focused Search",
+ name = "header",
+ };
+
+ _videoProxy = new Image()
+ {
+ name = "video-proxy",
+ image = Resources.Load<Texture2D>("videoStill"),
+ scaleMode = ScaleMode.ScaleToFit,
+ };
+
+ _toolbar = new Toolbar();
+ _toolbar.Add(new ToolbarButton(Play) {text = "Play", name = "button-play"});
+ _toolbar.Add(new ToolbarButton(Pause) {text = "Pause", name = "button-pause"});
+
+ // Adding Items
+ Add(_header);
+ Add(_videoProxy);
+ Add(_toolbar);
+ }
+
+ private void OnEnable()
+ {
+ ShowFrame();
+ RegisterCallback<MouseDownEvent>(OnMouseDown);
+ }
+
+ private void OnDisable()
+ {
+ UnregisterCallback<MouseDownEvent>(OnMouseDown);
+ if (_tempScene != null && _tempScene.IsValid())
+ {
+ EditorSceneManager.UnloadSceneAsync(_tempScene);
+ }
+ }
+
+ private void OnMouseDown(MouseDownEvent evt)
+ {
+ PlayPauseToggle();
+ }
+
+ public void PlayPauseToggle()
+ {
+ if (GetCurrentPlayer().isPlaying)
+ {
+ Pause();
+ }
+ else if (GetCurrentPlayer().isPaused)
+ {
+ Play();
+ }
+ }
+
+ public void Play()
+ {
+ GetCurrentPlayer().Play();
+ }
+
+ public void Pause()
+ {
+ GetCurrentPlayer().Pause();
+ }
+
+ public void LoadVideo(string url)
+ {
+ var player = GetCurrentPlayer();
+ player.url = url;
+ player.sendFrameReadyEvents = true;
+ player.frameReady += OnFrameReady;
+ player.isLooping = true;
+ ShowFrame();
+ }
+
+ public void ShowFrame()
+ {
+ var player = GetCurrentPlayer();
+ if (player == null || string.IsNullOrEmpty(player.url))
+ {
+ return;
+ }
+
+ player.frameReady += PauseOnNextFrame;
+ player.Play();
+ }
+
+ public void UnloadVideo()
+ {
+ var player = GetCurrentPlayer();
+ player.url = null;
+ player.sendFrameReadyEvents = false;
+ player.frameReady -= OnFrameReady;
+ }
+
+ private void OnFrameReady(VideoPlayer source, long frameIdx)
+ {
+ _videoProxy.image = source.texture;
+ MarkDirtyRepaint();
+ }
+
+ private void PauseOnNextFrame(VideoPlayer source, long frameIdx)
+ {
+ var player = GetCurrentPlayer();
+ player.frameReady -= PauseOnNextFrame;
+ player.Pause();
+ }
+
+ private Scene GetTempScene()
+ {
+ if (_tempScene == null)
+ {
+ _tempScene = EditorSceneManager.NewPreviewScene();
+ var root = new GameObject("VideoPlayer");
+ EditorSceneManager.MoveGameObjectToScene(root, _tempScene);
+ }
+
+ return _tempScene;
+ }
+
+ private VideoPlayer GetCurrentPlayer()
+ {
+ // Try to get player from scene if it's not cached
+ if (_player == null)
+ {
+ _player = GetTempScene().GetRootGameObjects()[0].GetComponent<VideoPlayer>();
+
+ // Make new player if it doesn't exist in the scene
+ if (_player == null)
+ {
+ _player = GetTempScene().GetRootGameObjects()[0].AddComponent<VideoPlayer>();
+ _player.renderMode = VideoRenderMode.APIOnly;
+ _player.source = VideoSource.Url;
+ _player.audioOutputMode = VideoAudioOutputMode.None;
+ _player.playOnAwake = false;
+ _player.Prepare();
+ }
+ }
+
+ return _player;
+ }
+
+
+ public new class UxmlFactory : UxmlFactory<VideoPlayerElement, UxmlTraits>
+ {
+ }
+
+#if UNITY_2019_3_OR_NEWER
+ public new class UxmlTraits : UnityEngine.UIElements.UxmlTraits
+#else
+ public new class UxmlTraits : UnityEngine.Experimental.UIElements.UxmlTraits
+#endif
+ {
+ UxmlStringAttributeDescription m_Url = new UxmlStringAttributeDescription
+ {name = "url-attr", defaultValue = ""};
+
+ public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription { get; }
+
+ public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
+ {
+ base.Init(ve, bag, cc);
+
+ var ate = ve as VideoPlayerElement;
+
+ ate.Clear();
+
+ ate.urlAttr = m_Url.GetValueFromBag(bag, cc);
+ }
+ }
+
+ public string urlAttr { get; set; }
+ }
+} \ No newline at end of file
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs.meta
new file mode 100644
index 00000000..5a3d82fb
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/GraphView/VideoPlayerElement.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: aabdd863f82551d40bd3a1b0835d2fc3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs
new file mode 100644
index 00000000..98183bf4
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs
@@ -0,0 +1,637 @@
+using System;
+using System.CodeDom;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.CSharp;
+using UnityEngine;
+using VRC.Udon.Compiler.Compilers;
+using VRC.Udon.Graph;
+using VRC.Udon.Graph.Interfaces;
+using CompressionLevel = System.IO.Compression.CompressionLevel;
+using Object = UnityEngine.Object;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
+{
+ public static class UdonGraphExtensions
+ {
+ private static readonly Dictionary<string, string> FriendlyNameCache;
+
+ static UdonGraphExtensions()
+ {
+ FriendlyNameCache = new Dictionary<string, string>();
+ StartsWithCache = new Dictionary<(string s, string prefix), bool>();
+ }
+
+ #region Serialization Utilities
+ private const byte ZIP_VERSION = 0;
+ public static string ZipString(string str)
+ {
+ using (MemoryStream output = new MemoryStream())
+ {
+ using (DeflateStream gzip =
+ new DeflateStream(output, CompressionLevel.Optimal)) //, CompressionMode.Compress
+ {
+ using (StreamWriter writer =
+ new StreamWriter(gzip, Encoding.UTF8))
+ {
+ writer.Write(str);
+ }
+ }
+ List<byte> outputList = output.ToArray().ToList();
+ outputList.Insert(0, ZIP_VERSION); //Version Number
+ return Convert.ToBase64String(outputList.ToArray());
+ }
+ }
+
+ public static string UnZipString(string input)
+ {
+ List<byte> inputList = new List<byte>(Convert.FromBase64String(input));
+ if (inputList[0] != ZIP_VERSION) //Version Number
+ {
+ return "";
+ }
+ inputList.RemoveAt(0);
+ using (MemoryStream inputStream = new MemoryStream(inputList.ToArray()))
+ {
+ using (DeflateStream gzip =
+ new DeflateStream(inputStream, CompressionMode.Decompress))
+ {
+ using (StreamReader reader =
+ new StreamReader(gzip, Encoding.UTF8))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+ }
+ }
+ #endregion
+
+ #region Color Utilities
+
+ public static class NodeColors
+ {
+ public static readonly Color Base = new Color(77f / 255f, 157f / 255f, 1);
+ public static readonly Color Const = new Color(0.4f, 0.14f, 1f);
+ public static readonly Color Variable = new Color(1f, 0.86f, 0.3f);
+ public static readonly Color Function = new Color(1f, 0.42f, 0.14f);
+ public static readonly Color Event = new Color(0.53f, 1f, 0.3f);
+ public static readonly Color Return = new Color(0.89f, 0.2f, 0.2f);
+ }
+
+ private static readonly MD5 _md5Hasher = MD5.Create();
+ private static readonly Dictionary<Type, Color> _typeColors = new Dictionary<Type, Color>();
+
+ public static Color MapTypeToColor(Type type)
+ {
+ if (type == null)
+ {
+ return Color.white;
+ }
+
+ if (type.IsPrimitive)
+ {
+ return new Color(0.12f, 0.53f, 0.9f);
+ }
+
+ if (typeof(Object).IsAssignableFrom(type))
+ {
+ return new Color(0.9f, 0.23f, 0.39f);
+ }
+
+ if (type.IsValueType)
+ {
+ return NodeColors.Variable;
+ }
+
+ if (_typeColors.ContainsKey(type))
+ {
+ return _typeColors[type];
+ }
+
+ byte[] hashed = _md5Hasher.ComputeHash(type.ToString() == "T"
+ ? Encoding.UTF8.GetBytes("T")
+ : Encoding.UTF8.GetBytes(type.Name));
+ int iValue = BitConverter.ToInt32(hashed, 0);
+
+ //TODO: Make this provide more varied colors
+ Color color = Color.HSVToRGB((iValue & 0xff) / 255f, .69f, 1f);
+
+
+ _typeColors.Add(type, color);
+ return color;
+ }
+ #endregion
+
+ #region Documentation Utilities
+
+ public static bool ShouldShowDocumentationLink(UdonNodeDefinition definition)
+ {
+ List<string> specialNames = new List<string>
+ {
+ "Block",
+ "Branch",
+ "For",
+ "While",
+ "Foreach",
+ "Get_Variable",
+ "Set_Variable",
+ "Set_ReturnValue",
+ "Event_Custom",
+ "Event_OnDataStorageAdded",
+ "Event_OnDataStorageChanged",
+ "Event_OnDataStorageRemoved",
+ "Event_OnDrop",
+ "Event_Interact",
+ "Event_OnNetworkReady",
+ "Event_OnOwnershipTransferred",
+ "Event_OnPickup",
+ "Event_OnPickupUseDown",
+ "Event_OnPickupUseUp",
+ "Event_OnPlayerJoined",
+ "Event_OnPlayerLeft",
+ "Event_OnSpawn",
+ "Event_OnStationEntered",
+ "Event_OnStationExited",
+ "Event_OnVideoEnd",
+ "Event_OnVideoPause",
+ "Event_OnVideoPlay",
+ "Event_OnVideoStart",
+ "Event_MidiNoteOn",
+ "Event_MidiNoteOff",
+ "Event_MidiControlChange",
+ "VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEvent__SystemString__SystemVoid",
+ "VRCUdonCommonInterfacesIUdonEventReceiver.__SetHeapVariable__SystemString_SystemObject__SystemVoid",
+ "VRCUdonCommonInterfacesIUdonEventReceiver.__GetHeapVariable__SystemString__SystemObject",
+ "Const_VRCUdonCommonInterfacesIUdonEventReceiver",
+ };
+
+ // Don't show for any of these
+ return !(definition.type == null ||
+ definition.type.Namespace == null ||
+ specialNames.Contains(definition.fullName) ||
+ (!definition.type.Namespace.Contains("UnityEngine") &&
+ !definition.type.Namespace.Contains("System")));
+ }
+
+ public static string GetDocumentationLink(UdonNodeDefinition definition)
+ {
+ if (definition.fullName.StartsWithCached("Event_"))
+ {
+ string url = "https://docs.unity3d.com/2018.4/Documentation/ScriptReference/MonoBehaviour.";
+ url += definition.name;
+ url += ".html";
+ return url;
+ }
+
+ if (definition.fullName.Contains("Array.__ctor"))
+ {
+ //I couldn't find the array constructor documentation
+ return "https://docs.microsoft.com/en-us/dotnet/api/system.array?view=netframework-4.8";
+ }
+
+ if (definition.fullName.Contains("Array.__Get"))
+ {
+ return "https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#indexer-operator-";
+ }
+
+ if (definition.fullName.Contains(".__Equals__SystemObject"))
+ {
+ return "https://docs.microsoft.com/en-us/dotnet/api/system.object.equals?view=netframework-4.8";
+ }
+
+ if (definition.name.Contains("[]"))
+ {
+ string url = "https://docs.microsoft.com/en-us/dotnet/api/system.array.";
+ url += definition.name.Split(' ')[1];
+ url += "?view=netframework-4.8";
+ return url;
+ }
+
+ if (definition.type.Namespace.Contains("UnityEngine"))
+ {
+ string url = "https://docs.unity3d.com/2018.4/Documentation/ScriptReference/";
+ if (definition.type.Namespace != "UnityEngine")
+ {
+ url += definition.type.Namespace.Replace("UnityEngine.", "");
+ url += ".";
+ }
+ url += definition.type.Name;
+
+ if (definition.fullName.Contains("__get_") || definition.fullName.Contains("__set_"))
+ {
+ if (definition.fullName.Contains("__get_"))
+ {
+ url += "-" + definition.name.Split(new[] { "get_" }, StringSplitOptions.None)[1];
+ }
+ else
+ {
+ url += "-" + definition.name.Split(new[] { "set_" }, StringSplitOptions.None)[1];
+ }
+
+ url += ".html";
+ return url;
+ }
+
+ if (definition.fullName.Contains("Const_") || definition.fullName.Contains("Type_") || definition.fullName.Contains("Variable_"))
+ {
+ url += ".html";
+ return url;
+ }
+
+ {
+ // Methods
+ url += "." + definition.name.Split(' ')[1];
+ url += ".html";
+ return url;
+ }
+ }
+
+ if (definition.type.Namespace.Contains("System"))
+ {
+ string url = "https://docs.microsoft.com/en-us/dotnet/api/system.";
+ url += definition.type.Name;
+ if (definition.fullName.Contains("__get_") || definition.fullName.Contains("__set_"))
+ {
+ url += "." + definition.name.Split(' ')[1].Replace("get_", "").Replace("set_", "");
+ url += "?view=netframework-4.8";
+ return url;
+ }
+
+ if (definition.name == "ctor")
+ {
+ url += ".-ctor";
+ url += "?view=netframework-4.8#System_";
+ url += definition.type.Name + "__ctor_";
+ foreach (var pType in definition.Inputs)
+ {
+ url += "System_" + pType.type.Name.Replace('[', '_').Replace(']', '_') + "_";
+ }
+
+ return url;
+ }
+
+ if (definition.fullName.Contains("Const_") || definition.fullName.Contains("Type_"))
+ {
+ url += "?view=netframework-4.8";
+ return url;
+ }
+
+ {
+ // Methods
+ // not entirely sure what case this catches, but we were always doing the split before, and it was breaking if we didn't have . in the name.
+ if (definition.name.Contains('.'))
+ {
+ url += "." + definition.name.Split(' ')[1];
+ }
+ url += "?view=netframework-4.8";
+ return url;
+ }
+ }
+
+ return "";
+ }
+
+ #endregion
+
+ public static string GetVariableChangeEventName(string variableName)
+ {
+ return UdonGraphCompiler.GetVariableChangeEventName(variableName);
+ }
+
+ public static string FriendlyNameify(this string typeString)
+ {
+ if (typeString == null)
+ {
+ return null;
+ }
+
+ if (FriendlyNameCache.ContainsKey(typeString))
+ {
+ return FriendlyNameCache[typeString];
+ }
+ string originalString = typeString;
+ typeString = typeString.Replace("Single", "float");
+ typeString = typeString.Replace("Int32", "int");
+ typeString = typeString.Replace("String", "string");
+ typeString = typeString.Replace("VRCUdonCommonInterfacesIUdonEventReceiver", "UdonBehaviour");
+ typeString = typeString.Replace("UdonCommonInterfacesIUdonEventReceiver", "UdonBehaviour");
+ typeString = typeString.Replace("IUdonEventReceiver", "UdonBehaviour");
+ typeString = typeString.Replace("Const_VRCUdonCommonInterfacesIUdonEventReceiver", "UdonBehaviour");
+ if(typeString != "SystemArray")
+ {
+ typeString = typeString.Replace("Array", "[]");
+ }
+
+ typeString = typeString.Replace("SDK3VideoComponentsBaseBase", "");
+ typeString = typeString.Replace("SDKBase", "");
+ typeString = typeString.Replace("SDK3Components", "");
+ typeString = typeString.Replace("VRCVRC", "VRC");
+ typeString = typeString.Replace("TMPro", "");
+ typeString = typeString.Replace("VideoVideo", "Video");
+ typeString = typeString.Replace("VRCUdonCommon", "");
+ typeString = typeString.Replace("Shuffle[]", "ShuffleArray");
+ // ReSharper disable once StringLiteralTypo
+ if (typeString.Replace("ector", "").Contains("ctor")) //Handle "Vector/vector"
+ {
+ typeString = typeString.ReplaceLast("ctor", "constructor");
+ }
+
+ if (typeString == "IUdonEventReceiver")
+ {
+ typeString = "UdonBehaviour";
+ }
+ FriendlyNameCache.Add(originalString, typeString);
+ return typeString;
+ }
+
+ private static readonly Dictionary<(string s, string prefix), bool> StartsWithCache;
+ public static bool StartsWithCached(this string s, string prefix)
+ {
+ if (StartsWithCache.ContainsKey((s, prefix)))
+ {
+ return StartsWithCache[(s, prefix)];
+ }
+ bool doesStartWith = s.StartsWith(prefix);
+ StartsWithCache.Add((s, prefix), doesStartWith);
+ return doesStartWith;
+ }
+
+ public static string UppercaseFirst(this string s)
+ {
+ if (string.IsNullOrEmpty(s))
+ {
+ return string.Empty;
+ }
+ char[] a = s.ToCharArray();
+ a[0] = char.ToUpper(a[0]);
+ return new string(a);
+ }
+
+ public static string ReplaceFirst(this string text, string search, string replace)
+ {
+ int pos = text.IndexOf(search, StringComparison.Ordinal);
+ if (pos < 0)
+ {
+ return text;
+ }
+ return text.Substring(0, pos) + replace + text.Substring(pos + search.Length);
+ }
+
+ public static Type SlotTypeConverter(Type type, string fullName)
+ {
+ if (type == null)
+ {
+ return typeof(object);
+ }
+
+ if (fullName.Contains("IUdonEventReceiver") && type == typeof(Object))
+ {
+ return typeof(UdonBehaviour);
+ }
+
+ return type;
+ }
+
+ public static string FriendlyTypeName(Type t)
+ {
+ if (t == null)
+ {
+ return "Flow";
+ }
+
+ if (!t.IsPrimitive)
+ {
+ if (t == typeof(Object))
+ {
+ return "Unity Object";
+ }
+ return t.Name;
+ }
+ using (CSharpCodeProvider provider = new CSharpCodeProvider())
+ {
+ CodeTypeReference typeRef = new CodeTypeReference(t);
+ return provider.GetTypeOutput(typeRef);
+ }
+ }
+
+ public static string ReplaceLast(this string source, string find, string replace)
+ {
+ int place = source.LastIndexOf(find, StringComparison.Ordinal);
+
+ if (place == -1)
+ return source;
+
+ string result = source.Remove(place, find.Length).Insert(place, replace);
+ return result;
+ }
+
+ public static string PrettyBaseName(string baseIdentifier)
+ {
+ string result = baseIdentifier.Replace("UnityEngine", "").Replace("System", "");
+ string[] resultSplit = result.Split(new[] { "__" }, StringSplitOptions.None);
+ if (resultSplit.Length >= 2)
+ {
+ result = $"{resultSplit[0]}{resultSplit[1]}";
+ }
+ result = result.FriendlyNameify();
+ result = result.Replace("op_", "");
+ result = result.Replace("_", " ");
+ return result;
+ }
+
+ public static string PrettyString(string s)
+ {
+ switch (s)
+ {
+ case "op_Equality":
+ s = "==";
+ break;
+
+ case "op_Inequality":
+ s = "!=";
+ break;
+
+ case "op_Addition":
+ s = "+";
+ break;
+ case "VRCUdonCommonInterfacesIUdonEventReceiver":
+ s = "UdonBehaviour";
+ break;
+ // ReSharper disable once RedundantEmptySwitchSection
+ default:
+ break;
+ }
+ return s;
+ }
+
+ public static string ParseByCase(string strInput)
+ {
+ string strOutput = "";
+ int intCurrentCharPos = 0;
+ int intLastCharPos = strInput.Length - 1;
+ for (intCurrentCharPos = 0; intCurrentCharPos <= intLastCharPos; intCurrentCharPos++)
+ {
+ char chrCurrentInputChar = strInput[intCurrentCharPos];
+ char chrPreviousInputChar = chrCurrentInputChar;
+ if (intCurrentCharPos > 0)
+ {
+ chrPreviousInputChar = strInput[intCurrentCharPos - 1];
+ }
+
+ if (char.IsUpper(chrCurrentInputChar) && char.IsLower(chrPreviousInputChar))
+ {
+ strOutput += " ";
+ }
+
+ strOutput += chrCurrentInputChar;
+ }
+
+ return strOutput;
+ }
+
+ public static string PrettyFullName(UdonNodeDefinition nodeDefinition, bool keepLong = false)
+ {
+ string fullName = nodeDefinition.fullName;
+ string result;
+ if (keepLong)
+ {
+ result = fullName.Replace("UnityEngine", "UnityEngine.").Replace("System", "System.");
+ }
+ else
+ {
+ result = fullName.Replace("UnityEngine", "").Replace("System", "");
+ }
+
+ string[] resultSplit = result.Split(new[] { "__" }, StringSplitOptions.None);
+ if (resultSplit.Length >= 3)
+ {
+ string outName = "";
+ if (nodeDefinition.type != typeof(void))
+ {
+ if (nodeDefinition.Outputs.Count > 0)
+ {
+ outName = string.Join(", ", nodeDefinition.Outputs.Select(o => o.name));
+ }
+ }
+
+ result = nodeDefinition.Inputs.Count > 0
+ ? $"{resultSplit[0]}{resultSplit[1]}({string.Join(", ", nodeDefinition.Inputs.Select(s => s.name))}{outName})"
+ : $"{resultSplit[0]}{resultSplit[1]}({resultSplit[2].Replace("_", ", ")}{outName})";
+ }
+ else if (resultSplit.Length >= 2)
+ {
+ result = $"{resultSplit[0]}{resultSplit[1]}()";
+ }
+
+ if (!keepLong)
+ {
+ result = result.FriendlyNameify();
+ result = result.Replace("op_", "");
+ result = result.Replace("_", " ");
+ }
+
+ return result;
+ }
+
+ public static string GetSimpleNameForRegistry(INodeRegistry registry)
+ {
+ string registryName = registry.ToString().Replace("NodeRegistry", "").FriendlyNameify();
+ registryName = registryName.Substring(registryName.LastIndexOf(".") + 1);
+ registryName = registryName.Replace("UnityEngine", "");
+ return registryName;
+ }
+
+ private static Dictionary<UdonNodeDefinition, INodeRegistry> _definitionToRegistryLookup;
+ public static INodeRegistry GetRegistryForDefinition(UdonNodeDefinition definition)
+ {
+ // Create lookup if needed
+ if(_definitionToRegistryLookup == null)
+ {
+ _definitionToRegistryLookup = new Dictionary<UdonNodeDefinition, INodeRegistry>();
+
+ foreach (var registry in UdonEditorManager.Instance.GetNodeRegistries())
+ {
+ foreach (var nodeDefinition in registry.Value.GetNodeDefinitions())
+ {
+ _definitionToRegistryLookup.Add(nodeDefinition, registry.Value);
+ }
+ }
+ }
+
+ // Return found Registry or Null
+ if (_definitionToRegistryLookup.ContainsKey(definition))
+ {
+ return _definitionToRegistryLookup[definition];
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ public static string ToLowerFirstChar(this string input)
+ {
+ string newString = input;
+ if (!String.IsNullOrEmpty(newString) && Char.IsUpper(newString[0]))
+ newString = Char.ToLower(newString[0]) + newString.Substring(1);
+ return newString;
+ }
+
+ public static string SanitizeVariableName(this string result)
+ {
+ result = result.Replace(" ", "");
+ if (char.IsNumber(result[0]))
+ {
+ result = $"A{result}";
+ }
+ Regex rgx = new Regex("[^a-zA-Z0-9 _]");
+ result = rgx.Replace(result, "");
+ return result;
+ }
+
+ public static char EscapeLikeALiteral(string src)
+ {
+ switch (src)
+ {
+ //case "\\'": return '\'';
+ //case "\\"": return '\"';
+ case "\\0": return '\0';
+ case "\\a": return '\a';
+ case "\\b": return '\b';
+ case "\\f": return '\f';
+ case "\\n": return '\n';
+ case "\\r": return '\r';
+ case "\\t": return '\t';
+ case "\\v": return '\v';
+ case "\\": return '\\';
+ default:
+ throw new InvalidOperationException($"src was {src}");
+ }
+ }
+
+ public static string UnescapeLikeALiteral(char src)
+ {
+ switch (src)
+ {
+ //case "\\'": return '\'';
+ //case "\\"": return '\"';
+ case '\0': return "\\0";
+ case '\a': return "\\a";
+ case '\b': return "\\b";
+ case '\f': return "\\f";
+ case '\n': return "\\n";
+ case '\r': return "\\r";
+ case '\t': return "\\t";
+ case '\v': return "\\v";
+ //case '\\': return "\\";
+ default:
+ return src.ToString();
+ }
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs.meta
new file mode 100644
index 00000000..7b34c5a2
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UI/UdonGraphExtensions.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 57422d3fdb0cc124189c68f87b7157cd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs
new file mode 100644
index 00000000..af8d5efd
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using UnityEditor;
+using UnityEngine;
+using VRC.Udon.Common.Interfaces;
+using VRC.Udon.Editor.ProgramSources.Attributes;
+using VRC.Udon.Editor.ProgramSources.UdonGraphProgram;
+using VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI.GraphView;
+using VRC.Udon.Graph;
+using VRC.Udon.Graph.Interfaces;
+using VRC.Udon.Serialization.OdinSerializer;
+using Object = UnityEngine.Object;
+
+[assembly: UdonProgramSourceNewMenu(typeof(UdonGraphProgramAsset), "Udon Graph Program Asset")]
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram
+{
+ [CreateAssetMenu(menuName = "VRChat/Udon/Udon Graph Program Asset", fileName = "New Udon Graph Program Asset")]
+ public class UdonGraphProgramAsset : UdonAssemblyProgramAsset, IUdonGraphDataProvider
+ {
+ [SerializeField]
+ public UdonGraphData graphData = new UdonGraphData();
+
+ [SerializeField]
+ public UdonGraphElementData[] graphElementData = new UdonGraphElementData[0];
+
+ [SerializeField]
+ public UdonGraph.ViewTransformData viewTransform = new UdonGraph.ViewTransformData();
+
+ [SerializeField]
+ public string version = "1.0.0";
+
+ [SerializeField]
+ private bool showAssembly = false;
+
+ [NonSerialized, OdinSerialize]
+ private Dictionary<string, (object value, Type type)> heapDefaultValues = new Dictionary<string, (object value, Type type)>();
+
+ protected override void DrawProgramSourceGUI(UdonBehaviour udonBehaviour, ref bool dirty)
+ {
+ if (GUILayout.Button("Open Udon Graph", "LargeButton"))
+ {
+ var w = EditorWindow.GetWindow<UdonGraphWindow>("Udon Graph", true, typeof(SceneView));
+ w.InitializeGraph(this, udonBehaviour);
+ }
+
+ DrawInteractionArea(udonBehaviour);
+ DrawPublicVariables(udonBehaviour, ref dirty);
+ DrawAssemblyErrorTextArea();
+ DrawAssemblyTextArea(false, ref dirty);
+ }
+
+ protected override void RefreshProgramImpl()
+ {
+ if(graphData == null)
+ {
+ return;
+ }
+
+ CompileGraph();
+ base.RefreshProgramImpl();
+ ApplyDefaultValuesToHeap();
+ }
+
+ protected override void DrawAssemblyTextArea(bool allowEditing, ref bool dirty)
+ {
+ EditorGUI.BeginChangeCheck();
+ bool newShowAssembly = EditorGUILayout.Foldout(showAssembly, "Compiled Graph Assembly");
+ if(EditorGUI.EndChangeCheck())
+ {
+ Undo.RecordObject(this, "Toggle Assembly Foldout");
+ showAssembly = newShowAssembly;
+ }
+
+ if(!showAssembly)
+ {
+ return;
+ }
+
+ EditorGUI.indentLevel++;
+ base.DrawAssemblyTextArea(allowEditing, ref dirty);
+ EditorGUI.indentLevel--;
+ }
+
+ [PublicAPI]
+ protected void CompileGraph()
+ {
+ udonAssembly = UdonEditorManager.Instance.CompileGraph(graphData, null, out Dictionary<string, (string uid, string fullName, int index)> _, out heapDefaultValues);
+ }
+
+ [PublicAPI]
+ protected void ApplyDefaultValuesToHeap()
+ {
+ IUdonSymbolTable symbolTable = program?.SymbolTable;
+ IUdonHeap heap = program?.Heap;
+ if(symbolTable == null || heap == null)
+ {
+ return;
+ }
+
+ foreach(KeyValuePair<string, (object value, Type type)> defaultValue in heapDefaultValues)
+ {
+ if(!symbolTable.HasAddressForSymbol(defaultValue.Key))
+ {
+ continue;
+ }
+
+ uint symbolAddress = symbolTable.GetAddressFromSymbol(defaultValue.Key);
+ (object value, Type declaredType) = defaultValue.Value;
+ if(typeof(Object).IsAssignableFrom(declaredType))
+ {
+ if(value != null && !declaredType.IsInstanceOfType(value))
+ {
+ heap.SetHeapVariable(symbolAddress, null, declaredType);
+ continue;
+ }
+
+ if((Object)value == null)
+ {
+ heap.SetHeapVariable(symbolAddress, null, declaredType);
+ continue;
+ }
+ }
+
+ if(value != null)
+ {
+ if(!declaredType.IsInstanceOfType(value))
+ {
+ value = declaredType.IsValueType ? Activator.CreateInstance(declaredType) : null;
+ }
+ }
+
+ if(declaredType == null)
+ {
+ declaredType = typeof(object);
+ }
+ heap.SetHeapVariable(symbolAddress, value, declaredType);
+ }
+ }
+
+ protected override object GetPublicVariableDefaultValue(string symbol, Type type)
+ {
+ IUdonSymbolTable symbolTable = program?.SymbolTable;
+ IUdonHeap heap = program?.Heap;
+ if(symbolTable == null || heap == null)
+ {
+ return null;
+ }
+
+ if(!heapDefaultValues.ContainsKey(symbol))
+ {
+ return null;
+ }
+
+ (object value, Type declaredType) = heapDefaultValues[symbol];
+ if(!typeof(Object).IsAssignableFrom(declaredType))
+ {
+ return value;
+ }
+
+ return (Object)value == null ? null : value;
+ }
+
+ protected override object DrawPublicVariableField(string symbol, object variableValue, Type variableType, ref bool dirty,
+ bool enabled)
+ {
+ EditorGUILayout.BeginHorizontal();
+ variableValue = base.DrawPublicVariableField(symbol, variableValue, variableType, ref dirty, enabled);
+ object defaultValue = null;
+ if(heapDefaultValues.ContainsKey(symbol))
+ {
+ defaultValue = heapDefaultValues[symbol].value;
+ }
+
+ if(variableValue == null || !variableValue.Equals(defaultValue))
+ {
+ if(defaultValue != null)
+ {
+ if(!dirty && GUILayout.Button("Reset to Default Value"))
+ {
+ variableValue = defaultValue;
+ dirty = true;
+ }
+ }
+ }
+
+ EditorGUILayout.EndHorizontal();
+
+ return variableValue;
+ }
+
+ #region Serialization Methods
+
+ protected override void OnAfterDeserialize()
+ {
+ foreach(UdonNodeData node in graphData.nodes)
+ {
+ node.SetGraph(graphData);
+ }
+ }
+
+ #endregion
+
+ public UdonGraphData GetGraphData()
+ {
+ return graphData;
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs.meta
new file mode 100644
index 00000000..99057af8
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAsset.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4f11136daadff0b44ac2278a314682ab
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs
new file mode 100644
index 00000000..219bfa57
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs
@@ -0,0 +1,21 @@
+using UnityEditor;
+
+namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram
+{
+ [CustomEditor(typeof(UdonGraphProgramAsset))]
+ public class UdonGraphProgramAssetEditor : UdonAssemblyProgramAssetEditor
+ {
+ public override void OnInspectorGUI()
+ {
+ base.OnInspectorGUI();
+ var asset = (UdonGraphProgramAsset) target;
+ EditorGUI.BeginChangeCheck();
+ asset.graphData.updateOrder = EditorGUILayout.IntField("UpdateOrder", asset.graphData.updateOrder);
+ if (EditorGUI.EndChangeCheck())
+ {
+ EditorUtility.SetDirty(target);
+ AssetDatabase.SaveAssets();
+ }
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs.meta
new file mode 100644
index 00000000..cdae2436
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonGraphProgram/UdonGraphProgramAssetEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 31d6811854f59254aa1a263a8d566eb2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram.meta
new file mode 100644
index 00000000..a35c9141
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 47cfad4ac2eccd148aa5f5f9c5403d81
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs
new file mode 100644
index 00000000..dfd288bd
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs
@@ -0,0 +1,2080 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using JetBrains.Annotations;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEditorInternal;
+using UnityEngine;
+using UnityEngine.Assertions;
+using VRC.Udon.Common;
+using VRC.Udon.Common.Interfaces;
+using VRC.Udon.ProgramSources;
+
+namespace VRC.Udon.Editor.ProgramSources
+{
+ public class UdonProgramAsset : AbstractUdonProgramSource, ISerializationCallbackReceiver
+ {
+ protected IUdonProgram program;
+
+ [SerializeField]
+ protected AbstractSerializedUdonProgramAsset serializedUdonProgramAsset;
+
+ public override AbstractSerializedUdonProgramAsset SerializedProgramAsset
+ {
+ get
+ {
+ AssetDatabase.TryGetGUIDAndLocalFileIdentifier(this, out string guid, out long _);
+ if(serializedUdonProgramAsset != null)
+ {
+ if(serializedUdonProgramAsset.name == guid)
+ {
+ return serializedUdonProgramAsset;
+ }
+
+ string oldSerializedUdonProgramAssetPath = Path.Combine("Assets", "SerializedUdonPrograms", $"{serializedUdonProgramAsset.name}.asset");
+ AssetDatabase.DeleteAsset(oldSerializedUdonProgramAssetPath);
+ }
+
+ string serializedUdonProgramAssetPath = Path.Combine("Assets", "SerializedUdonPrograms", $"{guid}.asset");
+
+ serializedUdonProgramAsset = (SerializedUdonProgramAsset)AssetDatabase.LoadAssetAtPath(
+ Path.Combine("Assets", "SerializedUdonPrograms", $"{guid}.asset"),
+ typeof(SerializedUdonProgramAsset)
+ );
+
+ if(serializedUdonProgramAsset != null)
+ {
+ return serializedUdonProgramAsset;
+ }
+
+ serializedUdonProgramAsset = CreateInstance<SerializedUdonProgramAsset>();
+ if(!AssetDatabase.IsValidFolder(Path.Combine("Assets", "SerializedUdonPrograms")))
+ {
+ AssetDatabase.CreateFolder("Assets", "SerializedUdonPrograms");
+ }
+
+ AssetDatabase.CreateAsset(serializedUdonProgramAsset, serializedUdonProgramAssetPath);
+ AssetDatabase.SaveAssets();
+
+ RefreshProgram();
+ AssetDatabase.SaveAssets();
+
+ AssetDatabase.Refresh();
+
+ return serializedUdonProgramAsset;
+ }
+ }
+
+ public sealed override void RunEditorUpdate(UdonBehaviour udonBehaviour, ref bool dirty)
+ {
+ if(program == null && serializedUdonProgramAsset != null)
+ {
+ program = serializedUdonProgramAsset.RetrieveProgram();
+ }
+
+ if(program == null)
+ {
+ RefreshProgram();
+ }
+
+ DrawProgramSourceGUI(udonBehaviour, ref dirty);
+
+ if(dirty)
+ {
+ EditorUtility.SetDirty(this);
+ }
+ }
+
+ protected virtual void DrawProgramSourceGUI(UdonBehaviour udonBehaviour, ref bool dirty)
+ {
+ DrawPublicVariables(udonBehaviour, ref dirty);
+ DrawProgramDisassembly();
+ }
+
+ public sealed override void RefreshProgram()
+ {
+ if(Application.isPlaying)
+ {
+ return;
+ }
+
+ RefreshProgramImpl();
+
+ SerializedProgramAsset.StoreProgram(program);
+ if (this != null)
+ {
+ EditorUtility.SetDirty(this);
+ }
+ }
+
+ protected virtual void RefreshProgramImpl()
+ {
+ }
+
+ [PublicAPI]
+ protected void DrawInteractionArea(UdonBehaviour udonBehaviour)
+ {
+ ImmutableArray<string> exportedSymbols = program.EntryPoints.GetExportedSymbols();
+ if (exportedSymbols.Contains("_interact"))
+ {
+ EditorGUILayout.LabelField("Interaction", EditorStyles.boldLabel);
+ EditorGUI.indentLevel++;
+
+ if(udonBehaviour != null)
+ {
+ udonBehaviour.interactText = EditorGUILayout.TextField("Interaction Text", udonBehaviour.interactText);
+ udonBehaviour.proximity = EditorGUILayout.Slider("Proximity", udonBehaviour.proximity, 0f, 100f);
+ udonBehaviour.interactTextPlacement = (Transform)EditorGUILayout.ObjectField("Text Placement", udonBehaviour.interactTextPlacement, typeof(Transform), true);
+ }
+ else
+ {
+ using(new EditorGUI.DisabledScope(true))
+ {
+ EditorGUILayout.TextField("Interaction Text", "Use");
+ EditorGUILayout.Slider("Proximity", 2.0f, 0f, 100f);
+ EditorGUILayout.ObjectField("Text Placement", null, typeof(Transform), true);
+ }
+ }
+
+
+
+ EditorGUI.indentLevel--;
+ }
+ }
+
+ [PublicAPI]
+ protected void DrawPublicVariables(UdonBehaviour udonBehaviour, ref bool dirty)
+ {
+ IUdonVariableTable publicVariables = null;
+ if(udonBehaviour != null)
+ {
+ publicVariables = udonBehaviour.publicVariables;
+ }
+
+ EditorGUILayout.LabelField("Public Variables", EditorStyles.boldLabel);
+ EditorGUI.indentLevel++;
+ if(program?.SymbolTable == null)
+ {
+ EditorGUILayout.LabelField("No public variables.");
+ EditorGUI.indentLevel--;
+ return;
+ }
+
+ IUdonSymbolTable symbolTable = program.SymbolTable;
+ // Remove non-exported public variables
+ if(publicVariables != null)
+ {
+ foreach(string publicVariableSymbol in publicVariables.VariableSymbols.ToArray())
+ {
+ if(!symbolTable.HasExportedSymbol(publicVariableSymbol))
+ {
+ publicVariables.RemoveVariable(publicVariableSymbol);
+ }
+ }
+ }
+
+ ImmutableArray<string> exportedSymbolNames = symbolTable.GetExportedSymbols();
+ if(exportedSymbolNames.Length <= 0)
+ {
+ EditorGUILayout.LabelField("No public variables.");
+ EditorGUI.indentLevel--;
+ return;
+ }
+
+ foreach(string exportedSymbol in exportedSymbolNames)
+ {
+ Type symbolType = symbolTable.GetSymbolType(exportedSymbol);
+ if(publicVariables == null)
+ {
+ DrawPublicVariableField(exportedSymbol, GetPublicVariableDefaultValue(exportedSymbol, symbolType), symbolType, ref dirty, false);
+ continue;
+ }
+
+ if(!publicVariables.TryGetVariableType(exportedSymbol, out Type declaredType) || declaredType != symbolType)
+ {
+ publicVariables.RemoveVariable(exportedSymbol);
+ if(!publicVariables.TryAddVariable(CreateUdonVariable(exportedSymbol, GetPublicVariableDefaultValue(exportedSymbol, declaredType), symbolType)))
+ {
+ EditorGUILayout.LabelField($"Error drawing field for symbol '{exportedSymbol}'.");
+ continue;
+ }
+ }
+
+ if(!publicVariables.TryGetVariableValue(exportedSymbol, out object variableValue))
+ {
+ variableValue = GetPublicVariableDefaultValue(exportedSymbol, declaredType);
+ }
+
+ variableValue = DrawPublicVariableField(exportedSymbol, variableValue, symbolType, ref dirty, true);
+ if(!dirty)
+ {
+ continue;
+ }
+
+ Undo.RecordObject(udonBehaviour, "Modify Public Variable");
+
+ if(!publicVariables.TrySetVariableValue(exportedSymbol, variableValue))
+ {
+ if(!publicVariables.TryAddVariable(CreateUdonVariable(exportedSymbol, variableValue, symbolType)))
+ {
+ Debug.LogError($"Failed to set public variable '{exportedSymbol}' value.");
+ }
+ }
+
+ EditorSceneManager.MarkSceneDirty(udonBehaviour.gameObject.scene);
+
+ if(PrefabUtility.IsPartOfPrefabInstance(udonBehaviour))
+ {
+ PrefabUtility.RecordPrefabInstancePropertyModifications(udonBehaviour);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+
+ private static IUdonVariable CreateUdonVariable(string symbolName, object value, Type declaredType)
+ {
+ Type udonVariableType = typeof(UdonVariable<>).MakeGenericType(declaredType);
+ return (IUdonVariable)Activator.CreateInstance(udonVariableType, symbolName, value);
+ }
+
+ [PublicAPI]
+ protected virtual object GetPublicVariableDefaultValue(string symbol, Type type)
+ {
+ return null;
+ }
+
+ [PublicAPI]
+ protected void DrawProgramDisassembly()
+ {
+ try
+ {
+ EditorGUILayout.LabelField("Disassembled Program", EditorStyles.boldLabel);
+ using(new EditorGUI.DisabledScope(true))
+ {
+ string[] disassembledProgram = UdonEditorManager.Instance.DisassembleProgram(program);
+ EditorGUILayout.TextArea(string.Join("\n", disassembledProgram));
+ }
+ }
+ catch(Exception e)
+ {
+ Debug.LogException(e);
+ }
+ }
+
+ [NonSerialized]
+ private readonly Dictionary<string, bool> _arrayStates = new Dictionary<string, bool>();
+
+ protected virtual object DrawPublicVariableField(string symbol, object variableValue, Type variableType, ref bool dirty, bool enabled)
+ {
+ using(new EditorGUI.DisabledScope(!enabled))
+ {
+ // ReSharper disable RedundantNameQualifier
+ if(!variableType.IsInstanceOfType(variableValue))
+ {
+ if(variableType.IsValueType)
+ {
+ variableValue = Activator.CreateInstance(variableType);
+ }
+ else
+ {
+ variableValue = null;
+ }
+ }
+
+ EditorGUILayout.BeginHorizontal();
+ if(typeof(UnityEngine.Object).IsAssignableFrom(variableType))
+ {
+ UnityEngine.Object unityEngineObjectValue = (UnityEngine.Object)variableValue;
+ EditorGUI.BeginChangeCheck();
+ Rect fieldRect = EditorGUILayout.GetControlRect();
+ variableValue = EditorGUI.ObjectField(fieldRect, symbol, unityEngineObjectValue, variableType, true);
+
+ if(variableValue == null && (variableType == typeof(GameObject) || variableType == typeof(Transform) ||
+ variableType == typeof(UdonBehaviour)))
+ {
+ EditorGUI.LabelField(
+ fieldRect,
+ new GUIContent(symbol),
+ new GUIContent("Self (" + variableType.Name + ")", AssetPreview.GetMiniTypeThumbnail(variableType)),
+ EditorStyles.objectField);
+ }
+
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(string))
+ {
+ string stringValue = (string)variableValue;
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.TextField(symbol, stringValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(string[]))
+ {
+ string[] valueArray = (string[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.TextField(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : "");
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(float))
+ {
+ float floatValue = (float?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.FloatField(symbol, floatValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(float[]))
+ {
+ float[] valueArray = (float[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.FloatField(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(int))
+ {
+ int intValue = (int?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.IntField(symbol, intValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(int[]))
+ {
+ int[] valueArray = (int[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.IntField(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(short))
+ {
+ short intValue = (short?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = (short)EditorGUILayout.IntField(symbol, intValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(short[]))
+ {
+ short[] valueArray = (short[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = (short)EditorGUILayout.IntField(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(long))
+ {
+ long intValue = (long?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = (long)EditorGUILayout.IntField(symbol, (int)intValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(long[]))
+ {
+ long[] valueArray = (long[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.IntField(
+ $"{i}:",
+ valueArray.Length > i ? (int)valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(uint))
+ {
+ uint intValue = (uint?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = (uint)EditorGUILayout.IntField(symbol, (int)intValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(uint[]))
+ {
+ uint[] valueArray = (uint[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = (uint)EditorGUILayout.IntField(
+ $"{i}:",
+ valueArray.Length > i ? (int)valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(ushort))
+ {
+ ushort intValue = (ushort?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = (ushort)EditorGUILayout.IntField(symbol, (int)intValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(ushort[]))
+ {
+ ushort[] valueArray = (ushort[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = (ushort)EditorGUILayout.IntField(
+ $"{i}:",
+ valueArray.Length > i ? (int)valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(ulong))
+ {
+ ulong intValue = (ulong?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = (ulong)EditorGUILayout.IntField(symbol, (int)intValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(ulong[]))
+ {
+ ulong[] valueArray = (ulong[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = (ulong)EditorGUILayout.IntField(
+ $"{i}:",
+ valueArray.Length > i ? (int)valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(byte))
+ {
+ byte intValue = (byte?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = (byte)EditorGUILayout.IntField(symbol, (int)intValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(byte[]))
+ {
+ byte[] valueArray = (byte[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = (byte)EditorGUILayout.IntField(
+ $"{i}:",
+ valueArray.Length > i ? (int)valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(sbyte))
+ {
+ sbyte intValue = (sbyte?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = (sbyte)EditorGUILayout.IntField(symbol, (int)intValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(sbyte[]))
+ {
+ sbyte[] valueArray = (sbyte[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = (sbyte)EditorGUILayout.IntField(
+ $"{i}:",
+ valueArray.Length > i ? (int)valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(double))
+ {
+ double intValue = (double?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = (double)EditorGUILayout.DoubleField(symbol, intValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(double[]))
+ {
+ double[] valueArray = (double[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.DoubleField(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(decimal))
+ {
+ decimal intValue = (decimal?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = (decimal)EditorGUILayout.DoubleField(symbol, (double)intValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(decimal[]))
+ {
+ decimal[] valueArray = (decimal[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = (decimal)EditorGUILayout.DoubleField(
+ $"{i}:",
+ valueArray.Length > i ? (double)valueArray[i] : 0);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(bool))
+ {
+ bool boolValue = (bool?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.Toggle(symbol, boolValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(bool[]))
+ {
+ bool[] valueArray = (bool[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.Toggle(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : false);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(UnityEngine.Vector2))
+ {
+ Vector2 vector2Value = (Vector2?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.Vector2Field(symbol, vector2Value);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Vector2[]))
+ {
+ Vector2[] valueArray = (Vector2[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.Vector2Field(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : Vector2.zero);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(UnityEngine.Vector3))
+ {
+ Vector3 vector3Value = (Vector3?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.Vector3Field(symbol, vector3Value);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Vector3[]))
+ {
+ Vector3[] valueArray = (Vector3[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.Vector3Field(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : Vector3.zero);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(UnityEngine.Vector2Int))
+ {
+ Vector2Int vector2IntValue = (Vector2Int?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ Vector2 vector2Value = EditorGUILayout.Vector2Field(symbol, vector2IntValue);
+ variableValue = new Vector2Int((int)vector2Value.x, (int)vector2Value.y);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Vector2Int[]))
+ {
+ Vector2Int[] valueArray = (Vector2Int[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ Vector2 vector2Value = EditorGUILayout.Vector2Field(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : Vector2.zero);
+ valueArray[i] = new Vector2Int((int)vector2Value.x, (int)vector2Value.y);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(UnityEngine.Vector3Int))
+ {
+ Vector3Int vector3IntValue = (Vector3Int?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ Vector3 vector3Value = EditorGUILayout.Vector3Field(symbol, vector3IntValue);
+ variableValue = new Vector3Int((int)vector3Value.x, (int)vector3Value.y, (int)vector3Value.z);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Vector3Int[]))
+ {
+ Vector3Int[] valueArray = (Vector3Int[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ Vector3 vector3Value = EditorGUILayout.Vector3Field(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : Vector3.zero);
+ valueArray[i] = new Vector3Int((int)vector3Value.x, (int)vector3Value.y,
+ (int)vector3Value.z);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(UnityEngine.Vector4))
+ {
+ Vector4 vector4Value = (Vector4?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.Vector4Field(symbol, vector4Value);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Vector4[]))
+ {
+ Vector4[] valueArray = (Vector4[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.Vector4Field(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : Vector4.zero);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(UnityEngine.Quaternion))
+ {
+ Quaternion quaternionValue = (Quaternion?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ Vector4 quaternionVector4 = EditorGUILayout.Vector4Field(symbol, new Vector4(quaternionValue.x, quaternionValue.y, quaternionValue.z, quaternionValue.w));
+ variableValue = new Quaternion(quaternionVector4.x, quaternionVector4.y, quaternionVector4.z, quaternionVector4.w);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Quaternion[]))
+ {
+ Quaternion[] valueArray = (Quaternion[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ Vector4 vector4 = EditorGUILayout.Vector4Field(
+ $"{i}:",
+ valueArray.Length > i ? new Vector4(valueArray[i].x, valueArray[i].y, valueArray[i].z, valueArray[i].w) : Vector4.zero);
+
+ valueArray[i] = new Quaternion(vector4.x, vector4.y, vector4.z, vector4.w);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Gradient))
+ {
+ Gradient color2Value = variableValue as Gradient;
+ if (color2Value == null) color2Value = new Gradient();
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.GradientField(symbol, color2Value);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Gradient[]))
+ {
+ Gradient[] valueArray = (Gradient[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ Gradient g = valueArray.Length > i ? (valueArray[i]) : new Gradient();
+ if (g == null) g = new Gradient();
+ valueArray[i] = EditorGUILayout.GradientField($"{i}:", g);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(AnimationCurve))
+ {
+ AnimationCurve curve2Value = variableValue as AnimationCurve;
+ if (curve2Value == null) curve2Value = new AnimationCurve();
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.CurveField(symbol, curve2Value);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(AnimationCurve[]))
+ {
+ AnimationCurve[] valueArray = (AnimationCurve[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ AnimationCurve curve = valueArray.Length > i ? (valueArray[i]) : new AnimationCurve();
+ if (curve == null) curve = new AnimationCurve();
+ valueArray[i] = EditorGUILayout.CurveField($"{i}:", curve);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(UnityEngine.Color))
+ {
+ Color color2Value = (Color?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.ColorField(symbol, color2Value);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Color[]))
+ {
+ Color[] valueArray = (Color[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.ColorField(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : Color.white);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(UnityEngine.Color32))
+ {
+ Color32 colorValue = (Color32?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ variableValue = (Color32)EditorGUILayout.ColorField(symbol, colorValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Color32[]))
+ {
+ Color32[] valueArray = (Color32[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+ EditorGUILayout.BeginVertical();
+
+ EditorGUI.BeginChangeCheck();
+ // Show Foldout Header
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ // Save foldout state
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.ColorField(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i] : (Color32)Color.white);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(ParticleSystem.MinMaxCurve))
+ {
+ ParticleSystem.MinMaxCurve minMaxCurve = (ParticleSystem.MinMaxCurve?)variableValue ?? default;
+ EditorGUI.BeginChangeCheck();
+ float multiplier = minMaxCurve.curveMultiplier;
+ AnimationCurve minCurve = minMaxCurve.curveMin;
+ AnimationCurve maxCurve = minMaxCurve.curveMax;
+ EditorGUILayout.BeginVertical();
+ EditorGUILayout.LabelField(symbol);
+ EditorGUI.indentLevel++;
+ multiplier = EditorGUILayout.FloatField("Multiplier", multiplier);
+ minCurve = EditorGUILayout.CurveField("Min Curve", minCurve);
+ maxCurve = EditorGUILayout.CurveField("Max Curve", maxCurve);
+ EditorGUI.indentLevel--;
+ EditorGUILayout.EndVertical();
+ variableValue = new ParticleSystem.MinMaxCurve(multiplier, minCurve, maxCurve);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(ParticleSystem.MinMaxCurve[]))
+ {
+ EditorGUI.BeginChangeCheck();
+ EditorGUILayout.BeginVertical();
+ ParticleSystem.MinMaxCurve[] valueArray = (ParticleSystem.MinMaxCurve[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+
+ showArray = EditorGUILayout.Foldout(showArray, symbol, true);
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ ParticleSystem.MinMaxCurve minMaxCurve = (ParticleSystem.MinMaxCurve)valueArray[i];
+ float multiplier = minMaxCurve.curveMultiplier;
+ AnimationCurve minCurve = minMaxCurve.curveMin;
+ AnimationCurve maxCurve = minMaxCurve.curveMax;
+ EditorGUILayout.BeginVertical();
+ EditorGUI.indentLevel++;
+ multiplier = EditorGUILayout.FloatField("Multiplier", multiplier);
+ minCurve = EditorGUILayout.CurveField("Min Curve", minCurve);
+ maxCurve = EditorGUILayout.CurveField("Max Curve", maxCurve);
+ EditorGUI.indentLevel--;
+ EditorGUILayout.EndVertical();
+ valueArray[i] = new ParticleSystem.MinMaxCurve(multiplier, minCurve, maxCurve);
+ EditorGUILayout.Space();
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType.IsEnum)
+ {
+ Enum enumValue = (Enum)variableValue;
+ GUI.SetNextControlName("NodeField");
+ EditorGUI.BeginChangeCheck();
+ variableValue = EditorGUILayout.EnumPopup(symbol, enumValue);
+ if(EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ // ReSharper disable once PossibleNullReferenceException
+ else if(variableType.IsArray && variableType.GetElementType().IsEnum)
+ {
+ EditorGUI.BeginChangeCheck();
+ EditorGUILayout.LabelField(symbol);
+ EditorGUILayout.BeginVertical();
+ Enum[] valueArray = (Enum[])variableValue;
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if(_arrayStates.ContainsKey(symbol))
+ {
+ showArray = _arrayStates[symbol];
+ }
+ else
+ {
+ _arrayStates.Add(symbol, false);
+ }
+
+ showArray = EditorGUILayout.Foldout(showArray, symbol);
+ _arrayStates[symbol] = showArray;
+
+ if(showArray)
+ {
+ EditorGUI.indentLevel++;
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1
+ );
+ EditorGUILayout.Space();
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if(valueArray != null && valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.EnumPopup(
+ $"{i}:",
+ valueArray[i]);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if(variableType == typeof(Type))
+ {
+ Type typeValue = (Type)variableValue;
+ EditorGUILayout.LabelField(symbol, typeValue == null ? $"Type = null" : $"Type = {typeValue.Name}");
+ }
+
+ else if(variableType.IsArray && typeof(UnityEngine.Object).IsAssignableFrom(variableType.GetElementType()))
+ {
+ Type elementType = variableType.GetElementType();
+ Assert.IsNotNull(elementType);
+
+ EditorGUI.BeginChangeCheck();
+ EditorGUILayout.BeginVertical();
+ GUI.SetNextControlName("NodeField");
+
+ bool showArray = false;
+ if (_arrayStates.ContainsKey(symbol))
+ showArray = _arrayStates[symbol];
+ else
+ _arrayStates.Add(symbol, false);
+ showArray = EditorGUILayout.Foldout( showArray, symbol, true );
+ _arrayStates[symbol] = showArray;
+
+ if(variableValue == null)
+ {
+ variableValue = Array.CreateInstance(elementType, 0);
+ }
+
+ UnityEngine.Object[] valueArray = (UnityEngine.Object[])variableValue;
+
+ if (showArray)
+ {
+ EditorGUI.indentLevel++;
+
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray.Length > 0 ? valueArray.Length : 1);
+
+ Array.Resize(ref valueArray, newSize);
+ Assert.IsNotNull(valueArray);
+
+ if(valueArray.Length > 0)
+ {
+ for(int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ valueArray[i] = EditorGUILayout.ObjectField($"{i}:", valueArray.Length > i ? valueArray[i] : null, variableType.GetElementType(), true);
+ }
+ }
+
+ EditorGUI.indentLevel--;
+ }
+
+ EditorGUILayout.EndVertical();
+ if(EditorGUI.EndChangeCheck())
+ {
+ Array destinationArray = Array.CreateInstance(elementType, valueArray.Length);
+ Array.Copy(valueArray, destinationArray, valueArray.Length);
+
+ variableValue = destinationArray;
+
+ dirty = true;
+ }
+ }
+ else if (variableType == typeof(VRC.SDKBase.VRCUrl))
+ {
+ if(variableValue == null)
+ variableValue = new VRC.SDKBase.VRCUrl("");
+
+ VRC.SDKBase.VRCUrl url = (VRC.SDKBase.VRCUrl)variableValue;
+ EditorGUI.BeginChangeCheck();
+ variableValue = new VRC.SDKBase.VRCUrl(EditorGUILayout.TextField(symbol, url.Get()));
+
+ if (EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else if (variableType == typeof(VRC.SDKBase.VRCUrl[]))
+ {
+ EditorGUI.BeginChangeCheck();
+ EditorGUILayout.BeginVertical();
+
+ GUI.SetNextControlName("NodeField");
+ bool showArray = false;
+ if (_arrayStates.ContainsKey(symbol))
+ showArray = _arrayStates[symbol];
+ else
+ _arrayStates.Add(symbol, false);
+ showArray = EditorGUILayout.Foldout( showArray, symbol, true );
+ _arrayStates[symbol] = showArray;
+
+ VRC.SDKBase.VRCUrl[] valueArray = (VRC.SDKBase.VRCUrl[])variableValue;
+
+ if (showArray)
+ {
+ EditorGUI.indentLevel++;
+ EditorGUILayout.Space();
+ int newSize = EditorGUILayout.IntField(
+ "size:",
+ valueArray != null && valueArray.Length > 0 ? valueArray.Length : 1);
+ newSize = newSize >= 0 ? newSize : 0;
+ Array.Resize(ref valueArray, newSize);
+
+ if (valueArray != null && valueArray.Length > 0)
+ {
+ for (int i = 0; i < valueArray.Length; i++)
+ {
+ GUI.SetNextControlName("NodeField");
+ if (valueArray[i] == null)
+ valueArray[i] = new VRC.SDKBase.VRCUrl("");
+
+ valueArray[i] = new VRC.SDKBase.VRCUrl(
+ EditorGUILayout.TextField(
+ $"{i}:",
+ valueArray.Length > i ? valueArray[i].Get() : ""));
+ }
+ }
+ EditorGUI.indentLevel--;
+ }
+
+ EditorGUILayout.EndVertical();
+ if (EditorGUI.EndChangeCheck())
+ {
+ variableValue = valueArray;
+ dirty = true;
+ }
+ }
+ else if (variableType == typeof(LayerMask))
+ {
+ LayerMask maskValue = (LayerMask)variableValue;
+ GUI.SetNextControlName("NodeField");
+ EditorGUI.BeginChangeCheck();
+ EditorGUILayout.LabelField(symbol);
+ // Using workaround from http://answers.unity.com/answers/1387522/view.html
+ LayerMask tempMask = EditorGUILayout.MaskField(InternalEditorUtility.LayerMaskToConcatenatedLayersMask(maskValue), InternalEditorUtility.layers);
+ variableValue = InternalEditorUtility.ConcatenatedLayersMaskToLayerMask(tempMask);
+ if (EditorGUI.EndChangeCheck())
+ {
+ dirty = true;
+ }
+ }
+ else
+ {
+ EditorGUILayout.LabelField(symbol + " no defined editor for type of " + variableType);
+ }
+ // ReSharper restore RedundantNameQualifier
+
+ IUdonSyncMetadata sync = program.SyncMetadataTable.GetSyncMetadataFromSymbol(symbol);
+ if(sync != null)
+ {
+ GUILayout.Label($"sync{sync.Properties[0].InterpolationAlgorithm.ToString()}", GUILayout.Width(80));
+ }
+ }
+
+ EditorGUILayout.EndHorizontal();
+
+ return variableValue;
+ }
+
+ #region Serialization Methods
+
+ void ISerializationCallbackReceiver.OnAfterDeserialize()
+ {
+ OnAfterDeserialize();
+ }
+
+ void ISerializationCallbackReceiver.OnBeforeSerialize()
+ {
+ OnBeforeSerialize();
+ }
+
+ [PublicAPI]
+ protected virtual void OnAfterDeserialize()
+ {
+ }
+
+ [PublicAPI]
+ protected virtual void OnBeforeSerialize()
+ {
+ }
+
+ #endregion
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs.meta
new file mode 100644
index 00000000..2495f546
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAsset.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 264ec3c8a1d423f42a144da0df6c5ebe
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs
new file mode 100644
index 00000000..56a21a85
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs
@@ -0,0 +1,19 @@
+using UnityEditor;
+
+namespace VRC.Udon.Editor.ProgramSources
+{
+ [CustomEditor(typeof(UdonProgramAsset))]
+ public class UdonProgramAssetEditor : UnityEditor.Editor
+ {
+ public override void OnInspectorGUI()
+ {
+ bool dirty = false;
+ UdonProgramAsset programAsset = (UdonProgramAsset)target;
+ programAsset.RunEditorUpdate(null, ref dirty);
+ if(dirty)
+ {
+ EditorUtility.SetDirty(target);
+ }
+ }
+ }
+}
diff --git a/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs.meta b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs.meta
new file mode 100644
index 00000000..3f085920
--- /dev/null
+++ b/VRCSDK3Worlds/Assets/Udon/Editor/ProgramSources/UdonProgram/UdonProgramAssetEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 41d70977fa7936441afe41442f1862b2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: