Last active
June 9, 2024 13:57
-
-
Save PiMaker/02d0dafe7e424a6ac198e2442bb66ac7 to your computer and use it in GitHub Desktop.
Avatar Phalanx - A way to upload multiple versions of a VRChat avatar with a single click
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Made by _pi_ in VRChat/@pimaker on GitHub | |
Usage: | |
* Make an empty GameObject | |
* "Add Component" a Phalanx | |
* Drop in your Avatar Descriptor | |
* Click "Get Data From Avatar" | |
* Get your Avatar ID from the pipeline component beneath the avatar descriptor | |
* Optionally: Set up a thumbnail and an overlay text to superimpose onto it dynamically | |
* Click one of the provided upload buttons | |
* Lean back and wait until Unity calms down again - all buttons will be pressed for you! | |
Available under the terms of the MIT license: | |
Copyright (c) 2022 @pimaker on GitHub | |
Permission is hereby granted, free of charge, to any person obtaining | |
a copy of this software and associated documentation files (the | |
"Software"), to deal in the Software without restriction, including | |
without limitation the rights to use, copy, modify, merge, publish, | |
distribute, sublicense, and/or sell copies of the Software, and to | |
permit persons to whom the Software is furnished to do so, subject to | |
the following conditions: | |
The above copyright notice and this permission notice shall be | |
included in all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.UI; | |
using UnityEngine.EventSystems; | |
using VRC.Core; | |
using VRC.SDK3.Avatars.Components; | |
#if UNITY_EDITOR | |
using UnityEditor; | |
using UnityEditor.SceneManagement; | |
#endif | |
namespace pi.AvatarPhalanx | |
{ | |
[ExecuteAlways] | |
public class Phalanx : MonoBehaviour | |
{ | |
public GameObject Avatar; | |
public string AvatarId; | |
public string AvatarName; | |
public string Description; | |
public Texture2D AvatarImage; | |
public string AvatarImageOverlay; | |
public GameObject[] EnableObjects; | |
public GameObject[] DisableObjects; | |
public Vector3 Scale; | |
public Vector3 EyePos; | |
[SerializeField] internal bool AutoUpload = true; | |
[SerializeField] internal bool UploadImage = false; | |
[SerializeReference] internal Phalanx ContinueWith; | |
internal enum PhalanxState | |
{ | |
Idle, | |
WaitingForUpload, | |
Uploading, | |
} | |
[SerializeField] internal PhalanxState State = PhalanxState.Idle; | |
[SerializeField] internal bool SetInactiveOnNextIdleAwake = false; | |
public void OnEnable() => Awake(); | |
public void Awake() | |
{ | |
allowCloseThread = false; | |
if (State == PhalanxState.WaitingForUpload) | |
{ | |
RunPhalanxAtRuntime(); | |
} | |
#if UNITY_EDITOR | |
else if (State == PhalanxState.Uploading) | |
{ | |
if (EditorApplication.isPlaying) return; | |
State = PhalanxState.Idle; | |
if (SetInactiveOnNextIdleAwake) | |
{ | |
Debug.Log("Phalanx: SetInactiveOnNextIdleAwake"); | |
Avatar.gameObject.SetActive(false); | |
SetInactiveOnNextIdleAwake = false; | |
} | |
if (ContinueWith != null) | |
{ | |
var cw = ContinueWith; | |
ContinueWith = null; | |
EditorApplication.delayCall += cw.RunPhalanx; | |
} | |
} | |
#endif | |
} | |
#if UNITY_EDITOR | |
internal bool ValidatePhalanx() | |
{ | |
if (Avatar == null) | |
{ | |
EditorUtility.DisplayDialog("Error", "Phalanx validation failed: Avatar is null", "Abort"); | |
return false; | |
} | |
var pipeline = Avatar.GetComponent<PipelineManager>(); | |
if (pipeline == null) | |
{ | |
EditorUtility.DisplayDialog("Error", "Phalanx validation failed: PipelineManager is null", "Abort"); | |
return false; | |
} | |
var avatarDescriptor = Avatar.GetComponent<VRCAvatarDescriptor>(); | |
if (avatarDescriptor == null) | |
{ | |
EditorUtility.DisplayDialog("Error", "Phalanx validation failed: VRCAvatarDescriptor is null", "Abort"); | |
return false; | |
} | |
return true; | |
} | |
internal void ApplyPhalanx() | |
{ | |
var pipeline = Avatar.GetComponent<PipelineManager>(); | |
var avatarDesc = Avatar.GetComponent<VRCAvatarDescriptor>(); | |
pipeline.blueprintId = AvatarId; | |
if (!Avatar.gameObject.activeSelf) | |
{ | |
Debug.Log("Phalanx: Activating game object"); | |
SetInactiveOnNextIdleAwake = true; | |
Avatar.gameObject.SetActive(true); | |
} | |
var others = FindObjectsOfType<VRCAvatarDescriptor>(); | |
foreach (var other in others) | |
{ | |
if (other == avatarDesc) continue; | |
other.gameObject.SetActive(false); | |
} | |
if (EnableObjects != null) | |
{ | |
foreach (var en in EnableObjects) | |
{ | |
if (!en) continue; | |
en.SetActive(true); | |
en.tag = "Untagged"; | |
} | |
} | |
if (DisableObjects != null) | |
{ | |
foreach (var dis in DisableObjects) | |
{ | |
if (!dis) continue; | |
dis.SetActive(false); | |
dis.tag = "EditorOnly"; | |
} | |
} | |
Avatar.transform.localScale = Scale; | |
avatarDesc.ViewPosition = EyePos; | |
} | |
internal void RunPhalanx() | |
{ | |
if (!ValidatePhalanx()) return; | |
Debug.Log("Running Phalanx for " + AvatarName + " (" + AvatarId + ")"); | |
State = PhalanxState.WaitingForUpload; | |
ApplyPhalanx(); | |
AssetDatabase.SaveAssets(); | |
EditorSceneManager.SaveOpenScenes(); | |
// start avatar upload | |
//VRC.SDKBase.Editor.VRC_SdkBuilder.VerifyCredentials(); | |
VRC.SDKBase.Editor.VRC_SdkBuilder.ExportAndUploadAvatarBlueprint(Avatar); | |
} | |
#endif | |
private void RunPhalanxAtRuntime() | |
{ | |
if (State != PhalanxState.WaitingForUpload) return; | |
State = PhalanxState.Uploading; | |
StartCoroutine(RunPhalanxAtRuntimeStage2()); | |
} | |
private IEnumerator RunPhalanxAtRuntimeStage2() | |
{ | |
yield return new WaitForSeconds(1.5f); | |
var titleText = GameObject.Find("VRCSDK/UI/Canvas/AvatarPanel/Title Text") | |
.GetComponent<Text>(); | |
titleText.text = "Phalanx Upload!"; | |
yield return new WaitForSeconds(2.5f); | |
// we're running baby! | |
Debug.Log("Runtime Phalanx for " + AvatarName + " (" + AvatarId + ")"); | |
var nameField = GameObject.Find("VRCSDK/UI/Canvas/AvatarPanel/Avatar Info Panel/Settings Section/Name Input Field") | |
.GetComponent<InputField>(); | |
var descriptionField = GameObject.Find("VRCSDK/UI/Canvas/AvatarPanel/Avatar Info Panel/Settings Section/DescriptionBackdrop/Description Input Field") | |
.GetComponent<InputField>(); | |
var warrantToggle = GameObject.Find("VRCSDK/UI/Canvas/AvatarPanel/Avatar Info Panel/Settings Section/Upload Section/ToggleWarrant") | |
.GetComponent<Toggle>(); | |
var uploadButton = GameObject.Find("VRCSDK/UI/Canvas/AvatarPanel/Avatar Info Panel/Settings Section/Upload Section/UploadButton") | |
.GetComponent<Button>(); | |
nameField.text = AvatarName; | |
descriptionField.text = Description; | |
if (UploadImage && AvatarImage != null) | |
{ | |
var cam = GameObject.Find("VRCCam").GetComponent<Camera>(); | |
cam.orthographic = true; | |
cam.orthographicSize = 1.0f; | |
cam.nearClipPlane = 0.1f; | |
cam.farClipPlane = 1.2f; | |
cam.transform.position = new Vector3(0, -10, 0); | |
var canvas = new GameObject(); | |
canvas.transform.SetParent(cam.transform, false); | |
canvas.transform.localPosition += Vector3.forward; | |
canvas.transform.localScale = new Vector3(2 * cam.aspect, 2, 1); | |
canvas.AddComponent<MeshFilter>().sharedMesh = Resources.GetBuiltinResource<Mesh>("Quad.fbx"); | |
var canvasRenderer = canvas.AddComponent<MeshRenderer>(); | |
canvasRenderer.sharedMaterial = new Material(Shader.Find("Unlit/Texture")); | |
canvasRenderer.sharedMaterial.SetTexture("_MainTex", AvatarImage); | |
if (!string.IsNullOrEmpty(AvatarImageOverlay)) | |
{ | |
var overlayCanvas = new GameObject(); | |
overlayCanvas.transform.SetParent(cam.transform, false); | |
overlayCanvas.transform.localPosition += Vector3.forward * 0.99f; | |
overlayCanvas.transform.localScale = Vector3.one * 0.002f; | |
var overlayCC = overlayCanvas.AddComponent<Canvas>(); | |
overlayCC.renderMode = RenderMode.WorldSpace; | |
overlayCC.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 1000.0f * cam.aspect); | |
overlayCC.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 1000.0f); | |
var textGO = new GameObject(); | |
textGO.transform.SetParent(overlayCanvas.transform, false); | |
var textComp = textGO.AddComponent<Text>(); | |
var textTrans = textGO.GetComponent<RectTransform>(); | |
textTrans.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 1000.0f * cam.aspect - 90.0f); | |
textTrans.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 1000.0f - 75.0f); | |
textComp.text = AvatarImageOverlay; | |
textComp.font = Resources.GetBuiltinResource<Font>("Arial.ttf"); | |
textComp.fontSize = 180; | |
textComp.color = new Color(1.0f, 0.2f, 0.0f, 0.8f); | |
textComp.alignment = TextAnchor.UpperCenter; | |
textComp.fontStyle = FontStyle.Bold; | |
} | |
var imageUploadToggle = GameObject.Find("VRCSDK/UI/Canvas/AvatarPanel/Avatar Info Panel/Thumbnail Section/ImageUploadToggle") | |
.GetComponent<Toggle>(); | |
imageUploadToggle.isOn = true; | |
} | |
if (AutoUpload) | |
{ | |
warrantToggle.isOn = true; | |
allowCloseThread = true; | |
var t = new System.Threading.Thread(CloseMsgBox); | |
t.Start(); | |
uploadButton.OnPointerClick(new PointerEventData(EventSystem.current)); | |
} | |
} | |
[System.Runtime.InteropServices.DllImport("user32.dll")] | |
public static extern int FindWindow(string lpClassName, string lpWindowName); | |
[System.Runtime.InteropServices.DllImport("user32.dll")] | |
public static extern int SendMessage(int hWnd, uint Msg, int wParam, int lParam); | |
public const int WM_SYSCOMMAND = 0x0112; | |
public const int SC_CLOSE = 0xF060; | |
private bool allowCloseThread = false; | |
private void CloseMsgBox() | |
{ | |
/* | |
Window Spy: | |
VRChat SDK | |
ahk_class #32770 | |
ahk_exe Unity.exe | |
ahk_pid 26684 | |
*/ | |
System.Threading.Thread.Sleep(2500); | |
for (int i = 0; i < 120; i++) | |
{ | |
var handle = FindWindow("#32770", "VRChat SDK"); | |
if (handle > 0) | |
{ | |
SendMessage(handle, WM_SYSCOMMAND, SC_CLOSE, 0); | |
break; | |
} | |
if (!allowCloseThread) return; | |
System.Threading.Thread.Sleep(1000); | |
} | |
} | |
} | |
#if UNITY_EDITOR | |
[CustomEditor(typeof(Phalanx))] | |
public class PhalanxEditor : Editor | |
{ | |
public override void OnInspectorGUI() | |
{ | |
var ph = target as Phalanx; | |
serializedObject.Update(); | |
var avatarProp = serializedObject.FindProperty("Avatar"); | |
var avatarIdProp = serializedObject.FindProperty("AvatarId"); | |
var scaleProp = serializedObject.FindProperty("Scale"); | |
var eyePosProp = serializedObject.FindProperty("EyePos"); | |
var avatarImgProp = serializedObject.FindProperty("AvatarImage"); | |
var avatarNameProp = serializedObject.FindProperty("AvatarName"); | |
var prevAvatar = avatarProp.objectReferenceValue; | |
EditorGUILayout.ObjectField(avatarProp); | |
if (GUILayout.Button("Get Data From Avatar") || (string.IsNullOrWhiteSpace(avatarIdProp.stringValue) && prevAvatar != avatarProp.objectReferenceValue)) | |
{ | |
var newAvGO = avatarProp.objectReferenceValue as GameObject; | |
if (newAvGO != null) | |
{ | |
if (scaleProp.vector3Value == Vector3.zero) | |
{ | |
scaleProp.vector3Value = newAvGO.transform.localScale; | |
} | |
var pipeline = newAvGO.GetComponent<PipelineManager>(); | |
if (pipeline != null) | |
{ | |
avatarIdProp.stringValue = pipeline.blueprintId; | |
} | |
var avatarDesc = newAvGO.GetComponent<VRCAvatarDescriptor>(); | |
if (avatarDesc != null) | |
{ | |
eyePosProp.vector3Value = avatarDesc.ViewPosition; | |
} | |
} | |
} | |
EditorGUILayout.PropertyField(avatarIdProp); | |
EditorGUILayout.PropertyField(avatarNameProp); | |
EditorGUILayout.PropertyField(serializedObject.FindProperty("Description")); | |
EditorGUILayout.PropertyField(avatarImgProp); | |
EditorGUILayout.PropertyField(serializedObject.FindProperty("AvatarImageOverlay")); | |
EditorGUILayout.Space(); | |
EditorGUILayout.PropertyField(serializedObject.FindProperty("EnableObjects")); | |
EditorGUILayout.PropertyField(serializedObject.FindProperty("DisableObjects")); | |
EditorGUILayout.PropertyField(scaleProp); | |
EditorGUILayout.PropertyField(eyePosProp); | |
EditorGUILayout.Space(); | |
EditorGUILayout.LabelField("State: " + ph.State); | |
EditorGUILayout.LabelField("SetInactiveOnNextIdleAwake: " + ph.SetInactiveOnNextIdleAwake); | |
EditorGUILayout.Space(); | |
EditorGUILayout.PropertyField(serializedObject.FindProperty("AutoUpload")); | |
if (avatarImgProp.objectReferenceValue != null) | |
{ | |
EditorGUILayout.PropertyField(serializedObject.FindProperty("UploadImage")); | |
} | |
EditorGUILayout.Space(); | |
serializedObject.ApplyModifiedProperties(); | |
if (GUILayout.Button("Apply Phalanx locally")) | |
{ | |
ph.ApplyPhalanx(); | |
ph.SetInactiveOnNextIdleAwake = false; | |
} | |
if (GUILayout.Button("Run This Phalanx (" + avatarNameProp.stringValue + ")")) | |
{ | |
ph.RunPhalanx(); | |
} | |
if (GUILayout.Button("Run all Phalanxes on '" + ph.gameObject.name + "'")) | |
{ | |
RunAllPhalanxes(ph.gameObject); | |
} | |
if (GUILayout.Button("Run all Phalanxes")) | |
{ | |
RunAllPhalanxes(); | |
} | |
EditorGUILayout.Space(); | |
EditorGUILayout.HelpBox("Clicking the upload buttons above means you consent to the VRChat terms of use regarding uploading content.", MessageType.Info); | |
} | |
public static void RunAllPhalanxes(GameObject on = null) | |
{ | |
List<Phalanx> found = new List<Phalanx>(); | |
if (on == null) | |
{ | |
foreach (Phalanx br in Resources.FindObjectsOfTypeAll(typeof(Phalanx)) as Phalanx[]) | |
{ | |
if (!EditorUtility.IsPersistent(br.gameObject.transform.root.gameObject) && | |
!(br.gameObject.hideFlags == HideFlags.NotEditable || br.gameObject.hideFlags == HideFlags.HideAndDontSave)) | |
{ | |
br.AutoUpload = true; | |
br.ContinueWith = null; | |
found.Add(br); | |
} | |
} | |
} | |
else | |
{ | |
foreach (Phalanx br in on.GetComponents<Phalanx>()) | |
{ | |
br.AutoUpload = true; | |
br.ContinueWith = null; | |
found.Add(br); | |
} | |
} | |
if (found.Count == 0) return; | |
foreach (var f in found) | |
{ | |
if (!f.ValidatePhalanx()) | |
{ | |
return; | |
} | |
} | |
for (int i = 0; i < found.Count - 1; i++) | |
{ | |
found[i].ContinueWith = found[i + 1]; | |
} | |
found[0].RunPhalanx(); | |
} | |
[MenuItem("Tools/Phalanx/Enable In All Phalanxes")] | |
public static void EnableGameObjectsInAllPhalanxes() | |
{ | |
SetGameObjectsInAllPhalanxes(true); | |
} | |
[MenuItem("Tools/Phalanx/Disable In All Phalanxes")] | |
public static void DisableGameObjectsInAllPhalanxes() | |
{ | |
SetGameObjectsInAllPhalanxes(false); | |
} | |
[MenuItem("Tools/Phalanx/Unset In All Phalanxes")] | |
public static void UnsetGameObjectsInAllPhalanxes() | |
{ | |
SetGameObjectsInAllPhalanxes(null); | |
} | |
private static void SetGameObjectsInAllPhalanxes(bool? state) | |
{ | |
var objs = Selection.gameObjects; | |
if (objs == null || objs.Length == 0) return; | |
foreach (Phalanx br in Resources.FindObjectsOfTypeAll(typeof(Phalanx)) as Phalanx[]) | |
{ | |
if (!EditorUtility.IsPersistent(br.gameObject.transform.root.gameObject) && | |
!(br.gameObject.hideFlags == HideFlags.NotEditable || br.gameObject.hideFlags == HideFlags.HideAndDontSave) && | |
br.enabled) | |
{ | |
foreach (var o in objs) | |
{ | |
Debug.Log($"DisableGameObjectInAllPhalanxes: Phalanx={br.name} Obj={o.name}"); | |
if (!state.HasValue) | |
{ | |
var list = new List<GameObject>(br.EnableObjects); | |
list.Remove(o); | |
br.EnableObjects = list.ToArray(); | |
list = new List<GameObject>(br.DisableObjects); | |
list.Remove(o); | |
br.DisableObjects = list.ToArray(); | |
} | |
else if (state.Value) | |
{ | |
var list = new List<GameObject>(br.EnableObjects); | |
list.Add(o); | |
br.EnableObjects = list.ToArray(); | |
list = new List<GameObject>(br.DisableObjects); | |
list.Remove(o); | |
br.DisableObjects = list.ToArray(); | |
} | |
else | |
{ | |
var list = new List<GameObject>(br.DisableObjects); | |
list.Add(o); | |
br.DisableObjects = list.ToArray(); | |
list = new List<GameObject>(br.EnableObjects); | |
list.Remove(o); | |
br.EnableObjects = list.ToArray(); | |
} | |
} | |
} | |
} | |
} | |
} | |
#endif | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment