using System; using System.Collections.Generic; using System.Linq; using PleasureArcade.ThiccWater.Editor; using UnityEditor; using UnityEditor.Animations; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.Animations; using UnityEngine.UIElements; using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.ScriptableObjects; public class ThiccWaterInstaller : EditorWindow { private string generatedAssetsFolder = "Assets/PleasureArcade/ThiccWater/GeneratedAssets"; private ThiccWaterResources resources; private VRCAvatarDescriptor avatar; private bool[] selectedEmitters; private Transform parentBone; private bool scaleToggle; private VRCExpressionsMenu parentMenu; private ObjectField _menuSelectField; [MenuItem("PleasureArcade/ThiccWater Installer")] public static void OpenInstaller() { // This method is called when the user selects the menu item in the Editor EditorWindow wnd = GetWindow(); wnd.titleContent = new GUIContent("ThiccWater Installer"); // Limit size of the window wnd.minSize = new Vector2(360, 583); wnd.maxSize = new Vector2(360, 583); } private void CreateGUI() { // Storing emitter data in a scriptable object so I don't have to do a ton of loading in code resources = Resources.Load("ThiccWaterResources"); // Add a container later? styling? var root = rootVisualElement; // Avatar selector var avatarSelectorField = new ObjectField("Avatar") { objectType = typeof(VRCAvatarDescriptor), allowSceneObjects = true, value = avatar }; avatarSelectorField.RegisterValueChangedCallback(e => { avatar = (VRCAvatarDescriptor)e.newValue; UpdateMenuSelector(); }); root.Add(avatarSelectorField); root.Add(new Label(" ")); // Select emitters root.Add(new Label("Emitters to install")); selectedEmitters = new bool[resources.emitterNames.Length]; selectedEmitters.Initialize(); var emitterToggles = new Toggle[resources.emitterNames.Length]; for (var i = 0; i < resources.emitterNames.Length; i++) { emitterToggles[i] = new Toggle(resources.emitterNames[i]) { value = selectedEmitters[i] }; emitterToggles[i].RegisterValueChangedCallback(GenerateBoolArrayEventCallback(i)); root.Add(emitterToggles[i]); } root.Add(new Label(" ")); // Parent bone selector var boneField = new ObjectField("Parent Bone") { objectType = typeof(Transform), allowSceneObjects = true, value = parentBone }; boneField.RegisterValueChangedCallback(e => { parentBone = (Transform)e.newValue; }); root.Add(boneField); root.Add(new Label(" ")); // Extra options var scalingToggle = new Toggle("Enable Scaling") { value = scaleToggle, tooltip = "Enable this if you want the particles to scale up/down with the parent bone" }; scalingToggle.RegisterValueChangedCallback(e => scaleToggle = e.newValue); root.Add(scalingToggle); _menuSelectField = new ObjectField("Add to Menu") { objectType = typeof(VRCExpressionsMenu), value = parentMenu }; _menuSelectField.RegisterValueChangedCallback(e => { parentMenu = (VRCExpressionsMenu)e.newValue; }); root.Add(_menuSelectField); root.Add(new Label(" ")); // Finish root.Add(new Button(Install) { text = "Install Emitters" }); // Adjustments root.Add(new Label(" ")); root.Add(new Button(RemoveBottles) { text = "Remove Bottles" }); } /** * Delegate generator to get around non-scoped variable in closure */ public EventCallback> GenerateBoolArrayEventCallback(int i) { return delegate(ChangeEvent evt) { selectedEmitters[i] = evt.newValue; }; } /** * Update the menu to be the root menu whenever an avatar is selected */ public void UpdateMenuSelector() { _menuSelectField.value = avatar?.expressionsMenu; parentMenu = avatar?.expressionsMenu; } /** * Do the thing! */ public void Install() { // Sanity checks if (!avatar) { EditorUtility.DisplayDialog("Error", "Please select an avatar to install emitters onto", "OK"); return; } if (selectedEmitters.All(s => !s)) { EditorUtility.DisplayDialog("Error", "Please select at least one emitter to install", "OK"); return; } if (!parentBone) { EditorUtility.DisplayDialog("Error", "Please select a parent bone for emitters to attach to", "OK"); return; } // Add container to avatar base var container = new GameObject("ThiccWater").transform; container.SetParent(avatar.transform); container.localPosition = Vector3.zero; container.localRotation = Quaternion.identity; // Add depth provider var depthProvider = Instantiate(resources.depthProvider, Vector3.up, Quaternion.identity, container); depthProvider.name = "Depth Provider"; // Add parent constraint var parentConstraint = container.gameObject.AddComponent(); parentConstraint.SetSources(new List(new [] { new ConstraintSource() { sourceTransform = parentBone, weight = 1.0f } })); parentConstraint.locked = false; parentConstraint.SetRotationOffset(0, Vector3.zero); parentConstraint.SetTranslationOffset(0, Vector3.zero); parentConstraint.locked = true; parentConstraint.constraintActive = true; // Add scale constraint if (scaleToggle) { var scaleConstraint = container.gameObject.AddComponent(); scaleConstraint.AddSource(new ConstraintSource() { sourceTransform = parentBone, weight = 1.0f }); scaleConstraint.constraintActive = true; } // Find/Create FX controller avatar.customizeAnimationLayers = true; var fxAnimIndex = -1; for (var i = 0; i < avatar.baseAnimationLayers.Length; i++) { if (avatar.baseAnimationLayers[i].type == VRCAvatarDescriptor.AnimLayerType.FX) fxAnimIndex = i; } if (avatar.baseAnimationLayers[fxAnimIndex].isDefault || avatar.baseAnimationLayers[fxAnimIndex].animatorController == null) { InitGeneratedAssetFolder(); var fxController = AnimatorController.CreateAnimatorControllerAtPath(generatedAssetsFolder + "/ThiccWaterFX.controller"); avatar.baseAnimationLayers[fxAnimIndex].isDefault = false; avatar.baseAnimationLayers[fxAnimIndex].animatorController = fxController; } // Remove [existing] menu from parent menu if (parentMenu) { parentMenu.controls.RemoveAll(c => c.name == "Thicc Water"); } // Per selected emitter... var expressionParams = avatar.expressionParameters.parameters.ToList(); var fxLayers = ((AnimatorController)avatar.baseAnimationLayers[fxAnimIndex].animatorController).layers.ToList(); var fxParams = ((AnimatorController)avatar.baseAnimationLayers[fxAnimIndex].animatorController).parameters.ToList(); var menu = ScriptableObject.CreateInstance(); menu.controls = new List(); for (var i = 0; i < resources.emitterNames.Length; i++) { if (selectedEmitters[i]) { // Add component[s] var emitter = Instantiate(resources.emitters[i], Vector3.zero, Quaternion.identity, container); emitter.name = resources.emitterNames[i]; emitter.transform.localPosition = Vector3.zero; emitter.transform.localRotation = Quaternion.identity; // Add (non-existing) animator param if (expressionParams.All(param => param.name != emitter.name)) { expressionParams.Add(new VRCExpressionParameters.Parameter() { name = emitter.name, valueType = VRCExpressionParameters.ValueType.Bool, defaultValue = 0, saved = false }); } if (fxParams.All(param => param.name != emitter.name)) { fxParams.Add(new AnimatorControllerParameter() { name = emitter.name, type = AnimatorControllerParameterType.Bool, defaultBool = false }); } // Clear/Add animator layer fxLayers.RemoveAll(layer => layer.name == emitter.name); fxLayers.Add(resources.AnimatorControllers[i].layers.First()); // Add control to menu menu.controls.Add(resources.menu.controls.Find(c => c.name == emitter.name)); } } // Reassign animator arrays avatar.expressionParameters.parameters = expressionParams.ToArray(); ((AnimatorController)avatar.baseAnimationLayers[fxAnimIndex].animatorController).layers = fxLayers.ToArray(); ((AnimatorController)avatar.baseAnimationLayers[fxAnimIndex].animatorController).parameters = fxParams.ToArray(); // Add menu if (parentMenu) { InitGeneratedAssetFolder(); AssetDatabase.CreateAsset(menu, generatedAssetsFolder + "/ThiccWaterMenu.asset"); parentMenu.controls.Add(new VRCExpressionsMenu.Control() { icon = resources.menuIcon, name = "Thicc Water", type = VRCExpressionsMenu.Control.ControlType.SubMenu, subMenu = menu }); } EditorUtility.DisplayDialog("Complete", "Installation complete", "OK"); } /** * Remove bottle meshes from root objects */ public void RemoveBottles() { // Sanity checks if (!avatar) { EditorUtility.DisplayDialog("Error", "Please select an avatar to install emitters onto", "OK"); return; } var parent = avatar.transform.Find("ThiccWater"); if (parent) { for (var i = 0; i < parent.childCount; i++) { var meshFilter = parent.GetChild(i).gameObject.GetComponent(); if (meshFilter) { DestroyImmediate(meshFilter); } var meshRenderer = parent.GetChild(i).gameObject.GetComponent(); if (meshRenderer) { DestroyImmediate(meshRenderer); } } } } /** * Ensure GeneratedAssets folder exists; if not, create it */ public void InitGeneratedAssetFolder() { if (AssetDatabase.IsValidFolder(generatedAssetsFolder)) return; var i = generatedAssetsFolder.LastIndexOf('/'); AssetDatabase.CreateFolder(generatedAssetsFolder.Substring(0, i), generatedAssetsFolder.Substring(i+1)); } }