Skip to content

Instantly share code, notes, and snippets.

@PiMaker
Last active June 9, 2024 13:57
Show Gist options
  • Save PiMaker/02d0dafe7e424a6ac198e2442bb66ac7 to your computer and use it in GitHub Desktop.
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
/*
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