public void TriggerUdonBehaviourProgramRefresh(AbstractUdonProgramSource updatedProgramSource)
        {
            _udonBehaviours.Clear();
            _udonBehaviours.UnionWith(Object.FindObjectsOfType <UdonBehaviour>());

            HashSet <string> prefabAssetPaths = new HashSet <string>();

            foreach (UdonBehaviour udonBehaviour in _udonBehaviours)
            {
                if (udonBehaviour.programSource != updatedProgramSource)
                {
                    continue;
                }

                udonBehaviour.RefreshProgram();

                if (!PrefabUtility.IsPartOfPrefabInstance(udonBehaviour))
                {
                    continue;
                }

                string prefabAssetPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(udonBehaviour);
                if (!prefabAssetPaths.Contains(prefabAssetPath))
                {
                    prefabAssetPaths.Add(prefabAssetPath);
                }
            }

            RefreshPrefabUdonBehaviours(prefabAssetPaths.ToList());
        }
        public void CancelQueuedProgramSourceRefresh(AbstractUdonProgramSource programSource)
        {
            if (programSource == null)
            {
                return;
            }

            if (_programSourceRefreshQueue.Contains(programSource))
            {
                _programSourceRefreshQueue.Remove(programSource);
            }
        }
        public bool IsProgramSourceRefreshQueued(AbstractUdonProgramSource programSource)
        {
            if (_programSourceRefreshQueue.Count <= 0)
            {
                return(false);
            }

            if (!_programSourceRefreshQueue.Contains(programSource))
            {
                return(false);
            }

            return(true);
        }
        static bool UdonSharpBehaviourTypeMatches(object symbolValue, System.Type expectedType, string behaviourName, string variableName)
        {
            if (symbolValue == null)
            {
                return(true);
            }

            // A reference to an actual UdonSharpBehaviour has been put in the UdonBehaviour, UdonSharpBehaviours are not serializable into VRC so this will cause issues
            if (symbolValue is UdonSharpBehaviour)
            {
                Debug.LogWarning($"Clearing reference to an UdonSharpBehaviour's proxy '{symbolValue}' from variable '{variableName}' on behaviour '{behaviourName}' You must only reference backer UdonBehaviours, not their proxies.");
                return(false);
            }

            if (!(expectedType == typeof(UdonBehaviour) ||
                  expectedType == typeof(UdonSharpBehaviour) ||
                  expectedType.IsSubclassOf(typeof(UdonSharpBehaviour))))
            {
                return(true);
            }

            if (symbolValue.GetType() != typeof(UdonBehaviour))
            {
                return(false);
            }

            UdonBehaviour otherBehaviour = (UdonBehaviour)symbolValue;

            AbstractUdonProgramSource behaviourProgramAsset = otherBehaviour.programSource;

            if (behaviourProgramAsset == null)
            {
                return(true);
            }

            if (behaviourProgramAsset is UdonSharpProgramAsset behaviourUSharpAsset &&
                expectedType != typeof(UdonBehaviour)) // Leave references to UdonBehaviours intact to prevent breaks on old behaviours, this may be removed in 1.0 to enforce the correct division in types in C# land
            {
                System.Type symbolUSharpType = behaviourUSharpAsset.sourceCsScript?.GetClass();

                if (symbolUSharpType != null &&
                    symbolUSharpType != expectedType &&
                    !symbolUSharpType.IsSubclassOf(expectedType))
                {
                    return(false);
                }
            }
Пример #5
0
        public static void RecompileAllProgramSources()
        {
            string[] programSourceGUIDs = AssetDatabase.FindAssets("t:AbstractUdonProgramSource");
            foreach (string guid in programSourceGUIDs)
            {
                string assetPath = AssetDatabase.GUIDToAssetPath(guid);
                AbstractUdonProgramSource programSource = AssetDatabase.LoadAssetAtPath <AbstractUdonProgramSource>(assetPath);
                if (programSource == null)
                {
                    continue;
                }
                programSource.RefreshProgram();
            }

            AssetDatabase.SaveAssets();

            PopulateAllPrefabSerializedProgramAssetReferences();
        }
Пример #6
0
        public void QueueProgramSourceRefresh(AbstractUdonProgramSource programSource)
        {
            if (Application.isPlaying)
            {
                return;
            }

            if (programSource == null)
            {
                return;
            }

            if (IsProgramSourceRefreshQueued(programSource))
            {
                return;
            }

            _programSourceRefreshQueue.Add(programSource);
        }
        public void QueueProgramSourceRefresh(AbstractUdonProgramSource programSource)
        {
            if (Application.isPlaying)
            {
                return;
            }

            if (programSource == null)
            {
                return;
            }

            _lastProgramRefreshQueueTime = EditorApplication.timeSinceStartup;

            if (IsProgramSourceRefreshQueued(programSource))
            {
                return;
            }

            _programSourceRefreshQueue.Add(programSource);
        }
Пример #8
0
        private static AbstractUdonProgramSource CreateUdonProgramSourceAsset(Type newProgramType, string displayName, Scene scene, string udonBehaviourName)
        {
            string scenePath = Path.GetDirectoryName(scene.path) ?? "Assets";

            string folderName = $"{scene.name}_UdonProgramSources";
            string folderPath = Path.Combine(scenePath, folderName);

            if (!AssetDatabase.IsValidFolder(folderPath))
            {
                AssetDatabase.CreateFolder(scenePath, folderName);
            }

            string assetPath = Path.Combine(folderPath, $"{udonBehaviourName} {displayName}.asset");

            assetPath = AssetDatabase.GenerateUniqueAssetPath(assetPath);

            AbstractUdonProgramSource asset = (AbstractUdonProgramSource)CreateInstance(newProgramType);

            AssetDatabase.CreateAsset(asset, assetPath);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
            return(asset);
        }
Пример #9
0
 public void QueueAndRefreshProgram(AbstractUdonProgramSource programSource)
 {
     QueueProgramSourceRefresh(programSource);
     RefreshQueuedProgramSources();
 }
Пример #10
0
        private void OnGUILogic()
        {
            _drawGraph = true;

            if (Selection.gameObjects.Count(g => g.GetComponent <UdonBehaviour>()) > 1)
            {
                _displayText = "Multi-object editing not supported";
                _drawGraph   = false;
            }
            else if (Selection.objects.Count(o => o != null && o is IUdonGraphDataProvider) > 1) // == typeof(UdonGraphProgramAsset)
            {
                _displayText = "Multi-object editing not supported";
                _drawGraph   = false;
            }

            IUdonGraphDataProvider udonGraphProgramAsset = (IUdonGraphDataProvider)Selection.objects.FirstOrDefault(g => g != null && g is IUdonGraphDataProvider);

            if (udonGraphProgramAsset == null)
            {
                GameObject behaviourObject = Selection.gameObjects.FirstOrDefault(g => g.GetComponent <UdonBehaviour>());
                if (behaviourObject != null)
                {
                    UdonBehaviour udonBehaviour;
                    var           udonBehaviours = behaviourObject.GetComponents <UdonBehaviour>();
                    if (udonBehaviours.Length == 1 || lastClickedProgramSource == null)
                    {
                        udonBehaviour = udonBehaviours[0];
                    }
                    else
                    {
                        udonBehaviour = udonBehaviours.FirstOrDefault(u => u.programSource == lastClickedProgramSource);
                        if (udonBehaviour == null)
                        {
                            // the last clicked graph is not available on this object, reset it
                            lastClickedProgramSource = null;
                            udonBehaviour            = udonBehaviours[0];
                        }
                    }
                    AbstractUdonProgramSource programSource = udonBehaviour.programSource;
                    if (programSource is IUdonGraphDataProvider asUdonGraphProgramAsset)
                    {
                        udonGraphProgramAsset = asUdonGraphProgramAsset;
                    }
                }
            }

            if (graph == null)
            {
                graph = CreateInstance <UdonGraph>();
            }

            if (udonGraphProgramAsset != null)
            {
                if (graphGUI == null)
                {
                    graphGUI       = CreateInstance <UdonGraphGUI>();
                    graphGUI.graph = graph;
                }
                if (graph == null)
                {
                    graph          = CreateInstance <UdonGraph>();
                    graphGUI.graph = graph;
                }

                if (graph.graphProgramAsset == udonGraphProgramAsset)
                {
                    if (graph.data != null)
                    {
                        return;
                    }
                }

                titleContent = new GUIContent($"Udon - {udonGraphProgramAsset}");

                graph.data = new UdonGraphData(udonGraphProgramAsset.GetGraphData());
                graph.graphProgramAsset = udonGraphProgramAsset;
                graph.Reload();
                graphGUI.CenterGraph();
            }
            else
            {
                if (graph.graphProgramAsset != null)
                {
                    return;
                }

                _displayText = "Create an Udon Graph Asset to begin.";
                _drawGraph   = false;
            }
        }
Пример #11
0
        public override void OnInspectorGUI()
        {
            UdonBehaviour udonTarget = (UdonBehaviour)target;

            using (new EditorGUI.DisabledScope(Application.isPlaying))
            {
                bool dirty = false;

                EditorGUILayout.BeginVertical(new GUIStyle(EditorStyles.helpBox));
                {
                    SyncTypes selected = (SyncTypes)EditorGUILayout.EnumPopup("Sync Method", udonTarget.Reliable ? SyncTypes.Manual : SyncTypes.Continuous);

                    bool shouldBeReliable = selected == SyncTypes.Manual;
                    if (udonTarget.Reliable != shouldBeReliable)
                    {
                        udonTarget.Reliable = shouldBeReliable;
                        dirty = true;
                    }

                    if (udonTarget.Reliable)
                    {
                        EditorGUILayout.LabelField("Manual replication is intended for infrequently-updated variables of small or large size, and will not be tweened.", EditorStyles.wordWrappedMiniLabel);
                    }
                    else
                    {
                        EditorGUILayout.LabelField("Continuous replication is intended for frequently-updated variable of small size, and will be tweened.", EditorStyles.wordWrappedMiniLabel);
                    }
                }
                EditorGUILayout.EndVertical();

                EditorGUILayout.Space();

                EditorGUILayout.LabelField("Udon");

                EditorGUILayout.BeginHorizontal();
                EditorGUI.BeginChangeCheck();
                _programSourceProperty.objectReferenceValue = EditorGUILayout.ObjectField(
                    "Program Source",
                    _programSourceProperty.objectReferenceValue,
                    typeof(AbstractUdonProgramSource),
                    false
                    );

                if (EditorGUI.EndChangeCheck())
                {
                    dirty = true;
                    serializedObject.ApplyModifiedProperties();
                }

                if (_programSourceProperty.objectReferenceValue == null)
                {
                    if (_serializedProgramAssetProperty.objectReferenceValue != null)
                    {
                        _serializedProgramAssetProperty.objectReferenceValue = null;
                        serializedObject.ApplyModifiedPropertiesWithoutUndo();
                    }

                    List <(string displayName, Type newProgramType)> programSourceTypesForNewMenu = GetProgramSourceTypesForNewMenu();
                    if (GUILayout.Button("New Program"))
                    {
                        (string displayName, Type newProgramType) = programSourceTypesForNewMenu.ElementAt(_newProgramType);

                        string udonBehaviourName = udonTarget.name;
                        Scene  scene             = udonTarget.gameObject.scene;
                        if (string.IsNullOrEmpty(scene.path))
                        {
                            Debug.LogError("You need to save the scene before you can create new Udon program assets!");
                        }
                        else
                        {
                            AbstractUdonProgramSource newProgramSource = CreateUdonProgramSourceAsset(newProgramType, displayName, scene, udonBehaviourName);
                            _programSourceProperty.objectReferenceValue          = newProgramSource;
                            _serializedProgramAssetProperty.objectReferenceValue = newProgramSource.SerializedProgramAsset;
                            serializedObject.ApplyModifiedProperties();
                        }
                    }

                    EditorGUILayout.EndHorizontal();
                    EditorGUILayout.BeginHorizontal();
                    GUILayout.FlexibleSpace();

                    EditorGUI.BeginChangeCheck();
                    _newProgramType = EditorGUILayout.Popup(
                        "",
                        Mathf.Clamp(_newProgramType, 0, programSourceTypesForNewMenu.Count),
                        programSourceTypesForNewMenu.Select(t => t.displayName).ToArray(),
                        GUILayout.ExpandWidth(false)
                        );

                    if (EditorGUI.EndChangeCheck())
                    {
                        EditorPrefs.SetInt(VRC_UDON_NEW_PROGRAM_TYPE_PREF_KEY, _newProgramType);
                    }
                }
                else
                {
                    EditorGUILayout.EndHorizontal();
                    EditorGUILayout.BeginHorizontal();
                    using (new EditorGUI.DisabledScope(true))
                    {
                        EditorGUI.indentLevel++;
                        EditorGUILayout.ObjectField(
                            "Serialized Udon Program Asset ID: ",
                            _serializedProgramAssetProperty.objectReferenceValue,
                            typeof(AbstractSerializedUdonProgramAsset),
                            false
                            );

                        EditorGUI.indentLevel--;
                    }

                    AbstractUdonProgramSource          programSource = (AbstractUdonProgramSource)_programSourceProperty.objectReferenceValue;
                    AbstractSerializedUdonProgramAsset serializedUdonProgramAsset = programSource.SerializedProgramAsset;
                    if (_serializedProgramAssetProperty.objectReferenceValue != serializedUdonProgramAsset)
                    {
                        _serializedProgramAssetProperty.objectReferenceValue = serializedUdonProgramAsset;
                        serializedObject.ApplyModifiedPropertiesWithoutUndo();
                    }
                }

                EditorGUILayout.EndHorizontal();

                udonTarget.RunEditorUpdate(ref dirty);
                if (dirty && !Application.isPlaying)
                {
                    EditorSceneManager.MarkSceneDirty(udonTarget.gameObject.scene);
                }
            }
        }
Пример #12
0
        private void OnGUILogic()
        {
            _drawGraph = true;

            if (Selection.gameObjects.Count(g => g.GetComponent <UdonBehaviour>()) > 1)
            {
                _displayText = "Multi-object editing not supported";
                _drawGraph   = false;
            }
            else if (Selection.objects.Count(o => o != null && o is IUdonGraphDataProvider) > 1) // == typeof(UdonGraphProgramAsset)
            {
                _displayText = "Multi-object editing not supported";
                _drawGraph   = false;
            }

            IUdonGraphDataProvider udonGraphProgramAsset = (IUdonGraphDataProvider)Selection.objects.FirstOrDefault(g => g != null && g is IUdonGraphDataProvider);

            if (udonGraphProgramAsset == null)
            {
                GameObject behaviourObject = Selection.gameObjects.FirstOrDefault(g => g.GetComponent <UdonBehaviour>());
                if (behaviourObject != null)
                {
                    UdonBehaviour             udonBehaviour = behaviourObject.GetComponent <UdonBehaviour>();
                    AbstractUdonProgramSource programSource = udonBehaviour.programSource;
                    if (programSource is IUdonGraphDataProvider asUdonGraphProgramAsset)
                    {
                        udonGraphProgramAsset = asUdonGraphProgramAsset;
                    }
                }
            }

            if (graph == null)
            {
                graph = CreateInstance <UdonGraph>();
            }

            if (udonGraphProgramAsset != null)
            {
                if (graphGUI == null)
                {
                    graphGUI       = CreateInstance <UdonGraphGUI>();
                    graphGUI.graph = graph;
                }
                if (graph == null)
                {
                    graph          = CreateInstance <UdonGraph>();
                    graphGUI.graph = graph;
                }

                if (graph.graphProgramAsset == udonGraphProgramAsset)
                {
                    if (graph.data != null)
                    {
                        return;
                    }
                }

                graph.data = new UdonGraphData(udonGraphProgramAsset.GetGraphData());
                graph.graphProgramAsset = udonGraphProgramAsset;
                graph.Reload();
                graphGUI.CenterGraph();
            }
            else
            {
                if (graph.graphProgramAsset != null)
                {
                    return;
                }

                _displayText = "Create an Udon Graph Asset to begin.";
                _drawGraph   = false;
            }
        }
        public override void OnInspectorGUI()
        {
            UdonBehaviour udonTarget = (UdonBehaviour)target;

            using (new EditorGUI.DisabledScope(Application.isPlaying))
            {
                EditorGUILayout.LabelField("Specialized Synchronization Behaviour");

                //if(udonTarget.GetComponent<Animator>() != null || udonTarget.GetComponent<Animation>() != null)
                //    udonTarget.SynchronizeAnimation = EditorGUILayout.Toggle("Synchronize Animation", udonTarget.SynchronizeAnimation);
                //else
                //    udonTarget.SynchronizeAnimation = EditorGUILayout.Toggle("Synchronize Animation", false);
                using (new EditorGUI.DisabledScope(true))
                {
                    EditorGUILayout.BeginHorizontal();
                    EditorGUILayout.Toggle("Synchronize Animation", false);
                    EditorGUILayout.LabelField("Coming Soon!");
                    EditorGUILayout.EndHorizontal();
                }

                udonTarget.SynchronizePosition = EditorGUILayout.Toggle("Synchronize Position", udonTarget.SynchronizePosition);
                if (udonTarget.GetComponent <Collider>() != null)
                {
                    udonTarget.AllowCollisionOwnershipTransfer = EditorGUILayout.Toggle("Allow Ownership Transfer on Collision", udonTarget.AllowCollisionOwnershipTransfer);
                }
                else
                {
                    udonTarget.AllowCollisionOwnershipTransfer = EditorGUILayout.Toggle("Allow Ownership Transfer on Collision", false);
                }

                EditorGUILayout.Space();

                EditorGUILayout.LabelField("Udon");

                bool dirty = false;
                EditorGUILayout.BeginHorizontal();
                EditorGUI.BeginChangeCheck();
                _programSourceProperty.objectReferenceValue = EditorGUILayout.ObjectField(
                    "Program Source",
                    _programSourceProperty.objectReferenceValue,
                    typeof(AbstractUdonProgramSource),
                    false
                    );

                if (EditorGUI.EndChangeCheck())
                {
                    dirty = true;
                    serializedObject.ApplyModifiedProperties();
                }

                if (_programSourceProperty.objectReferenceValue == null)
                {
                    if (_serializedProgramAssetProperty.objectReferenceValue != null)
                    {
                        _serializedProgramAssetProperty.objectReferenceValue = null;
                        serializedObject.ApplyModifiedPropertiesWithoutUndo();
                    }

                    List <(string displayName, Type newProgramType)> programSourceTypesForNewMenu = GetProgramSourceTypesForNewMenu();
                    if (GUILayout.Button("New Program"))
                    {
                        (string displayName, Type newProgramType) = programSourceTypesForNewMenu.ElementAt(_newProgramType);

                        string udonBehaviourName = udonTarget.name;
                        Scene  scene             = udonTarget.gameObject.scene;
                        if (string.IsNullOrEmpty(scene.path))
                        {
                            Debug.LogError("You need to save the scene before you can create new Udon program assets!");
                        }
                        else
                        {
                            AbstractUdonProgramSource newProgramSource = CreateUdonProgramSourceAsset(newProgramType, displayName, scene, udonBehaviourName);
                            _programSourceProperty.objectReferenceValue          = newProgramSource;
                            _serializedProgramAssetProperty.objectReferenceValue = newProgramSource.SerializedProgramAsset;
                            serializedObject.ApplyModifiedProperties();
                        }
                    }

                    EditorGUILayout.EndHorizontal();
                    EditorGUILayout.BeginHorizontal();
                    GUILayout.FlexibleSpace();

                    EditorGUI.BeginChangeCheck();
                    _newProgramType = EditorGUILayout.Popup(
                        "",
                        Mathf.Clamp(_newProgramType, 0, programSourceTypesForNewMenu.Count),
                        programSourceTypesForNewMenu.Select(t => t.displayName).ToArray(),
                        GUILayout.ExpandWidth(false)
                        );

                    if (EditorGUI.EndChangeCheck())
                    {
                        EditorPrefs.SetInt(VRC_UDON_NEW_PROGRAM_TYPE_PREF_KEY, _newProgramType);
                    }
                }
                else
                {
                    EditorGUILayout.EndHorizontal();
                    EditorGUILayout.BeginHorizontal();
                    using (new EditorGUI.DisabledScope(true))
                    {
                        EditorGUI.indentLevel++;
                        EditorGUILayout.ObjectField(
                            "Serialized Udon Program Asset ID: ",
                            _serializedProgramAssetProperty.objectReferenceValue,
                            typeof(AbstractSerializedUdonProgramAsset),
                            false
                            );

                        EditorGUI.indentLevel--;
                    }

                    AbstractUdonProgramSource          programSource = (AbstractUdonProgramSource)_programSourceProperty.objectReferenceValue;
                    AbstractSerializedUdonProgramAsset serializedUdonProgramAsset = programSource.SerializedProgramAsset;
                    if (_serializedProgramAssetProperty.objectReferenceValue != serializedUdonProgramAsset)
                    {
                        _serializedProgramAssetProperty.objectReferenceValue = serializedUdonProgramAsset;
                        serializedObject.ApplyModifiedPropertiesWithoutUndo();
                    }
                }

                EditorGUILayout.EndHorizontal();

                udonTarget.RunEditorUpdate(ref dirty);
                if (dirty && !Application.isPlaying)
                {
                    EditorSceneManager.MarkSceneDirty(udonTarget.gameObject.scene);
                }
            }
        }
        private void CheckForMissingComponents()
        {
            Collider stationCollider = GetComponent <Collider>();

            if (stationCollider == null)
            {
                gameObject.AddComponent <BoxCollider>().isTrigger = true;
            }

#if UDON && UNITY_EDITOR
            UdonBehaviour udon = GetComponent <UdonBehaviour>();
            if (udon == null)
            {
                udon = gameObject.AddComponent <UdonBehaviour>();
                udon.interactText = "Sit";
                AbstractUdonProgramSource program = UnityEditor.AssetDatabase.LoadAssetAtPath <AbstractUdonProgramSource>("Assets/VRChat Examples/Prefabs/VRCChair/StationGraph.asset");
                if (program != null)
                {
                    udon.AssignProgramAndVariables(program.SerializedProgramAsset, new UdonVariableTable());
                }
            }
#endif

#if VRC_SDK_VRCSDK2
            // Auto add a Interact Trigger to use the station
            VRC_Trigger trigger = GetComponent <VRC_Trigger>();
            if (trigger == null)
            {
                trigger              = gameObject.AddComponent <VRCSDK2.VRC_Trigger>();
                trigger.Triggers     = new List <VRC_Trigger.TriggerEvent>();
                trigger.interactText = "Sit";

                VRC_Trigger.TriggerEvent onInteract = new VRC_Trigger.TriggerEvent
                {
                    BroadcastType = VRC_EventHandler.VrcBroadcastType.Local,
                    TriggerType   = VRC_Trigger.TriggerType.OnInteract,
                    Events        = new List <VRC_EventHandler.VrcEvent>()
                };

                VRC_EventHandler.VrcEvent useStationEvent = new VRC_EventHandler.VrcEvent
                {
                    EventType        = VRC_EventHandler.VrcEventType.SendRPC,
                    ParameterString  = "UseStation",
                    ParameterObjects = new[] { gameObject },
                    ParameterInt     = 6,
                };

                onInteract.Events.Add(useStationEvent);
                trigger.Triggers.Add(onInteract);

                // Reinitialize the trigger now that it has the proper events added.
                // Note that this only works as there were no vrc triggers on this object before.
                CyanEmuTriggerHelper helper = GetComponent <CyanEmuTriggerHelper>();
                if (helper != null)
                {
                    DestroyImmediate(helper);
                }
                CyanEmuTriggerHelper.InitializeTrigger(trigger);
            }
#endif
        }
Пример #15
0
        public override void OnInspectorGUI()
        {
            UdonBehaviour udonTarget = (UdonBehaviour)target;

            using (new EditorGUI.DisabledScope(Application.isPlaying))
            {
                bool dirty = false;

                EditorGUILayout.BeginVertical(new GUIStyle(EditorStyles.helpBox));
                {
                    // We skip the first option, Unknown, as it's reserved for older scenes.
                    VRC.SDKBase.Networking.SyncType method = (VRC.SDKBase.Networking.SyncType)(1 + EditorGUILayout.Popup("Synchronization", (int)udonTarget.SyncMethod - 1, Enum.GetNames(typeof(VRC.SDKBase.Networking.SyncType)).Skip(1).ToArray()));

                    if (method != udonTarget.SyncMethod)
                    {
                        udonTarget.SyncMethod = method;
                        dirty = true;
                    }

                    switch (method)
                    {
                    case VRC.SDKBase.Networking.SyncType.None:
                        EditorGUILayout.LabelField("Replication will be disabled.", EditorStyles.wordWrappedLabel);
                        break;

                    case VRC.SDKBase.Networking.SyncType.Continuous:
                        EditorGUILayout.LabelField("Continuous replication is intended for frequently-updated variables of small size, and will be tweened. Ideal for physics objects and objects that must be in sync with players.", EditorStyles.wordWrappedLabel);
                        break;

                    case VRC.SDKBase.Networking.SyncType.Manual:
                        EditorGUILayout.LabelField("Manual replication is intended for infrequently-updated variables of small or large size, and will not be tweened. Ideal for infrequently modified abstract data.", EditorStyles.wordWrappedLabel);
                        break;

                    default:
                        EditorGUILayout.LabelField("What have you done?!", EditorStyles.wordWrappedLabel);
                        break;
                    }
                }
                EditorGUILayout.EndVertical();

                EditorGUILayout.Space();

                EditorGUILayout.LabelField("Udon");

                EditorGUILayout.BeginHorizontal();
                EditorGUI.BeginChangeCheck();
                _programSourceProperty.objectReferenceValue = EditorGUILayout.ObjectField(
                    "Program Source",
                    _programSourceProperty.objectReferenceValue,
                    typeof(AbstractUdonProgramSource),
                    false
                    );

                if (EditorGUI.EndChangeCheck())
                {
                    if (_programSourceProperty.objectReferenceValue == null)
                    {
                        _serializedProgramAssetProperty.objectReferenceValue = null;
                    }

                    dirty = true;
                    serializedObject.ApplyModifiedProperties();
                }

                if (_programSourceProperty.objectReferenceValue == null)
                {
                    List <(string displayName, Type newProgramType)> programSourceTypesForNewMenu = GetProgramSourceTypesForNewMenu();
                    if (GUILayout.Button("New Program"))
                    {
                        (string displayName, Type newProgramType) = programSourceTypesForNewMenu.ElementAt(_newProgramType);

                        string udonBehaviourName = udonTarget.name;
                        Scene  scene             = udonTarget.gameObject.scene;
                        if (string.IsNullOrEmpty(scene.path))
                        {
                            Debug.LogError("You need to save the scene before you can create new Udon program assets!");
                        }
                        else
                        {
                            AbstractUdonProgramSource newProgramSource = CreateUdonProgramSourceAsset(newProgramType, displayName, scene, udonBehaviourName);
                            _programSourceProperty.objectReferenceValue          = newProgramSource;
                            _serializedProgramAssetProperty.objectReferenceValue = newProgramSource.SerializedProgramAsset;
                            serializedObject.ApplyModifiedProperties();
                        }
                    }

                    EditorGUILayout.EndHorizontal();
                    EditorGUILayout.BeginHorizontal();
                    GUILayout.FlexibleSpace();

                    EditorGUI.BeginChangeCheck();
                    _newProgramType = EditorGUILayout.Popup(
                        "",
                        Mathf.Clamp(_newProgramType, 0, programSourceTypesForNewMenu.Count),
                        programSourceTypesForNewMenu.Select(t => t.displayName).ToArray(),
                        GUILayout.ExpandWidth(false)
                        );

                    if (EditorGUI.EndChangeCheck())
                    {
                        EditorPrefs.SetInt(VRC_UDON_NEW_PROGRAM_TYPE_PREF_KEY, _newProgramType);
                    }
                }
                else
                {
                    EditorGUILayout.EndHorizontal();
                    EditorGUILayout.BeginHorizontal();
                    using (new EditorGUI.DisabledScope(true))
                    {
                        EditorGUI.indentLevel++;
                        EditorGUILayout.ObjectField(
                            "Serialized Udon Program Asset ID: ",
                            _serializedProgramAssetProperty.objectReferenceValue,
                            typeof(AbstractSerializedUdonProgramAsset),
                            false
                            );

                        EditorGUI.indentLevel--;
                    }

                    AbstractUdonProgramSource          programSource = (AbstractUdonProgramSource)_programSourceProperty.objectReferenceValue;
                    AbstractSerializedUdonProgramAsset serializedUdonProgramAsset = programSource.SerializedProgramAsset;
                    if (_serializedProgramAssetProperty.objectReferenceValue != serializedUdonProgramAsset)
                    {
                        _serializedProgramAssetProperty.objectReferenceValue = serializedUdonProgramAsset;
                        serializedObject.ApplyModifiedPropertiesWithoutUndo();
                    }
                }

                EditorGUILayout.EndHorizontal();

                udonTarget.RunEditorUpdate(ref dirty);
                if (dirty && !Application.isPlaying)
                {
                    EditorSceneManager.MarkSceneDirty(udonTarget.gameObject.scene);
                }
            }
        }