diff --git a/Editor/AV3InventoriesWindow.cs b/Editor/AV3InventoriesWindow.cs deleted file mode 100644 index 5eb429e..0000000 --- a/Editor/AV3InventoriesWindow.cs +++ /dev/null @@ -1,320 +0,0 @@ -using Boo.Lang; -using NUnit.Framework.Internal.Execution; -using System; -using System.IO; -using UnityEditor; -using UnityEngine; -using UnityEngine.Experimental.UIElements; -using VRC.SDK3.Avatars.Components; -using VRC.SDK3.Avatars.ScriptableObjects; - -public class AV3InventoriesWindow : EditorWindow -{ - private readonly AV3InventoriesManager manager = new AV3InventoriesManager(); - - int windowTab; - bool focused; - List pages = new List(); - - Vector2 scroll; - - [MenuItem("Window/AV3 Tools/Inventory Inventor/Manage Inventory")] - public static void ManageInventory() - { - AV3InventoriesWindow window = (AV3InventoriesWindow)GetWindow(typeof(AV3InventoriesWindow), false, "Inventory Inventor"); - //window.minSize = new Vector2(375f, 294f); - window.wantsMouseMove = true; - window.Show(); - } - - private void OnFocus() - { - manager.UpdatePaths(); - } - - private void OnGUI() - { - GUILayout.BeginVertical(); - GUILayout.BeginArea(new Rect((position.width / 2f) - (375f / 2f), 5f, 375f, 800f)); - windowTab = GUILayout.Toolbar(windowTab, new string[] { "Create", "About" }); - DrawLine(false); - switch (windowTab) - { - case 0: - if (EditorApplication.isPlaying) - { - EditorGUILayout.Space(); - EditorGUILayout.HelpBox("Inventories can only be created outside of Play Mode.", MessageType.Info); - } - else - { - GUILayout.BeginVertical(); - DrawSetupWindow(); - GUILayout.EndVertical(); - GUILayout.EndArea(); - GUILayout.FlexibleSpace(); - EditorGUILayout.Space(); - GUILayout.EndVertical(); - } - break; - case 1: - GUILayout.BeginVertical(); - DrawAboutWindow(); - GUILayout.EndVertical(); - GUILayout.EndArea(); - GUILayout.FlexibleSpace(); - EditorGUILayout.Space(); - GUILayout.EndVertical(); - break; - } - } - - private void DrawSetupWindow() - { - EditorGUILayout.BeginVertical(); - GUILayout.BeginVertical(GUILayout.Height(54f)); - EditorGUI.BeginChangeCheck(); - GUILayout.FlexibleSpace(); - SelectAvatarDescriptor(); - if (manager.avatar == null) - { - EditorGUILayout.HelpBox("No Avatars found in the current Scene!", MessageType.Warning); - } - GUILayout.FlexibleSpace(); - GUILayout.EndVertical(); - DrawLine(); - GUILayout.Label("Optional Settings", EditorStyles.boldLabel); - EditorGUILayout.BeginVertical(new GUIStyle(GUI.skin.GetStyle("Box"))); - manager.menu = (VRCExpressionsMenu)EditorGUILayout.ObjectField(new GUIContent("Expressions Menu", "(Optional) The Expressions Menu you want the inventory controls added to. Leave this empty if you don't want any menus to be affected.\n(Controls will be added as a submenu.)"), manager.menu, typeof(VRCExpressionsMenu), true); - GUILayout.EndVertical(); - EditorGUILayout.Space(); - DrawLine(); - GUILayout.Label("Inventory Settings", EditorStyles.boldLabel); - EditorGUILayout.BeginVertical(new GUIStyle(GUI.skin.GetStyle("Box")), GUILayout.MaxHeight(200f)); - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField(new GUIContent("Menu Name", "Name used for the Expressions Menu."), new GUIStyle(GUI.skin.GetStyle("Box")) { alignment = TextAnchor.MiddleCenter, normal = new GUIStyleState() { background = null }, richText = true }, GUILayout.Width(355f / 2)); - GUILayout.FlexibleSpace(); - EditorGUILayout.LabelField(new GUIContent("Animation", "Animation to be toggled by the menu."), new GUIStyle(GUI.skin.GetStyle("Box")) { alignment = TextAnchor.MiddleCenter, normal = new GUIStyleState() { background = null }, richText = true }, GUILayout.Width(355f / 2)); - EditorGUILayout.EndHorizontal(); - scroll = EditorGUILayout.BeginScrollView(scroll); - int pageTotal = (manager.toggleables.ToArray().Length / 8) + 1; - while (pages.ToArray().Length > pageTotal) - { - pages.RemoveAt(pages.ToArray().Length - 1); - } - while (pages.ToArray().Length < pageTotal) - { - pages.Add(true); - } - int count = 0; - for (int i = 0; i < manager.toggleables.ToArray().Length; i++) - { - if (i % 8 == 0 && manager.toggleables.ToArray().Length > 8) - { - count++; - EditorGUILayout.BeginHorizontal(); - pages[count - 1] = EditorGUILayout.Foldout(pages[count - 1], "Page " + count, true); - EditorGUILayout.EndHorizontal(); - } - if (count == 0 || pages[count - 1]) - { - EditorGUILayout.BeginHorizontal(); - manager.aliases[i] = EditorGUILayout.TextField(manager.aliases[i]); - manager.toggleables[i] = (AnimationClip)EditorGUILayout.ObjectField(manager.toggleables[i], typeof(AnimationClip), false); - EditorGUILayout.EndHorizontal(); - } - } - EditorGUILayout.EndScrollView(); - GUILayout.FlexibleSpace(); - EditorGUILayout.BeginHorizontal(); - if (manager.toggleables.ToArray().Length < 24) - { - if (manager.toggleables.ToArray().Length == 1) - { - if (GUILayout.Button("Add", GUILayout.Width(355f))) - { - manager.toggleables.Add(null); - manager.aliases.Add("Slot " + manager.toggleables.ToArray().Length); - } - } - else - { - if (GUILayout.Button("Add", GUILayout.Width(355f / 2))) - { - manager.toggleables.Add(null); - manager.aliases.Add("Slot " + manager.toggleables.ToArray().Length); - } - } - } - GUILayout.FlexibleSpace(); - if (manager.toggleables.ToArray().Length > 1) - { - if (manager.toggleables.ToArray().Length == 24) - { - if (GUILayout.Button("Remove", GUILayout.Width(355f))) - { - if (manager.toggleables.ToArray().Length > 1) - { - manager.toggleables.Remove(manager.toggleables[manager.toggleables.ToArray().Length - 1]); - manager.aliases.Remove(manager.aliases[manager.aliases.ToArray().Length - 1]); - } - } - } - else - { - if (GUILayout.Button("Remove", GUILayout.Width(355f / 2))) - { - if (manager.toggleables.ToArray().Length > 1) - { - manager.toggleables.Remove(manager.toggleables[manager.toggleables.ToArray().Length - 1]); - manager.aliases.Remove(manager.aliases[manager.aliases.ToArray().Length - 1]); - } - } - } - } - EditorGUILayout.EndHorizontal(); - EditorGUILayout.EndVertical(); - EditorGUILayout.Space(); - DrawLine(); - GUILayout.Label("Output Settings", EditorStyles.boldLabel); - EditorGUILayout.BeginVertical(new GUIStyle(GUI.skin.GetStyle("Box")), GUILayout.Height(50f)); - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField(new GUIContent("Destination", "The folder where generated files will be saved to."), new GUIStyle(GUI.skin.GetStyle("Box")) { alignment = TextAnchor.MiddleLeft, normal = new GUIStyleState() { background = null } }); - GUILayout.FlexibleSpace(); - string displayPath = manager.outputPath.Replace('\\', '/'); - displayPath = ((new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, hover = GUI.skin.GetStyle("Button").active }).CalcSize(new GUIContent("" + displayPath + "")).x > 210) ? "..." + displayPath.Substring((displayPath.IndexOf('/', displayPath.Length - 29) != -1) ? displayPath.IndexOf('/', displayPath.Length - 29) : displayPath.Length - 29) : displayPath; - while ((new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, hover = GUI.skin.GetStyle("Button").active }).CalcSize(new GUIContent("" + displayPath + "")).x > 210) - { - displayPath = "..." + displayPath.Substring(4); - } - if (GUILayout.Button("" + displayPath + "", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, hover = GUI.skin.GetStyle("Button").active }, GUILayout.MinWidth(210))) - { - string absPath = EditorUtility.OpenFolderPanel("Destination Folder", "", ""); - if (absPath.StartsWith(Application.dataPath)) - { - manager.outputPath = "Assets" + absPath.Substring(Application.dataPath.Length); - } - } - EditorGUILayout.EndHorizontal(); - GUILayout.BeginHorizontal(); - GUILayout.Label(new GUIContent("Overwrite All", "Automatically overwrite existing files if needed."), GUILayout.Width(145)); - manager.autoOverwrite = Convert.ToBoolean(GUILayout.Toolbar(Convert.ToInt32(manager.autoOverwrite), new string[] { "No", "Yes" })); - GUILayout.EndHorizontal(); - EditorGUILayout.EndVertical(); - EditorGUILayout.Space(); - DrawLine(); - if (GUILayout.Button("Create")) - { - manager.CreateInventory(); - EditorUtility.ClearProgressBar(); - } - EditorGUILayout.EndVertical(); - - if (mouseOverWindow != null && mouseOverWindow == this) - { - Repaint(); - focused = true; - } - else if (focused && mouseOverWindow == null) - { - Repaint(); - focused = false; - } - } - - private void DrawAboutWindow() - { - string version = (AssetDatabase.FindAssets("VERSION", new string[] { manager.relativePath }).Length > 0) ? " " + File.ReadAllText(AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("VERSION", new string[] { manager.relativePath })[0])) : ""; - GUILayout.BeginHorizontal(); - GUILayout.FlexibleSpace(); - GUILayout.Box("Inventory Inventor" + version + "", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true }, GUILayout.Width(300f)); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.BeginHorizontal(); - GUILayout.FlexibleSpace(); - GUILayout.Box("Author: Joshuarox100", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, normal = new GUIStyleState() { background = null } }); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - EditorGUILayout.Space(); - DrawLine(); - GUILayout.BeginHorizontal(); - GUILayout.FlexibleSpace(); - GUILayout.Box("Summary", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true }, GUILayout.Width(200f)); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.BeginHorizontal(); - GUILayout.FlexibleSpace(); - GUILayout.Box("Make upgrading to 3.0 a breeze with AV3 Overrides!", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, normal = new GUIStyleState() { background = null } }, GUILayout.Width(350f)); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - EditorGUILayout.Space(); - DrawLine(); - GUILayout.BeginHorizontal(); - GUILayout.FlexibleSpace(); - GUILayout.Box("Troubleshooting", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true }, GUILayout.Width(200f)); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.BeginHorizontal(); - GUILayout.FlexibleSpace(); - GUILayout.Box("If you're having issues or want to contact me, you can find more information at the Github page linked below!", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, normal = new GUIStyleState() { background = null } }, GUILayout.Width(350f)); - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - EditorGUILayout.Space(); - DrawLine(false); - GUILayout.Label("Github Repository", new GUIStyle(EditorStyles.boldLabel) { alignment = TextAnchor.UpperCenter }); - GUILayout.BeginHorizontal(); - GUILayout.FlexibleSpace(); - if (GUILayout.Button("Open in Browser", GUILayout.Width(250))) - { - Application.OpenURL("https://github.com/Joshuarox100/VRC-AV3-Inventories"); - } - GUILayout.FlexibleSpace(); - GUILayout.EndHorizontal(); - GUILayout.FlexibleSpace(); - } - - /* - * These next two functions are literally just code from the Expression Menu for selecting the avatar. - */ - - void SelectAvatarDescriptor() - { - var descriptors = FindObjectsOfType(); - if (descriptors.Length > 0) - { - //Compile list of names - string[] names = new string[descriptors.Length]; - for (int i = 0; i < descriptors.Length; i++) - names[i] = descriptors[i].gameObject.name; - - //Select - var currentIndex = Array.IndexOf(descriptors, manager.avatar); - var nextIndex = EditorGUILayout.Popup(new GUIContent("Active Avatar", "The Avatar you want to setup scaling for."), currentIndex, names); - if (nextIndex < 0) - nextIndex = 0; - if (nextIndex != currentIndex) - SelectAvatarDescriptor(descriptors[nextIndex]); - } - else - SelectAvatarDescriptor(null); - } - void SelectAvatarDescriptor(VRCAvatarDescriptor desc) - { - if (desc == manager.avatar) - return; - - manager.avatar = desc; - } - - private void DrawLine(bool addSpace = true) - { - var rect = EditorGUILayout.BeginHorizontal(); - Handles.color = Color.gray; - Handles.DrawLine(new Vector2(rect.x - 15, rect.y), new Vector2(rect.width + 15, rect.y)); - EditorGUILayout.EndHorizontal(); - if (addSpace) - { - EditorGUILayout.Space(); - } - } -} diff --git a/Editor/InventoryInventor.cs b/Editor/InventoryInventor.cs new file mode 100644 index 0000000..9944541 --- /dev/null +++ b/Editor/InventoryInventor.cs @@ -0,0 +1,561 @@ +using Boo.Lang; +using System; +using System.IO; +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; + +public class InventoryInventor : EditorWindow +{ + //Model Object + private readonly InventoryInventorManager manager = new InventoryInventorManager(); + + //Window Size + private Rect windowSize = new Rect(0, 0, 375f, 800f); + + //Various Trackers + private int windowTab; + private bool focused; + private ReorderableList itemList; + private readonly List pages = new List(); + private int currentPage = 0; + + //Class used for representing an Inventory Page + // string name : name of the page + //List items : list of items held in the page + private class Page + { + public string name; + public readonly List items; + + //Constructor + public Page(string name, ref ReorderableList itemList) + { + this.name = name; + items = new List() { new ListItem("Slot 1") }; + itemList.list = items; + } + + //Returns array of item names + public string[] GetNames() + { + List names = new List(); + foreach (ListItem item in items) + { + names.Add(item.name); + } + return names.ToArray(); + } + + //Returns array of item clips + public AnimationClip[] GetClips() + { + List clips = new List(); + foreach (ListItem item in items) + { + clips.Add(item.clip); + } + return clips.ToArray(); + } + } + + //Class used for storing page items + // string name : name of the item + //AnimationClip : animation to be toggled + [Serializable] + private class ListItem + { + public string name; + public AnimationClip clip; + + //Default Constructor + public ListItem() + { + name = "Slot 1"; + clip = null; + } + + //Additional Constructor + public ListItem(string name) + { + this.name = name; + clip = null; + } + } + + [MenuItem("Tools/Avatars 3.0/Inventory Inventor/Manage Inventory")] + public static void ManageInventory() + { + InventoryInventor window = (InventoryInventor)GetWindow(typeof(InventoryInventor), false, "Inventory Inventor"); + window.minSize = new Vector2(375f, 600f); + window.wantsMouseMove = true; + window.Show(); + } + [MenuItem("Tools/Avatars 3.0/Inventory Inventor/Check For Updates")] + public static void CheckForUpdates() + { + InventoryInventorManager.CheckForUpdates(); + } + + //Remove listeners when window is closed to prevent memory leaks + private void OnDestroy() + { + if (itemList != null) + { + itemList.drawHeaderCallback -= DrawHeader; + itemList.drawElementCallback -= DrawElement; + itemList.onAddCallback -= AddItem; + itemList.onRemoveCallback -= RemoveItem; + } + } + + //Relocate file directory path + private void OnFocus() + { + manager.UpdatePaths(); + } + + //Assign listeners to itemList + private void AssignListeners() + { + itemList.drawHeaderCallback += DrawHeader; + itemList.drawElementCallback += DrawElement; + itemList.onAddCallback += AddItem; + itemList.onRemoveCallback += RemoveItem; + } + + //Draw GUI + private void OnGUI() + { + //Make sure itemList isn't null and has listeners assigned + if (itemList == null) + { + itemList = new ReorderableList(null, typeof(ListItem), true, true, true, true) + { + elementHeight = 18f + }; + AssignListeners(); + } + + //Define main window area + GUILayout.BeginVertical(); + windowSize.x = (position.width / 2f) - (375f / 2f); + windowSize.y = 5f; + GUILayout.BeginArea(windowSize); + + //Create toolbar + windowTab = GUILayout.Toolbar(windowTab, new string[] { "Create", "About" }); + DrawLine(false); + + //Respond to current toolbar selection + switch (windowTab) + { + case 0: + if (EditorApplication.isPlaying) + { + //Stop usage within play mode + EditorGUILayout.Space(); + EditorGUILayout.HelpBox("Inventories can only be created outside of Play Mode.", MessageType.Info); + } + else + { + //Draw setup window + GUILayout.BeginVertical(); + DrawSetupWindow(); + GUILayout.EndVertical(); + GUILayout.EndArea(); + GUILayout.FlexibleSpace(); + EditorGUILayout.Space(); + GUILayout.EndVertical(); + } + break; + case 1: + //Draw about window + GUILayout.BeginVertical(); + DrawAboutWindow(); + GUILayout.EndVertical(); + GUILayout.EndArea(); + GUILayout.FlexibleSpace(); + EditorGUILayout.Space(); + GUILayout.EndVertical(); + break; + } + if (GUI.Button(windowSize, "", GUIStyle.none)) + { + //Unfocus item in window when empty space is clicked + GUI.FocusControl(null); + itemList.index = 8; + } + } + + //Draws the setup GUI + private void DrawSetupWindow() + { + EditorGUILayout.BeginVertical(); + + //Check for avatars in the scene + GUILayout.BeginVertical(GUILayout.Height(54f)); + GUILayout.FlexibleSpace(); + SelectAvatarDescriptor(); + if (manager.avatar == null) + { + EditorGUILayout.HelpBox("No Avatars found in the current Scene!", MessageType.Warning); + } + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + DrawLine(); + + //List optional settings + GUILayout.Label("Optional Settings", EditorStyles.boldLabel); + EditorGUILayout.BeginVertical(new GUIStyle(GUI.skin.GetStyle("Box")), GUILayout.Height(65f)); + manager.menu = (VRCExpressionsMenu)EditorGUILayout.ObjectField(new GUIContent("Expressions Menu", "The Expressions Menu you want the inventory controls added to. Leave this empty if you don't want any menus to be affected.\n(Controls will be added as a submenu.)"), manager.menu, typeof(VRCExpressionsMenu), true); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Auto Sync", "Off: Items will not auto-sync with late joiners.\nOn: Items will auto-sync with late joiners."), new GUIStyle(GUI.skin.GetStyle("Box")) { alignment = TextAnchor.MiddleLeft, normal = new GUIStyleState() { background = null } }, GUILayout.ExpandWidth(false)); + manager.syncMode = GUILayout.Toolbar(manager.syncMode, new string[] { "Off", "On" }, GUILayout.Width(210f)); + EditorGUILayout.EndHorizontal(); + if (manager.syncMode == 1) + manager.refreshRate = EditorGUILayout.FloatField(new GUIContent("Refresh Rate", "How long each toggle is given to synchronize with late joiners (seconds per item)."), manager.refreshRate); + if (manager.refreshRate < 0) + manager.refreshRate = 0.05f; + GUILayout.EndVertical(); + EditorGUILayout.Space(); + DrawLine(); + + //List Inventory Settings + GUILayout.Label("Inventory Settings", EditorStyles.boldLabel); + //Make sure at least one page exists + if (pages.ToArray().Length == 0) + { + pages.Add(new Page("Page 1", ref itemList)); + } + string[] pageNames = new string[pages.ToArray().Length]; + for(int i = 0; i < pages.ToArray().Length; i++) + { + pageNames[i] = (i + 1).ToString(); + } + //Draw page renamer + EditorGUILayout.BeginVertical(new GUIStyle(GUI.skin.GetStyle("Box"))); + EditorGUILayout.BeginHorizontal(new GUIStyle(GUI.skin.GetStyle("Box")) { alignment = TextAnchor.MiddleLeft, normal = new GUIStyleState() { background = null } }); + GUILayout.Label("Name:", GUILayout.ExpandWidth(false)); + if (currentPage >= pages.ToArray().Length) + { + currentPage = pages.ToArray().Length - 1; + } + else if (currentPage < 0) + { + currentPage = 0; + } + pages[currentPage].name = EditorGUILayout.TextField(pages[currentPage].name, new GUIStyle(GUI.skin.GetStyle("Box")) { font = EditorStyles.toolbarTextField.font, alignment = TextAnchor.MiddleLeft, normal = EditorStyles.toolbarTextField.normal }); + if (pages[currentPage].name == "") + { + pages[currentPage].name = "Page " + (currentPage + 1); + } + //Draw page navigator + if (GUILayout.Button('\u25C0'.ToString())) + { + if (currentPage != 0) + { + currentPage--; + itemList.list = pages[currentPage].items; + } + } + EditorGUI.BeginChangeCheck(); + currentPage = EditorGUILayout.Popup(currentPage, pageNames, new GUIStyle(GUI.skin.GetStyle("Dropdown")), GUILayout.Width(30f)); + if (EditorGUI.EndChangeCheck()) + { + itemList.list = pages[currentPage].items; + } + if (GUILayout.Button('\u25B6'.ToString())) + { + if (currentPage != pages.ToArray().Length - 1) + { + currentPage++; + itemList.list = pages[currentPage].items; + } + } + EditorGUILayout.EndHorizontal(); + //Draw page creator/remover + EditorGUILayout.BeginHorizontal(new GUIStyle(GUI.skin.GetStyle("Box")) { alignment = TextAnchor.MiddleLeft, normal = new GUIStyleState() { background = null } }); + if (pages.ToArray().Length < 8) + { + if (pages.ToArray().Length == 1) + { + if (GUILayout.Button("Add Page", GUILayout.Width(350f))) + { + pages.Add(new Page("Page " + (pages.ToArray().Length + 1), ref itemList)); + currentPage = pages.ToArray().Length - 1; + itemList.list = pages[currentPage].items; + } + } + else if (GUILayout.Button("Add Page", GUILayout.Width(348f / 2))) + { + pages.Add(new Page("Page " + (pages.ToArray().Length + 1), ref itemList)); + currentPage = pages.ToArray().Length - 1; + itemList.list = pages[currentPage].items; + } + } + if (pages.ToArray().Length > 1) + { + if (pages.ToArray().Length == 8) + { + if (GUILayout.Button("Remove Page", GUILayout.Width(350f))) + { + pages.RemoveAt(currentPage); + currentPage--; + itemList.list = pages[currentPage].items; + } + } + else if (GUILayout.Button("Remove Page", GUILayout.Width(348f / 2))) + { + pages.RemoveAt(currentPage); + currentPage--; + itemList.list = pages[currentPage].items; + } + } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + DrawLine(); + //Draw currently selected page contents + EditorGUILayout.BeginVertical(GUILayout.Height(185f)); + if (itemList != null) + itemList.DoLayoutList(); + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(); + DrawLine(); + + //List Output Settings + GUILayout.Label("Output Settings", EditorStyles.boldLabel); + EditorGUILayout.BeginVertical(new GUIStyle(GUI.skin.GetStyle("Box")), GUILayout.Height(50f)); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Destination", "The folder where generated files will be saved to."), new GUIStyle(GUI.skin.GetStyle("Box")) { alignment = TextAnchor.MiddleLeft, normal = new GUIStyleState() { background = null } }); + GUILayout.FlexibleSpace(); + //Format file path to fit in a button + string displayPath = manager.outputPath.Replace('\\', '/'); + displayPath = (new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, hover = GUI.skin.GetStyle("Button").active }.CalcSize(new GUIContent("" + displayPath + "")).x > 210) ? "..." + displayPath.Substring((displayPath.IndexOf('/', displayPath.Length - 29) != -1) ? displayPath.IndexOf('/', displayPath.Length - 29) : displayPath.Length - 29) : displayPath; + while (new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, hover = GUI.skin.GetStyle("Button").active }.CalcSize(new GUIContent("" + displayPath + "")).x > 210) + { + displayPath = "..." + displayPath.Substring(4); + } + if (GUILayout.Button("" + displayPath + "", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, hover = GUI.skin.GetStyle("Button").active }, GUILayout.MinWidth(210))) + { + string absPath = EditorUtility.OpenFolderPanel("Destination Folder", "", ""); + if (absPath.StartsWith(Application.dataPath)) + { + manager.outputPath = "Assets" + absPath.Substring(Application.dataPath.Length); + } + } + EditorGUILayout.EndHorizontal(); + //Auto-overwrite toggle + GUILayout.BeginHorizontal(); + GUILayout.Label(new GUIContent("Overwrite All", "Automatically overwrite existing files if needed."), GUILayout.Width(145)); + manager.autoOverwrite = Convert.ToBoolean(GUILayout.Toolbar(Convert.ToInt32(manager.autoOverwrite), new string[] { "No", "Yes" })); + GUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(); + DrawLine(); + + //Draw create button + if (GUILayout.Button("Create")) + { + //Convert GUI data into data for the model to use + List allClips = new List(); + List allNames = new List(); + List allPageSizes = new List(); + List allPageNames = new List(); + foreach (Page page in pages) + { + allClips.AddRange(page.GetClips()); + allNames.AddRange(page.GetNames()); + allPageSizes.Add(page.GetClips().Length); + allPageNames.Add(page.name); + } + manager.toggleables = allClips.ToArray(); + manager.aliases = allNames.ToArray(); + manager.pageLength = allPageSizes.ToArray(); + manager.pageNames = allPageNames.ToArray(); + + //Create the inventory + manager.CreateInventory(); + EditorUtility.ClearProgressBar(); + } + EditorGUILayout.EndVertical(); + + //Repaint when hovering over window (needed for directory button) + if (mouseOverWindow != null && mouseOverWindow == this) + { + Repaint(); + focused = true; + } + else if (focused && mouseOverWindow == null) + { + Repaint(); + focused = false; + } + } + + //Draw about window + private void DrawAboutWindow() + { + //Load version number + string version = (AssetDatabase.FindAssets("VERSION", new string[] { manager.relativePath }).Length > 0) ? " " + File.ReadAllText(AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("VERSION", new string[] { manager.relativePath })[0])) : ""; + + //Display top header + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Box("Inventory Inventor" + version + "", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true }, GUILayout.Width(300f)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Box("Author: Joshuarox100", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, normal = new GUIStyleState() { background = null } }); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + EditorGUILayout.Space(); + DrawLine(); + + //Display summary + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Box("Summary", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true }, GUILayout.Width(200f)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Box("Make inventories fast with Inventory Inventor! With it, you can create inventories with up to 64 synced toggles, all contained within a single Expression Parameter!", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, normal = new GUIStyleState() { background = null } }, GUILayout.Width(350f)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + EditorGUILayout.Space(); + DrawLine(); + + //Display special thanks + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Box("Special Thanks", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true }, GUILayout.Width(200f)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Box("zachgregoire & ValliereMagic\nFor helping with development, giving advice, and improving my coding knowledge overall.", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, normal = new GUIStyleState() { background = null } }, GUILayout.Width(350f)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + EditorGUILayout.Space(); + DrawLine(); + + //Display troubleshooting + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Box("Troubleshooting", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true }, GUILayout.Width(200f)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Box("If you're having issues or want to contact me, you can find more information at the GitHub page linked below!", new GUIStyle(GUI.skin.GetStyle("Box")) { richText = true, normal = new GUIStyleState() { background = null } }, GUILayout.Width(350f)); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + EditorGUILayout.Space(); + DrawLine(false); + + //Display GitHub link + GUILayout.Label("GitHub Repository", new GUIStyle(EditorStyles.boldLabel) { alignment = TextAnchor.UpperCenter }); + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Open in Browser", GUILayout.Width(250))) + { + Application.OpenURL("https://github.com/Joshuarox100/VRC-Inventory-Inventor"); + } + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.FlexibleSpace(); + } + + /* + * These next two functions are literally just code from the Expression Menu for selecting the avatar. + */ + + void SelectAvatarDescriptor() + { + var descriptors = FindObjectsOfType(); + if (descriptors.Length > 0) + { + //Compile list of names + string[] names = new string[descriptors.Length]; + for (int i = 0; i < descriptors.Length; i++) + names[i] = descriptors[i].gameObject.name; + + //Select + var currentIndex = Array.IndexOf(descriptors, manager.avatar); + var nextIndex = EditorGUILayout.Popup(new GUIContent("Active Avatar", "The Avatar you want to create an inventory for."), currentIndex, names); + if (nextIndex < 0) + nextIndex = 0; + if (nextIndex != currentIndex) + SelectAvatarDescriptor(descriptors[nextIndex]); + } + else + SelectAvatarDescriptor(null); + } + void SelectAvatarDescriptor(VRCAvatarDescriptor desc) + { + if (desc == manager.avatar) + return; + + manager.avatar = desc; + } + + //Draws a line across the GUI + private void DrawLine(bool addSpace = true) + { + var rect = EditorGUILayout.BeginHorizontal(); + Handles.color = Color.gray; + Handles.DrawLine(new Vector2(rect.x - 15, rect.y), new Vector2(rect.width + 15, rect.y)); + EditorGUILayout.EndHorizontal(); + if (addSpace) + { + EditorGUILayout.Space(); + } + } + + /* + //Reorderable List Code + */ + + //Draws the list header + private void DrawHeader(Rect rect) + { + GUI.Label(rect, pages[currentPage].name); + } + + //Draws each element + private void DrawElement(Rect rect, int index, bool active, bool focused) + { + ListItem item = pages[currentPage].items[index]; + + item.name = EditorGUI.TextField(new Rect(rect.x, rect.y, rect.width / 2, rect.height), item.name); + item.clip = (AnimationClip)EditorGUI.ObjectField(new Rect(rect.x + (rect.width / 2), rect.y, rect.width / 2, rect.height), item.clip, typeof(AnimationClip), false); + + if (item.name == "") + { + item.name = "Slot " + (index + 1); + } + } + + //Adds a slot when the add button is pressed, if room is available + private void AddItem(ReorderableList list) + { + if (pages[currentPage].items.ToArray().Length < 8) + pages[currentPage].items.Add(new ListItem("Slot " + (pages[currentPage].GetClips().Length + 1))); + list.list = pages[currentPage].items; + list.index = list.list.Count - 1; + } + + //Removes the currently selected item from the list, if at least two are present + private void RemoveItem(ReorderableList list) + { + if (pages[currentPage].items.ToArray().Length > 1) + pages[currentPage].items.RemoveAt(list.index); + list.list = pages[currentPage].items; + if (list.index == list.list.Count) + list.index -= 1; + } +} diff --git a/Editor/Scripts/AV3InventoriesManager.cs b/Editor/Scripts/AV3InventoriesManager.cs deleted file mode 100644 index cd1ec2f..0000000 --- a/Editor/Scripts/AV3InventoriesManager.cs +++ /dev/null @@ -1,817 +0,0 @@ -using BMBLibraries.Classes; -using BMBLibraries.Extensions; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using TMPro; -using UnityEditor; -using UnityEditor.Animations; -using UnityEditor.VersionControl; -using UnityEngine; -using UnityEngine.Networking; -using VRC.SDK3.Avatars.Components; -using VRC.SDK3.Avatars.ScriptableObjects; - -public class AV3InventoriesManager : UnityEngine.Object -{ - public VRCAvatarDescriptor avatar; - public VRCExpressionsMenu menu; - - public List toggleables = new List() { null }; - public List aliases = new List() { "Slot 1" }; - - public string relativePath; - public string outputPath; - public bool autoOverwrite = false; - - private Backup backupManager; - private AssetList generated; - - public AV3InventoriesManager() { } - - public void CreateInventory() - { - try - { - - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - - /* - 0. Check for space in parameters list & check for incompatible Animations. - */ - - if (avatar == null) - { - Debug.LogError("no avatar"); - return; - } - else if (avatar.baseAnimationLayers.Length != 5) - { - Debug.LogError("avatar is not humanoid"); - return; - } - else if (avatar.expressionParameters == null) - { - Debug.LogError("no parameters"); - return; - } - else if (menu == null && avatar.expressionsMenu == null) - { - Debug.LogError("no menu"); - return; - } - - bool toggleExists = false; - bool stateExists = false; - int paramCount = 0; - int present = 0; - foreach (VRCExpressionParameters.Parameter param in avatar.expressionParameters.parameters) - { - if (toggleExists && stateExists) - { - break; - } - switch (param.name) - { - case "Inventory Toggle": - if (param.valueType == VRCExpressionParameters.ValueType.Int) - { - present++; - toggleExists = true; - } - else - { - Debug.LogError("expression parameter exists with wrong type"); - return; - } - break; - case "Inventory State": - if (param.valueType == VRCExpressionParameters.ValueType.Float) - { - present++; - stateExists = true; - } - else - { - Debug.LogError("expression parameter exists with wrong type"); - return; - } - break; - default: - if (param.name != "") - paramCount++; - break; - } - } - - if (16 - paramCount < 2 - present) - { - Debug.LogError("too many parameters"); - return; - } - - foreach (AnimationClip clip in toggleables) - { - if (!CheckCompatibility(clip, false, out Type problem, out string propertyName)) - { - EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: " + clip.name + " cannot be used because it modifies an invalid property type!\n\nInvalid Property Type: " + problem.Name + "\nName: " + propertyName, "Close"); - return; - } - } - - VerifyDestination(); - - backupManager = new Backup(); - generated = new AssetList(); - - /* - 1. Get FX Animator - */ - - AnimatorController animator = (avatar.baseAnimationLayers[4].animatorController != null) ? (AnimatorController)avatar.baseAnimationLayers[4].animatorController : null; - - if (animator == null) - { - switch (CopySDKTemplate(avatar.name + "_FX.controller", "vrc_AvatarV3FaceLayer")) - { - case 1: - EditorUtility.DisplayDialog("Inventory Inventor", "Cancelled.", "Close"); - RevertChanges(); - return; - case 3: - EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: Failed to create one or more files!", "Close"); - RevertChanges(); - return; - } - - animator = (AssetDatabase.FindAssets(avatar.name + "_FX", new string[] { outputPath + Path.DirectorySeparatorChar + "Animators" }).Length != 0) ? (AnimatorController)AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets(avatar.name + "_FX", new string[] { outputPath + Path.DirectorySeparatorChar + "Animators" })[0]), typeof(AnimatorController)) : null; - - if (animator == null) - { - Debug.LogError("failed to find template"); - RevertChanges(); - return; - } - } - else - { - backupManager.AddToBackup(new Asset(AssetDatabase.GetAssetPath(animator))); - } - - /* - 2. Create parameters - */ - - //toggle, state, and one bool for each anim. - AnimatorControllerParameter[] srcParam = animator.parameters; - bool[] existing = new bool[toggleables.ToArray().Length + 2]; - for (int i = 0; i < srcParam.Length; i++) - { - bool flag = true; - foreach (bool exists in existing) - { - if (!exists) - { - flag = false; - } - } - if (flag) - { - break; - } - for (int j = 0; j < toggleables.ToArray().Length; j++) - { - if (srcParam[i].name == "Inventory " + (j + 1)) - { - if (srcParam[i].type == AnimatorControllerParameterType.Bool) - { - existing[j] = true; - break; - } - else - { - Debug.LogError("parameter exists with wrong type"); - RevertChanges(); - return; - } - } - else if (srcParam[i].name == "Inventory Toggle") - { - if (srcParam[i].type == AnimatorControllerParameterType.Int) - { - existing[existing.Length - 2] = true; - break; - } - else - { - Debug.LogError("parameter exists with wrong type"); - RevertChanges(); - return; - } - } - else if (srcParam[i].name == ("Inventory State")) - { - if (srcParam[i].type == AnimatorControllerParameterType.Float) - { - existing[existing.Length - 1] = true; - break; - } - else - { - Debug.LogError("parameter exists with wrong type"); - RevertChanges(); - return; - } - } - } - } - - for (int i = 0; i < existing.Length; i++) - { - if (i < existing.Length - 2) - { - if (!existing[i]) - { - animator.AddParameter("Inventory " + (i + 1), AnimatorControllerParameterType.Bool); - } - } - else if (i == existing.Length - 2) - { - if (!existing[i]) - { - animator.AddParameter("Inventory Toggle", AnimatorControllerParameterType.Int); - } - } - else if (i == existing.Length - 1) - { - if (!existing[i]) - { - animator.AddParameter("Inventory State", AnimatorControllerParameterType.Float); - } - } - } - - AssetDatabase.SaveAssets(); - - /* - 3. Create layers - */ - - CreateRemoteLayer(animator, toggleables.ToArray().Length); - CreateMasterLayer(animator, toggleables.ToArray().Length, out List> activeStates); ; - CreateItemLayers(animator, activeStates); - - animator.SaveController(); - AssetDatabase.SaveAssets(); - - /* - 4. Add expression parameters to the list. - */ - - foreach (VRCExpressionParameters.Parameter param in avatar.expressionParameters.parameters) - { - if (toggleExists && stateExists) - { - break; - } - else if (param.name == "") - { - if (!toggleExists) - { - param.name = "Inventory Toggle"; - param.valueType = VRCExpressionParameters.ValueType.Int; - toggleExists = true; - } - else if (!stateExists) - { - param.name = "Inventory State"; - param.valueType = VRCExpressionParameters.ValueType.Float; - stateExists = true; - } - } - } - - /* - 5. Create Expressions menu for toggles. - */ - - switch (CreateMenus(out VRCExpressionsMenu inventory)) - { - case 1: - EditorUtility.DisplayDialog("Inventory Inventor", "Cancelled.", "Close"); - RevertChanges(); - return; - case 3: - EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: Failed to create one or more files!", "Close"); - RevertChanges(); - return; - } - - if (menu != null && menu.controls.ToArray().Length < 8) - { - bool exists = false; - foreach (VRCExpressionsMenu.Control control in menu.controls) - { - if (control.name == "Inventory" && control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu == null) - { - exists = true; - control.subMenu = inventory; - break; - } - } - if (!exists) - { - menu.controls.Add(new VRCExpressionsMenu.Control() { name = "Inventory", type = VRCExpressionsMenu.Control.ControlType.SubMenu, subMenu = inventory }); - } - } - - /* - 6. Save configuration - */ - - AssetDatabase.SaveAssets(); - return; - } - catch (Exception err) - { - EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: An exception has occured!\nCheck the console for more details.", "Close"); - Debug.LogError(err); - RevertChanges(); - return; - } - } - - private bool CheckCompatibility(AnimationClip clip, bool transformsOnly, out Type problem, out string name) - { - if (clip != null) - { - foreach (var binding in AnimationUtility.GetCurveBindings(clip)) - { - if ((transformsOnly && binding.type != typeof(Transform) && binding.type != typeof(Animator)) || (!transformsOnly && (binding.type == typeof(Transform) || binding.type == typeof(Animator)))) - { - problem = binding.type; - name = binding.propertyName; - return false; - } - } - } - - problem = null; - name = ""; - return true; - } - - private void VerifyDestination() - { - if (!AssetDatabase.IsValidFolder(outputPath)) - { - if (!AssetDatabase.IsValidFolder(relativePath + Path.DirectorySeparatorChar + "Output")) - { - string guid = AssetDatabase.CreateFolder(relativePath, "Output"); - outputPath = AssetDatabase.GUIDToAssetPath(guid); - } - else - { - outputPath = relativePath + Path.DirectorySeparatorChar + "Output"; - } - } - } - - private int CopySDKTemplate(string outFile, string SDKfile) - { - if (!AssetDatabase.IsValidFolder(outputPath + Path.DirectorySeparatorChar + "Animators")) - AssetDatabase.CreateFolder(outputPath, "Animators"); - bool existed = true; - if (File.Exists(outputPath + Path.DirectorySeparatorChar + "Animators" + Path.DirectorySeparatorChar + outFile)) - { - if (!autoOverwrite) - { - switch (EditorUtility.DisplayDialogComplex("Inventory Inventor", outFile + " already exists!\nOverwrite the file?", "Overwrite", "Cancel", "Skip")) - { - case 1: - return 1; - case 2: - return 2; - } - } - backupManager.AddToBackup(new Asset(outputPath + Path.DirectorySeparatorChar + "Animators" + Path.DirectorySeparatorChar + outFile)); - } - else - { - existed = false; - } - if (!AssetDatabase.CopyAsset(AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets(SDKfile, new string[] { "Assets" + Path.DirectorySeparatorChar + "VRCSDK" + Path.DirectorySeparatorChar + "Examples3" + Path.DirectorySeparatorChar + "Animation" + Path.DirectorySeparatorChar + "Controllers" })[0]), outputPath + Path.DirectorySeparatorChar + "Animators" + Path.DirectorySeparatorChar + outFile)) - { - return 3; - } - else - { - AssetDatabase.Refresh(); - if (!existed) - generated.Add(new Asset(outputPath + Path.DirectorySeparatorChar + "Animators" + Path.DirectorySeparatorChar + outFile)); - } - return 0; - } - - private int CreateMenus(out VRCExpressionsMenu mainMenu) - { - mainMenu = null; - int totalMenus = (toggleables.ToArray().Length / 8) + 1; - VRCExpressionsMenu inventory = ScriptableObject.CreateInstance(); - inventory.name = avatar.name + "_Inventory"; - if (totalMenus == 1) - { - for (int i = 0; i < toggleables.ToArray().Length; i++) - { - inventory.controls.Add(new VRCExpressionsMenu.Control() { name = (aliases[i] != "") ? aliases[i] : "Slot " + (i + 1), type = VRCExpressionsMenu.Control.ControlType.Toggle, parameter = new VRCExpressionsMenu.Control.Parameter() { name = "Inventory Toggle" }, value = i + 1 }); - } - if (!AssetDatabase.IsValidFolder(outputPath + Path.DirectorySeparatorChar + "Menus")) - AssetDatabase.CreateFolder(outputPath, "Menus"); - bool existed = true; - if (File.Exists(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")) - { - if (!autoOverwrite) - { - switch (EditorUtility.DisplayDialogComplex("Inventory Inventor", inventory.name + ".asset" + " already exists!\nOverwrite the file?", "Overwrite", "Cancel", "Skip")) - { - case 1: - return 1; - case 2: - return 2; - } - } - backupManager.AddToBackup(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")); - } - else - { - existed = false; - } - AssetDatabase.CreateAsset(inventory, outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset"); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - if (AssetDatabase.FindAssets(avatar.name + "_Inventory", new string[] { outputPath + Path.DirectorySeparatorChar + "Menus" }).Length == 0) - { - return 3; - } - else - { - AssetDatabase.Refresh(); - if (!existed) - generated.Add(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")); - } - mainMenu = inventory; - return 0; - } - else - { - List pages = new List(); - for(int i = 0; i < totalMenus && i < 3; i++) - { - pages.Add(ScriptableObject.CreateInstance()); - pages[pages.ToArray().Length - 1].name = "Page " + (i + 1); - inventory.controls.Add(new VRCExpressionsMenu.Control() { name = pages[pages.ToArray().Length - 1].name, type = VRCExpressionsMenu.Control.ControlType.SubMenu, subMenu = pages[pages.ToArray().Length - 1] }); - } - for (int i = 0; i < toggleables.ToArray().Length; i++) - { - pages[i / 8].controls.Add(new VRCExpressionsMenu.Control() { name = (aliases[i] != "") ? aliases[i] : "Slot " + (i + 1), type = VRCExpressionsMenu.Control.ControlType.Toggle, parameter = new VRCExpressionsMenu.Control.Parameter() { name = "Inventory Toggle" }, value = i + 1 }); - } - if (!AssetDatabase.IsValidFolder(outputPath + Path.DirectorySeparatorChar + "Menus")) - AssetDatabase.CreateFolder(outputPath, "Menus"); - foreach (VRCExpressionsMenu page in pages) - { - bool exists = true; - if (File.Exists(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + avatar.name + "_" + page.name + ".asset")) - { - if (!autoOverwrite) - { - switch (EditorUtility.DisplayDialogComplex("Inventory Inventor", avatar.name + "_" + page.name + ".asset" + " already exists!\nOverwrite the file?", "Overwrite", "Cancel", "Skip")) - { - case 1: - return 1; - case 2: - return 2; - } - } - backupManager.AddToBackup(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + avatar.name + "_" + page.name + ".asset")); - } - else - { - exists = false; - } - AssetDatabase.CreateAsset(inventory, outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + avatar.name + "_" + page.name + ".asset"); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - if (AssetDatabase.FindAssets(avatar.name + "_" + page.name, new string[] { outputPath + Path.DirectorySeparatorChar + "Menus" }).Length == 0) - { - return 3; - } - else - { - AssetDatabase.Refresh(); - if (!exists) - generated.Add(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + avatar.name + "_" + page.name + ".asset")); - } - } - bool existed = true; - if (File.Exists(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")) - { - if (!autoOverwrite) - { - switch (EditorUtility.DisplayDialogComplex("Inventory Inventor", inventory.name + ".asset" + " already exists!\nOverwrite the file?", "Overwrite", "Cancel", "Skip")) - { - case 1: - return 1; - case 2: - return 2; - } - } - backupManager.AddToBackup(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")); - } - else - { - existed = false; - } - AssetDatabase.CreateAsset(inventory, outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset"); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - if (AssetDatabase.FindAssets(avatar.name + "_Inventory", new string[] { outputPath + Path.DirectorySeparatorChar + "Menus" }).Length == 0) - { - return 3; - } - else - { - AssetDatabase.Refresh(); - if (!existed) - generated.Add(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")); - } - mainMenu = inventory; - return 0; - } - } - - private void CreateItemLayers(AnimatorController source, List> activeStates) - { - for (int i = 0; i < toggleables.ToArray().Length; i++) - { - double[] active = activeStates[i].Value; - for (int j = 0; j < source.layers.Length; j++) - { - if (source.layers[j].name == "Inventory" + (j + 1)) - { - source.RemoveLayer(j); - break; - } - } - source.AddLayer("Inventory " + (i + 1)); - AnimatorControllerLayer currentLayer = source.layers[source.layers.Length - 1]; - currentLayer.defaultWeight = 1; - - AnimatorState offState = currentLayer.stateMachine.AddState("Off"); - currentLayer.stateMachine.defaultState = offState; - AnimatorStateTransition offTransition = currentLayer.stateMachine.AddAnyStateTransition(offState); - offTransition.canTransitionToSelf = false; - offTransition.hasExitTime = false; - offTransition.duration = 0; - foreach (float state in active) - { - offTransition.AddCondition(AnimatorConditionMode.Less, state - 0.0000001f, "Inventory State"); - offTransition.AddCondition(AnimatorConditionMode.Greater, state + 0.0000001f, "Inventory State"); - } - - AnimatorState onState = currentLayer.stateMachine.AddState("On"); - onState.motion = toggleables[i]; - foreach(float state in active) - { - AnimatorStateTransition transition = currentLayer.stateMachine.AddAnyStateTransition(onState); - transition.canTransitionToSelf = false; - transition.hasExitTime = false; - transition.duration = 0; - transition.AddCondition(AnimatorConditionMode.Greater, state - 0.0000001f, "Inventory State"); - transition.AddCondition(AnimatorConditionMode.Less, state + 0.0000001f, "Inventory State"); - } - AnimatorControllerLayer[] layers = source.layers; - layers[layers.Length - 1] = currentLayer; - source.layers = layers; - } - return; - } - - private void CreateRemoteLayer(AnimatorController source, int itemTotal) - { - for (int i = 0; i < source.layers.Length; i++) - { - if (source.layers[i].name == "Inventory Remote") - { - source.RemoveLayer(i); - break; - } - } - source.AddLayer("Inventory Remote"); - AnimatorControllerLayer masterLayer = source.layers[source.layers.Length - 1]; - AnimatorState waitState = masterLayer.stateMachine.AddState("Wait"); - masterLayer.stateMachine.defaultState = waitState; - AnimatorState resetState = masterLayer.stateMachine.AddState("Reset"); - VRCAvatarParameterDriver resetDriver = resetState.AddStateMachineBehaviour(); - resetDriver.parameters.Add(new VRC.SDKBase.VRC_AvatarParameterDriver.Parameter() { name = "Inventory Toggle", value = 0 }); - AnimatorStateTransition exitTransition = resetState.AddExitTransition(); - exitTransition.duration = 0; - exitTransition.hasExitTime = false; - exitTransition.AddCondition(AnimatorConditionMode.Equals, 0, "Inventory Toggle"); - - for (int i = 0; i < itemTotal; i++) - { - AnimatorState stateOn = masterLayer.stateMachine.AddState((i + 1) + " On"); - AnimatorStateTransition transitionOnIn= waitState.AddTransition(stateOn); - transitionOnIn.hasExitTime = false; - transitionOnIn.duration = 0; - transitionOnIn.AddCondition(AnimatorConditionMode.IfNot, 0, "Inventory " + (i + 1)); - transitionOnIn.AddCondition(AnimatorConditionMode.Equals, (i + 1), "Inventory Toggle"); - AnimatorStateTransition transitionOnOut = stateOn.AddTransition(resetState); - transitionOnOut.hasExitTime = false; - transitionOnOut.duration = 0; - transitionOnOut.AddCondition(AnimatorConditionMode.If, 0, "Inventory " + (i + 1)); - VRCAvatarParameterDriver driverOn = stateOn.AddStateMachineBehaviour(); - driverOn.parameters.Add(new VRC.SDKBase.VRC_AvatarParameterDriver.Parameter() { name = "Inventory " + (i + 1), value = 1 }); - - AnimatorState stateOff = masterLayer.stateMachine.AddState((i + 1) + " Off"); - AnimatorStateTransition transitionOffIn = waitState.AddTransition(stateOff); - transitionOffIn.hasExitTime = false; - transitionOffIn.duration = 0; - transitionOffIn.AddCondition(AnimatorConditionMode.If, 0, "Inventory " + (i + 1)); - transitionOffIn.AddCondition(AnimatorConditionMode.Equals, (i + 1), "Inventory Toggle"); - AnimatorStateTransition transitionOffOut = stateOff.AddTransition(resetState); - transitionOffOut.hasExitTime = false; - transitionOffOut.duration = 0; - transitionOffOut.AddCondition(AnimatorConditionMode.IfNot, 0, "Inventory " + (i + 1)); - VRCAvatarParameterDriver driverOff = stateOff.AddStateMachineBehaviour(); - driverOff.parameters.Add(new VRC.SDKBase.VRC_AvatarParameterDriver.Parameter() { name = "Inventory " + (i + 1), value = 0 }); - } - - AnimatorControllerLayer[] layers = source.layers; - layers[layers.Length - 1] = masterLayer; - source.layers = layers; - return; - } - - private void CreateMasterLayer(AnimatorController source, int itemTotal, out List> activeStates) - { - for (int i = 0; i < source.layers.Length; i++) - { - if (source.layers[i].name == "Inventory Master") - { - source.RemoveLayer(i); - break; - } - } - source.AddLayer("Inventory Master"); - AnimatorControllerLayer masterLayer = source.layers[source.layers.Length - 1]; - activeStates = new List>(); - - List temp = new List(); - for (int i = 0; i < itemTotal; i++) - { - temp.Add(i); - activeStates.Add(new KeyValuePair(i, new double[0])); - } - int[][] tempArray = ExtraMath.GetPowerSet(temp).Select(subset => subset.ToArray()).ToArray(); - //convert to bool array - bool[][] itemArray = new bool[tempArray.Length][]; - for (int i = 0; i < itemArray.Length; i++) - { - itemArray[i] = new bool[itemTotal]; - for (int j = 0; j < itemTotal; j++) - { - if (tempArray[i].Contains(j)) - { - itemArray[i][j] = true; - double[] active = activeStates[j].Value; - double[] newActive = new double[active.Length + 1]; - active.CopyTo(newActive, 0); - newActive[active.Length] = (Convert.ToDouble(i) % 20000000 / 10000000d) - 1; - activeStates[j] = new KeyValuePair(j, newActive); - } - } - } - - for (int i = 0; i < itemArray.Length; i++) - { - AnimatorState state = masterLayer.stateMachine.AddState(i.ToString()); - AnimatorStateTransition transition = masterLayer.stateMachine.AddAnyStateTransition(state); - transition.hasExitTime = false; - transition.duration = 0; - transition.canTransitionToSelf = false; - for (int j = 0; j < itemArray[0].Length; j++) - { - switch (itemArray[i][j]) - { - case true: - transition.AddCondition(AnimatorConditionMode.If, 0, "Inventory " + (j + 1)); - break; - case false: - transition.AddCondition(AnimatorConditionMode.IfNot, 0, "Inventory " + (j + 1)); - break; - } - } - VRCAvatarParameterDriver driver = state.AddStateMachineBehaviour(); - driver.parameters.Add(new VRC.SDKBase.VRC_AvatarParameterDriver.Parameter() { name = "Inventory State", value = float.Parse(((i % 20000000 / 10000000d) - 1).ToString()) }); - if (i == 0) - { - masterLayer.stateMachine.defaultState = state; - } - } - - AnimatorControllerLayer[] layers = source.layers; - layers[layers.Length - 1] = masterLayer; - source.layers = layers; - return; - } - - private void RevertChanges() - { - AssetDatabase.SaveAssets(); - if (backupManager != null && !backupManager.RestoreAssets()) - Debug.LogError("[Inventory Inventor] Failed to revert all changes."); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - for (int i = 0; generated != null && i < generated.ToArray().Length; i++) - if (!AssetDatabase.DeleteAsset(generated[i].path)) - Debug.LogError("[Inventory Inventor] Failed to revert all changes."); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - if (AssetDatabase.IsValidFolder(outputPath + Path.DirectorySeparatorChar + "Animators") && AssetDatabase.FindAssets("", new string[] { outputPath + Path.DirectorySeparatorChar + "Animators" }).Length == 0) - if (!AssetDatabase.DeleteAsset(outputPath + Path.DirectorySeparatorChar + "Animators")) - Debug.LogError("[Inventory Inventor] Failed to revert all changes."); - if (AssetDatabase.IsValidFolder(outputPath + Path.DirectorySeparatorChar + "Menus") && AssetDatabase.FindAssets("", new string[] { outputPath + Path.DirectorySeparatorChar + "Menus" }).Length == 0) - if (!AssetDatabase.DeleteAsset(outputPath + Path.DirectorySeparatorChar + "Menus")) - Debug.LogError("[Inventory Inventor] Failed to revert all changes."); - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - } - - public void UpdatePaths() - { - string old = relativePath; - relativePath = AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("AV3InventoriesWindow")[0]).Substring(0, AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("AV3InventoriesWindow")[0]).LastIndexOf("Editor") - 1); - if (relativePath == old) - return; - else if (outputPath == null || !AssetDatabase.IsValidFolder(outputPath)) - { - outputPath = relativePath + Path.DirectorySeparatorChar + "Output"; - } - } - - private class NetworkManager : MonoBehaviour { } - - public static void CheckForUpdates() - { - string relativePath = AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("AV3InventoriesWindow")[0]).Substring(0, AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("AV3InventoriesWindow")[0]).LastIndexOf("Editor") - 1); - string installedVersion = (AssetDatabase.FindAssets("VERSION", new string[] { relativePath }).Length > 0) ? File.ReadAllText(AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("VERSION", new string[] { relativePath })[0])) : ""; - - GameObject netMan = new GameObject { hideFlags = HideFlags.HideInHierarchy }; - netMan.AddComponent().StartCoroutine(GetText("https://raw.githubusercontent.com/Joshuarox100/VRC-AV3-Overrides/master/Editor/VERSION", latestVersion => { - if (latestVersion == "") - { - EditorUtility.DisplayDialog("AV3 Inventories", "Failed to fetch the latest version.\n(Check console for details.)", "Close"); - } - else if (installedVersion == "") - { - EditorUtility.DisplayDialog("AV3 Inventories", "Failed to identify installed version.\n(VERSION file was not found.)", "Close"); - } - else if (latestVersion == "RIP") - { - EditorUtility.DisplayDialog("AV3 Inventories", "Project has been put on hold indefinitely.", "Close"); - } - else if (installedVersion != latestVersion) - { - if (EditorUtility.DisplayDialog("AV3 Inventories", "A new update is available! (" + latestVersion + ")\nOpen the Releases page?", "Yes", "No")) - { - Application.OpenURL("https://github.com/Joshuarox100/VRC-AV3-Overrides/releases"); - } - } - else - { - EditorUtility.DisplayDialog("AV3 Inventories", "You are using the latest version.", "Close"); - } - DestroyImmediate(netMan); - })); - } - - private static IEnumerator GetText(string url, Action result) - { - UnityWebRequest www = UnityWebRequest.Get(url); - yield return www.SendWebRequest(); - - if (www.isNetworkError || www.isHttpError) - { - Debug.LogError(www.error); - result?.Invoke(""); - } - else - { - result?.Invoke(www.downloadHandler.text); - } - } -} diff --git a/Editor/Scripts/InventoryInventorManager.cs b/Editor/Scripts/InventoryInventorManager.cs new file mode 100644 index 0000000..35d367e --- /dev/null +++ b/Editor/Scripts/InventoryInventorManager.cs @@ -0,0 +1,1169 @@ +using BMBLibraries.Classes; +using BMBLibraries.Extensions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using UnityEditor; +using UnityEditor.Animations; +using UnityEditor.VersionControl; +using UnityEngine; +using UnityEngine.Networking; +using VRC.SDK3.Avatars.Components; +using VRC.SDK3.Avatars.ScriptableObjects; + +public class InventoryInventorManager : UnityEngine.Object +{ + //Objects to modify + public VRCAvatarDescriptor avatar; + public VRCExpressionsMenu menu; + public AnimatorController controller; + + //Input objects + public AnimationClip[] toggleables = new AnimationClip[0]; + public string[] aliases = new string[0]; + public int[] pageLength = new int[0]; + public string[] pageNames = new string[0]; + public int syncMode = 1; + public float refreshRate = 0.05f; + + //Path related + public string relativePath; + public string outputPath; + public bool autoOverwrite = false; + + //File backup + private Backup backupManager; + private AssetList generated; + + public InventoryInventorManager() { } + + public void CreateInventory() + { + try + { + //Initial Save + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + //Check that the selected avatar is valid + if (avatar == null) + { + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: No Avatar selected.", "Close"); + return; + } + else if (avatar.baseAnimationLayers.Length != 5) + { + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: Avatar is not humanoid.\n(Non-Humanoid avatars are not supported yet)", "Close"); + Selection.activeObject = avatar.gameObject; + return; + } + else if (avatar.expressionParameters == null) + { + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: Avatar does not have an Expression Parameters object assigned in the descriptor.", "Close"); + Selection.activeObject = avatar; + return; + } + + /* + Check for space in parameters list & check for incompatible Animations. + */ + + int paramCount = 0; + int present = 0; + //foreach parameter, check if it's one to be added and if it is the correct type. + foreach (VRCExpressionParameters.Parameter param in avatar.expressionParameters.parameters) + { + if (present == 1) + { + break; + } + switch (param.name) + { + case "Inventory": + if (param.valueType == VRCExpressionParameters.ValueType.Int) + { + present++; + } + else + { + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: Expression Parameter \"" + param.name + "\" is present with the wrong type.", "Close"); + Selection.activeObject = avatar.expressionParameters; + return; + } + break; + default: + if (param.name != "") + paramCount++; + break; + } + } + + if (16 - paramCount < 1 - present) + { + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: No unused Expression Parameters found.", "Close"); + Selection.activeObject = avatar.expressionParameters; + return; + } + + //Check that no animations modify a rig or Transform + foreach (AnimationClip clip in toggleables) + { + if (!CheckCompatibility(clip, false, out Type problem, out string propertyName)) + { + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: " + clip.name + " cannot be used because it modifies an invalid property type!\n\nInvalid Property Type: " + problem.Name + "\nName: " + propertyName, "Close"); + Selection.activeObject = clip; + return; + } + } + + //Check that the file destination exists + VerifyDestination(); + + EditorUtility.DisplayProgressBar("Inventory Inventor", "Starting", 0); + + //Initialize backup objects + backupManager = new Backup(); + generated = new AssetList(); + + /* + Get FX Animator + */ + + AnimatorController animator = (avatar.baseAnimationLayers[4].animatorController != null) ? (AnimatorController)avatar.baseAnimationLayers[4].animatorController : null; + bool replaceAnimator = animator != null; + animator = controller != null ? controller : animator; + + //Create new Animator from SDK template if none provided. + if (animator == null) + { + switch (CopySDKTemplate(avatar.name + "_FX.controller", "vrc_AvatarV3FaceLayer")) + { + case 1: + EditorUtility.DisplayDialog("Inventory Inventor", "Cancelled.", "Close"); + RevertChanges(); + return; + case 3: + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: Failed to create one or more files!", "Close"); + RevertChanges(); + return; + } + + animator = (AssetDatabase.FindAssets(avatar.name + "_FX", new string[] { outputPath + Path.DirectorySeparatorChar + "Animators" }).Length != 0) ? (AnimatorController)AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets(avatar.name + "_FX", new string[] { outputPath + Path.DirectorySeparatorChar + "Animators" })[0]), typeof(AnimatorController)) : null; + + if (animator == null) + { + EditorUtility.DisplayDialog("Inventory Inventor", "Failed to copy template Animator from VRCSDK.", "Close"); + RevertChanges(); + return; + } + } + else + { + backupManager.AddToBackup(new Asset(AssetDatabase.GetAssetPath(animator))); + } + + //Create fresh and clean Animator object + AnimatorController newAnimator = new AnimatorController + { + name = animator.name, + parameters = animator.parameters, + hideFlags = animator.hideFlags + }; + AssetDatabase.CreateAsset(newAnimator, relativePath + Path.DirectorySeparatorChar + "temp.controller"); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + generated.Add(new Asset(AssetDatabase.GetAssetPath(newAnimator))); + + //Clone provided Animator into the new object, without any Inventory layers or parameters. + for (int i = 0; i < animator.layers.Length; i++) + { + bool invLayer = false; + for (int j = 0; j < 12; j++) + { + if (animator.layers[i].stateMachine.name == "Inventory " + (j + 1)) + { + invLayer = true; + break; + } + } + if (!invLayer && animator.layers[i].stateMachine.name != "Inventory Master") + { + EditorUtility.DisplayProgressBar("Inventory Inventor", string.Format("Cloning Layers: {0}", animator.layers[i].name), 0.05f * (float.Parse(i.ToString()) / animator.layers.Length)); + newAnimator.AddLayer(animator.layers[i].name); + AnimatorControllerLayer[] layers = newAnimator.layers; + AnimatorControllerLayer layer = layers[layers.Length - 1]; + layer = animator.layers[i].DeepClone(); + layers[layers.Length - 1] = layer; + newAnimator.layers = layers; + EditorUtility.DisplayProgressBar("Inventory Inventor", string.Format("Cloning Layers: {0}", animator.layers[i].name), 0.05f * ((i + 1f) / animator.layers.Length)); + } + } + newAnimator.SaveController(); + string path = AssetDatabase.GetAssetPath(animator); + AssetDatabase.DeleteAsset(path); + AssetDatabase.MoveAsset(AssetDatabase.GetAssetPath(newAnimator), path); + AssetDatabase.RenameAsset(AssetDatabase.GetAssetPath(newAnimator), path.Substring(path.LastIndexOf(Path.DirectorySeparatorChar) + 1)); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + if (replaceAnimator) + avatar.baseAnimationLayers[4].animatorController = newAnimator; + + /* + Create parameters + */ + + EditorUtility.DisplayProgressBar("Inventory Inventor", "Creating Parameters", 0.05f); + + //Adds needed parameters to the Animator If one already exists as the wrong type, abort. + //toggle, state, and one bool for each anim. + AnimatorControllerParameter[] srcParam = newAnimator.parameters; + bool[] existing = new bool[toggleables.Length + 2]; + for (int i = 0; i < srcParam.Length; i++) + { + EditorUtility.DisplayProgressBar("Inventory Inventor", "Creating Parameters", 0.05f + (0.025f * (float.Parse(i.ToString()) / srcParam.Length))); + bool flag = true; + foreach (bool exists in existing) + { + if (!exists) + { + flag = false; + } + } + if (flag) + { + break; + } + for (int j = 0; j < toggleables.Length; j++) + { + if (srcParam[i].name == "Inventory " + (j + 1)) + { + if (srcParam[i].type == AnimatorControllerParameterType.Bool) + { + existing[j] = true; + break; + } + else + { + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: Animator Parameter \"" + srcParam[i].name + "\" already exists as the incorrect type.", "Close"); + RevertChanges(); + Selection.activeObject = animator; + return; + } + } + else if (srcParam[i].name == "Inventory") + { + if (srcParam[i].type == AnimatorControllerParameterType.Int) + { + existing[existing.Length - 2] = true; + break; + } + else + { + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: Animator Parameter \"" + srcParam[i].name + "\" already exists as the incorrect type.", "Close"); + RevertChanges(); + Selection.activeObject = animator; + return; + } + } + else if (srcParam[i].name == "IsLocal") + { + if (srcParam[i].type == AnimatorControllerParameterType.Bool) + { + existing[existing.Length - 1] = true; + break; + } + else + { + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: Animator Parameter \"" + srcParam[i].name + "\" already exists as the incorrect type.", "Close"); + RevertChanges(); + Selection.activeObject = animator; + return; + } + } + } + } + + for (int i = 0; i < existing.Length; i++) + { + EditorUtility.DisplayProgressBar("Inventory Inventor", "Creating Parameters", 0.075f + (0.025f * (float.Parse(i.ToString()) / existing.Length))); + if (i < existing.Length - 2) + { + if (!existing[i]) + { + newAnimator.AddParameter("Inventory " + (i + 1), AnimatorControllerParameterType.Bool); + } + } + else if (i == existing.Length - 2) + { + if (!existing[i]) + { + newAnimator.AddParameter("Inventory", AnimatorControllerParameterType.Int); + } + } + else if (i == existing.Length - 1) + { + if (!existing[i]) + { + newAnimator.AddParameter("IsLocal", AnimatorControllerParameterType.Bool); + } + } + } + + AssetDatabase.SaveAssets(); + EditorUtility.DisplayProgressBar("Inventory Inventor", "Creating Parameters", 0.1f); + + /* + Create layers + */ + + CreateMasterLayer(newAnimator, toggleables.Length, out List activeStates); + CreateItemLayers(newAnimator, ref activeStates); + + EditorUtility.DisplayProgressBar("Inventory Inventor", "Saving Controller", 0.9f); + newAnimator.SaveController(); + AssetDatabase.SaveAssets(); + + /* + Add expression parameters to the list. + */ + + EditorUtility.DisplayProgressBar("Inventory Inventor", "Finalizing", 0.9f); + foreach (VRCExpressionParameters.Parameter param in avatar.expressionParameters.parameters) + { + if (present == 1) + { + break; + } + else if (param.name == "") + { + param.name = "Inventory"; + param.valueType = VRCExpressionParameters.ValueType.Int; + break; + } + } + EditorUtility.DisplayProgressBar("Inventory Inventor", "Finalizing", 0.95f); + + /* + Create Expressions menu for toggles. + */ + + + switch (CreateMenus(out VRCExpressionsMenu inventory)) + { + case 1: + EditorUtility.DisplayDialog("Inventory Inventor", "Cancelled.", "Close"); + RevertChanges(); + return; + case 3: + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: Failed to create one or more files!", "Close"); + RevertChanges(); + return; + } + + //Assign inventory menu to given menu if possible. + if (menu != null && menu.controls.ToArray().Length < 8) + { + bool exists = false; + foreach (VRCExpressionsMenu.Control control in menu.controls) + { + if (control.name == "Inventory" && control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu == null) + { + exists = true; + control.subMenu = inventory; + break; + } + } + if (!exists) + { + menu.controls.Add(new VRCExpressionsMenu.Control() { name = "Inventory", type = VRCExpressionsMenu.Control.ControlType.SubMenu, subMenu = inventory }); + } + } + else if (menu != null && menu.controls.ToArray().Length >= 8) + { + EditorUtility.DisplayDialog("Inventory Inventory", "WARNING: Inventory menu not added to provided menu.\n(No space available in given menu.)", "Close"); + } + + EditorUtility.DisplayProgressBar("Inventory Inventor", "Finalizing", 1f); + + /* + 6. Save configuration + */ + + AssetDatabase.SaveAssets(); + EditorUtility.DisplayDialog("Inventory Inventory", "Success!", "Close"); + Selection.activeObject = menu != null ? menu : inventory; + return; + } + catch (Exception err) + { + EditorUtility.DisplayDialog("Inventory Inventor", "ERROR: An exception has occured!\nCheck the console for more details.", "Close"); + Debug.LogError(err); + RevertChanges(); + return; + } + } + + //Checks if an AnimationClip contains invalid bindings. + private bool CheckCompatibility(AnimationClip clip, bool transformsOnly, out Type problem, out string name) + { + if (clip != null) + { + foreach (var binding in AnimationUtility.GetCurveBindings(clip)) + { + if ((transformsOnly && binding.type != typeof(Transform) && binding.type != typeof(Animator)) || (!transformsOnly && (binding.type == typeof(Transform) || binding.type == typeof(Animator)))) + { + problem = binding.type; + name = binding.propertyName; + return false; + } + } + } + + problem = null; + name = ""; + return true; + } + + //Checks if the destination is valid. + private void VerifyDestination() + { + if (!AssetDatabase.IsValidFolder(outputPath)) + { + if (!AssetDatabase.IsValidFolder(relativePath + Path.DirectorySeparatorChar + "Output")) + { + string guid = AssetDatabase.CreateFolder(relativePath, "Output"); + outputPath = AssetDatabase.GUIDToAssetPath(guid); + } + else + { + outputPath = relativePath + Path.DirectorySeparatorChar + "Output"; + } + } + } + + //Copies an Animator from the VRCSDK to the given location. + private int CopySDKTemplate(string outFile, string SDKfile) + { + if (!AssetDatabase.IsValidFolder(outputPath + Path.DirectorySeparatorChar + "Animators")) + AssetDatabase.CreateFolder(outputPath, "Animators"); + bool existed = true; + if (File.Exists(outputPath + Path.DirectorySeparatorChar + "Animators" + Path.DirectorySeparatorChar + outFile)) + { + if (!autoOverwrite) + { + switch (EditorUtility.DisplayDialogComplex("Inventory Inventor", outFile + " already exists!\nOverwrite the file?", "Overwrite", "Cancel", "Skip")) + { + case 1: + return 1; + case 2: + return 2; + } + } + backupManager.AddToBackup(new Asset(outputPath + Path.DirectorySeparatorChar + "Animators" + Path.DirectorySeparatorChar + outFile)); + } + else + { + existed = false; + } + if (!AssetDatabase.CopyAsset(AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets(SDKfile, new string[] { "Assets" + Path.DirectorySeparatorChar + "VRCSDK" + Path.DirectorySeparatorChar + "Examples3" + Path.DirectorySeparatorChar + "Animation" + Path.DirectorySeparatorChar + "Controllers" })[0]), outputPath + Path.DirectorySeparatorChar + "Animators" + Path.DirectorySeparatorChar + outFile)) + { + return 3; + } + else + { + AssetDatabase.Refresh(); + if (!existed) + generated.Add(new Asset(outputPath + Path.DirectorySeparatorChar + "Animators" + Path.DirectorySeparatorChar + outFile)); + } + return 0; + } + + //Creates all the menus needed for the generated inventory. + private int CreateMenus(out VRCExpressionsMenu mainMenu) + { + mainMenu = null; + + //Create a main menu + VRCExpressionsMenu inventory = ScriptableObject.CreateInstance(); + inventory.name = avatar.name + "_Inventory"; + + //If there's a single page, put all the controls on the top level + if (pageLength.Length == 1) + { + //For each item in the page, add it to the menu + for (int i = 0; i < toggleables.Length; i++) + { + inventory.controls.Add(new VRCExpressionsMenu.Control() { name = aliases[i], type = VRCExpressionsMenu.Control.ControlType.Toggle, parameter = new VRCExpressionsMenu.Control.Parameter() { name = "Inventory" }, value = i + 1 }); + } + + //Create output folder if not present + if (!AssetDatabase.IsValidFolder(outputPath + Path.DirectorySeparatorChar + "Menus")) + AssetDatabase.CreateFolder(outputPath, "Menus"); + + //Create or overwrite the menu asset + bool existed = true; + if (File.Exists(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")) + { + if (!autoOverwrite) + { + switch (EditorUtility.DisplayDialogComplex("Inventory Inventor", inventory.name + ".asset" + " already exists!\nOverwrite the file?", "Overwrite", "Cancel", "Skip")) + { + case 1: + return 1; + case 2: + return 2; + } + } + backupManager.AddToBackup(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")); + AssetDatabase.DeleteAsset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset"); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + else + { + existed = false; + } + AssetDatabase.CreateAsset(inventory, outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset"); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + //Check that it was created successfully + if (AssetDatabase.FindAssets(inventory.name, new string[] { outputPath + Path.DirectorySeparatorChar + "Menus" }).Length == 0) + { + return 3; + } + else + { + AssetDatabase.Refresh(); + if (!existed) + generated.Add(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")); + } + + //out the main menu + mainMenu = inventory; + return 0; + } + //If there is more than one page... + else + { + //Create a list of menu objects and instantiate a new one for each page + List pages = new List(); + int index = 0; + for (int i = 0; i < pageLength.Length; i++) + { + //Add controls for the items contained in each page + pages.Add(ScriptableObject.CreateInstance()); + pages[i].name = pageNames[i]; + inventory.controls.Add(new VRCExpressionsMenu.Control() { name = pages[i].name, type = VRCExpressionsMenu.Control.ControlType.SubMenu, subMenu = pages[i] }); + for (int j = 0; j < pageLength[i]; j++) + { + pages[i].controls.Add(new VRCExpressionsMenu.Control() { name = aliases[index], type = VRCExpressionsMenu.Control.ControlType.Toggle, parameter = new VRCExpressionsMenu.Control.Parameter() { name = "Inventory" }, value = index + 1 }); + index++; + } + } + + //Create output directory if not present + if (!AssetDatabase.IsValidFolder(outputPath + Path.DirectorySeparatorChar + "Menus")) + AssetDatabase.CreateFolder(outputPath, "Menus"); + + //Create / overwrite each menu asset to the directory + foreach (VRCExpressionsMenu page in pages) + { + bool exists = true; + if (File.Exists(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + avatar.name + "_" + page.name + ".asset")) + { + if (!autoOverwrite) + { + switch (EditorUtility.DisplayDialogComplex("Inventory Inventor", avatar.name + "_" + page.name + ".asset" + " already exists!\nOverwrite the file?", "Overwrite", "Cancel", "Skip")) + { + case 1: + return 1; + case 2: + return 2; + } + } + backupManager.AddToBackup(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + avatar.name + "_" + page.name + ".asset")); + AssetDatabase.DeleteAsset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + avatar.name + "_" + page.name + ".asset"); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + else + { + exists = false; + } + AssetDatabase.CreateAsset(page, outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + avatar.name + "_" + page.name + ".asset"); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + //Check that the asset was saved successfully + if (AssetDatabase.FindAssets(page.name, new string[] { outputPath + Path.DirectorySeparatorChar + "Menus" }).Length == 0) + { + return 3; + } + else + { + AssetDatabase.Refresh(); + if (!exists) + generated.Add(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + page.name + ".asset")); + } + } + + //Create / overwrite the main menu asset + bool existed = true; + if (File.Exists(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")) + { + if (!autoOverwrite) + { + switch (EditorUtility.DisplayDialogComplex("Inventory Inventor", inventory.name + ".asset" + " already exists!\nOverwrite the file?", "Overwrite", "Cancel", "Skip")) + { + case 1: + return 1; + case 2: + return 2; + } + } + backupManager.AddToBackup(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")); + AssetDatabase.DeleteAsset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset"); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + else + { + existed = false; + } + AssetDatabase.CreateAsset(inventory, outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset"); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + //Check that the asset was saved successfully + if (AssetDatabase.FindAssets(inventory.name, new string[] { outputPath + Path.DirectorySeparatorChar + "Menus" }).Length == 0) + { + return 3; + } + else + { + AssetDatabase.Refresh(); + if (!existed) + generated.Add(new Asset(outputPath + Path.DirectorySeparatorChar + "Menus" + Path.DirectorySeparatorChar + inventory.name + ".asset")); + } + + //out the main menu + mainMenu = inventory; + return 0; + } + } + + //Creates layers for each item in the inventory (ordered by page) + private void CreateItemLayers(AnimatorController source, ref List activeStates) + { + //Create a template machine to duplicate + AnimatorStateMachine templateMachine = new AnimatorStateMachine(); + ChildAnimatorState[] states = new ChildAnimatorState[templateMachine.states.Length + 2]; + templateMachine.states.CopyTo(states, 2); + + //Create a template state to duplicate + ChildAnimatorState templateState = new ChildAnimatorState + { + state = new AnimatorState + { + name = "", + motion = null, + } + }; + + //Get a starting position for the states + Vector3 pos = templateMachine.anyStatePosition; + + //Create an off state + ChangeState(templateState, "Off"); + templateState.position = pos - new Vector3(150, -45); + states[0] = templateState.DeepClone(); + + //Create an on state + templateState.position = pos + new Vector3(100, 45); + ChangeState(templateState, "On"); + states[1] = templateState.DeepClone(); + + //Add the states to the template machine + templateMachine.states = states; + + //Create a template transition + AnimatorStateTransition templateTransition = new AnimatorStateTransition + { + destinationState = null, + isExit = false, + hasExitTime = false, + duration = 0, + canTransitionToSelf = false, + conditions = null + }; + + //For each item in the inventory... + for (int i = 0; i < toggleables.Length; i++) + { + EditorUtility.DisplayProgressBar("Inventory Inventor", string.Format(CultureInfo.InvariantCulture, "Creating Item Layers: {0} ({1:#0.##%})", aliases[i], (i + 1f) / toggleables.Length), 0.55f + (0.35f * (float.Parse(i.ToString()) / toggleables.Length))); + int[] active = activeStates[i]; + + //Create a layer + source.AddLayer(aliases[i]); + AnimatorControllerLayer[] layers = source.layers; + AnimatorControllerLayer currentLayer = layers[layers.Length - 1]; + currentLayer.defaultWeight = 1; + + //Create an AnyState transition to the on and off state with their assigned conditionals + AnimatorStateTransition[] transitions = new AnimatorStateTransition[2]; + ChangeTransition(templateTransition, ref active[0], templateMachine.states[0].state); + transitions[0] = (AnimatorStateTransition)templateTransition.DeepClone(templateMachine.states[0]); + ChangeState(templateMachine.states[1].state, toggleables[i]); + ChangeTransition(templateTransition, ref active[1], templateMachine.states[1].state); + transitions[1] = (AnimatorStateTransition)templateTransition.DeepClone(templateMachine.states[1]); + templateMachine.anyStateTransitions = transitions; + + //Name the machine for detection later and clone it + templateMachine.name = "Inventory " + (i + 1); + currentLayer.stateMachine = templateMachine.DeepClone(); + layers[layers.Length - 1] = currentLayer; + source.layers = layers; + } + return; + } + + //Creates the master layer that handles menu inputs and the idle sync + private void CreateMasterLayer(AnimatorController source, int itemTotal, out List activeStates) + { + EditorUtility.DisplayProgressBar("Inventory Inventor", "Creating Master Layer: Preparing", 0.1f); + + for (int i = 0; i < source.layers.Length; i++) + { + //Remove layer if already present + if (source.layers[i].name == "Inventory Master") + { + source.RemoveLayer(i); + break; + } + } + //Add Master Layer + source.AddLayer("Inventory Master"); + AnimatorControllerLayer masterLayer = source.layers[source.layers.Length - 1]; + + //Create List of state values + activeStates = new List(); + int value = itemTotal + 1; + for (int i = 0; i < itemTotal; i++) + { + activeStates.Add(new int[] { value, value + 1 }); + value += 2; + } + + //Create an array states to be created + ChildAnimatorState[] states = new ChildAnimatorState[masterLayer.stateMachine.states.Length + (itemTotal * 4) + 1]; + masterLayer.stateMachine.states.CopyTo(states, itemTotal * 4 + 1); + + //Store a starting position for the states + Vector3 pos = masterLayer.stateMachine.entryPosition; + + //Create the starting state + states[itemTotal * 4] = new ChildAnimatorState + { + position = pos + new Vector3(-25, 50), + state = new AnimatorState + { + name = "Remote Clients", + } + }; + + //Create a template state for cloning + ChildAnimatorState templateState = new ChildAnimatorState + { + state = new AnimatorState + { + name = "", + behaviours = new StateMachineBehaviour[] { ScriptableObject.CreateInstance() } + } + }; + ((VRCAvatarParameterDriver)templateState.state.behaviours[0]).parameters.Add(new VRC.SDKBase.VRC_AvatarParameterDriver.Parameter { name = "Inventory", value = 0 }); + + //Move down a row in the grid + pos += new Vector3(0, 125); + + //Create a template transition + AnimatorStateTransition templateTransition = new AnimatorStateTransition + { + destinationState = null, + isExit = false, + hasExitTime = true, + exitTime = refreshRate, + duration = 0, + canTransitionToSelf = false, + conditions = null + }; + + //Start with the 3rd state + int index = 2; + + //Only create the sync loop if Auto Sync is enabled + if (syncMode == 1) + { + //Create the first pair of syncing states + ChangeState(templateState, "Syncing " + 1, activeStates[0], true); + templateState.position = pos - new Vector3(150, 0); + states[0] = templateState.DeepClone(); + ChangeState(templateState, "Syncing " + 1 + " ", activeStates[0], false); + templateState.position = pos + new Vector3(100, 0); + states[1] = templateState.DeepClone(); + + //Move down a row + pos += new Vector3(0, 75); + + //foreach entry in the state array from 2 to 1 less then itemTotal doubled... + for (int i = 2; i < itemTotal * 2 && syncMode == 1; i++) + { + EditorUtility.DisplayProgressBar("Inventory Inventor", string.Format(CultureInfo.InvariantCulture, "Creating Master Layer: Creating States ({0:#0.##%})", (i + 1f) / (itemTotal * 4)), 0.1f + (0.225f * ((i + 1f) / (itemTotal * 4)))); + + //If 'i' is even, create an on sync state, otherwise make an off sync state + switch (i % 2 == 0) + { + case true: + ChangeState(templateState, "Syncing " + index, activeStates[index - 1], true); + templateState.position = pos - new Vector3(150, 0); + states[i] = templateState.DeepClone(); + + //Create transitions to this state from the previous pair + ChangeTransition(templateTransition, states[i], index, true); + states[i - 2].state.AddTransition((AnimatorStateTransition)AnimatorExtensions.DeepClone(templateTransition, states[i])); + ChangeTransition(templateTransition, states[i], index, true); + states[i - 1].state.AddTransition((AnimatorStateTransition)AnimatorExtensions.DeepClone(templateTransition, states[i])); + break; + case false: + ChangeState(templateState, "Syncing " + index + " ", activeStates[index - 1], false); + templateState.position = pos + new Vector3(100, 0); + states[i] = templateState.DeepClone(); + + //Create transitions to this state from the previous pair + ChangeTransition(templateTransition, states[i], index, false); + states[i - 2].state.AddTransition((AnimatorStateTransition)AnimatorExtensions.DeepClone(templateTransition, states[i])); + ChangeTransition(templateTransition, states[i], index, false); + states[i - 3].state.AddTransition((AnimatorStateTransition)AnimatorExtensions.DeepClone(templateTransition, states[i])); + + //Move on to the next item in the inventory + index++; + + //Move down a row + pos += new Vector3(0, 75); + break; + } + } + //Final Transitions + states[(itemTotal * 2) - 1].state.AddExitTransition(); + states[(itemTotal * 2) - 1].state.transitions[0].AddCondition(AnimatorConditionMode.If, 0, "IsLocal"); + states[(itemTotal * 2) - 1].state.transitions[0].hasExitTime = true; + states[(itemTotal * 2) - 1].state.transitions[0].exitTime = refreshRate; + states[(itemTotal * 2) - 1].state.transitions[0].duration = 0; + states[(itemTotal * 2) - 2].state.AddExitTransition(); + states[(itemTotal * 2) - 2].state.transitions[0].AddCondition(AnimatorConditionMode.If, 0, "IsLocal"); + states[(itemTotal * 2) - 2].state.transitions[0].hasExitTime = true; + states[(itemTotal * 2) - 2].state.transitions[0].exitTime = refreshRate; + states[(itemTotal * 2) - 2].state.transitions[0].duration = 0; + } + //First transition to trap remote clients (or acts as an idle state when Auto Sync is disabled) + states[itemTotal * 4].state.AddExitTransition(); + states[itemTotal * 4].state.transitions[0].AddCondition(AnimatorConditionMode.If, 0, "IsLocal"); + states[itemTotal * 4].state.transitions[0].hasExitTime = false; + states[itemTotal * 4].state.transitions[0].duration = 0; + masterLayer.stateMachine.exitPosition = pos; + + //Create a template toggle state + ChildAnimatorState templateToggle = new ChildAnimatorState + { + state = new AnimatorState { behaviours = new StateMachineBehaviour[] { ScriptableObject.CreateInstance() } } + }; + ((VRCAvatarParameterDriver)templateToggle.state.behaviours[0]).parameters.Add(new VRC.SDKBase.VRC_AvatarParameterDriver.Parameter { name = "Inventory", value = 0 }); + + //Create a template toggle transition + AnimatorStateTransition toggleTransition = new AnimatorStateTransition + { + isExit = true, + exitTime = 1f, + hasExitTime = true, + duration = 0f, + canTransitionToSelf = false + }; + + //Reset or adjust some existing values + templateTransition.hasExitTime = false; + index = itemTotal * 2; + pos += new Vector3(0, 60); + + //Create an array of AnyState transitions + AnimatorStateTransition[] anyTransitions = new AnimatorStateTransition[itemTotal * 2]; + + //For each item in the inventory... (Loops in reverse to make UI nicer) + for (int i = itemTotal - 1; i >= 0; i--) + { + EditorUtility.DisplayProgressBar("Inventory Inventor", string.Format(CultureInfo.InvariantCulture, "Creating Master Layer: Creating States ({0:#0.##%})", index / (itemTotal * 4)), 0.1f + (0.225f * (index / (itemTotal * 4f)))); + + //Create an On state + templateToggle.state.name = ("Toggling " + (i + 1) + ": On"); + templateToggle.position = pos - new Vector3(150, 0); + + //Adjust parameter settings + ((VRCAvatarParameterDriver)templateToggle.state.behaviours[0]).parameters[0].value = activeStates[i][1]; + ((VRCAvatarParameterDriver)templateToggle.state.behaviours[0]).parameters.Add(new VRC.SDKBase.VRC_AvatarParameterDriver.Parameter { name = "Inventory " + (i + 1), value = 1 }); + + //Clone the template state + states[index] = templateToggle.DeepClone(); + + //Remove additional parameter in the driver for the next state + ((VRCAvatarParameterDriver)templateToggle.state.behaviours[0]).parameters.RemoveAt(1); + + //Clone an exit transition + states[index].state.transitions = new AnimatorStateTransition[] { (AnimatorStateTransition)toggleTransition.DeepClone() }; + + //Configure the AnyState transition template + templateTransition.destinationState = states[index].state; + templateTransition.conditions = new AnimatorCondition[0]; + templateTransition.AddCondition(AnimatorConditionMode.Equals, i + 1, "Inventory"); + templateTransition.AddCondition(AnimatorConditionMode.IfNot, 0, "Inventory " + (i + 1)); + templateTransition.AddCondition(AnimatorConditionMode.If, 0, "IsLocal"); + + //Clone the transition and move on to the Off state + anyTransitions[index - (itemTotal * 2)] = (AnimatorStateTransition)templateTransition.DeepClone(states[index]); + index++; + + //Create an Off state + templateToggle.state.name = ("Toggling " + (i + 1) + ": Off"); + templateToggle.position = pos + new Vector3(100, 0); + + //Adjust parameter settings + ((VRCAvatarParameterDriver)templateToggle.state.behaviours[0]).parameters[0].value = activeStates[i][0]; + ((VRCAvatarParameterDriver)templateToggle.state.behaviours[0]).parameters.Add(new VRC.SDKBase.VRC_AvatarParameterDriver.Parameter { name = "Inventory " + (i + 1), value = 0 }); + + //Clone the template state + states[index] = templateToggle.DeepClone(); + + //Remove additional parameter in the driver for the next state + ((VRCAvatarParameterDriver)templateToggle.state.behaviours[0]).parameters.RemoveAt(1); + + //Clone an exit transition + states[index].state.transitions = new AnimatorStateTransition[] { (AnimatorStateTransition)toggleTransition.DeepClone() }; + + //Configure the AnyState transition template + templateTransition.destinationState = states[index].state; + templateTransition.conditions = new AnimatorCondition[0]; + templateTransition.AddCondition(AnimatorConditionMode.Equals, i + 1, "Inventory"); + templateTransition.AddCondition(AnimatorConditionMode.If, 0, "Inventory " + (i + 1)); + templateTransition.AddCondition(AnimatorConditionMode.If, 0, "IsLocal"); + + //Clone the transition and move on to next item in the inventory + anyTransitions[index - (itemTotal * 2)] = (AnimatorStateTransition)templateTransition.DeepClone(states[index]); + index++; + + //Move down a row + pos += new Vector3(0, 75); + } + + //Assign the states and transitions to the master layer + masterLayer.stateMachine.anyStatePosition = pos; + masterLayer.stateMachine.states = states; + masterLayer.stateMachine.anyStateTransitions = anyTransitions; + masterLayer.stateMachine.defaultState = states[itemTotal * 4].state; + + //Add the entry transitions if Auto Sync is enabled + if (syncMode == 1) + { + masterLayer.stateMachine.AddEntryTransition(states[0].state); + masterLayer.stateMachine.AddEntryTransition(states[1].state); + AnimatorTransition[] entryTransitions = masterLayer.stateMachine.entryTransitions; + entryTransitions[0].AddCondition(AnimatorConditionMode.If, 0, "Inventory 1"); + entryTransitions[1].AddCondition(AnimatorConditionMode.IfNot, 0, "Inventory 1"); + masterLayer.stateMachine.entryTransitions = entryTransitions; + } + + //Replace the layer in the Animator + AnimatorControllerLayer[] layers = source.layers; + layers[layers.Length - 1] = masterLayer; + source.layers = layers; + return; + } + + //Helper method for modifying transitions + public static void ChangeTransition(AnimatorStateTransition transition, ref int value, AnimatorState state) + { + transition.destinationState = state; + transition.conditions = new AnimatorCondition[0]; + transition.AddCondition(AnimatorConditionMode.Equals, value, "Inventory"); + } + + //Helper method for modifying transitions + public static void ChangeTransition(AnimatorStateTransition transition, ChildAnimatorState childState, int name, bool value) + { + transition.destinationState = childState.state; + transition.conditions = new AnimatorCondition[0]; + switch (value) + { + case true: + transition.AddCondition(AnimatorConditionMode.If, 0, "Inventory " + name); + break; + case false: + transition.AddCondition(AnimatorConditionMode.IfNot, 0, "Inventory " + name); + break; + } + } + + //Helper method for modifying states + public static void ChangeState(ChildAnimatorState childState, string name, int[] value, bool state) + { + switch (state) + { + case true: + ChangeState(childState.state, name, value[1]); + break; + case false: + ChangeState(childState.state, name, value[0]); + break; + } + return; + } + + //Helper method for modifying states + public static void ChangeState(AnimatorState state, string name, int value) + { + state.name = name; + ((VRCAvatarParameterDriver)state.behaviours[0]).parameters[0].value = value; + return; + } + + //Helper method for modifying states + public static void ChangeState(AnimatorState state, string name) + { + state.name = name; + return; + } + + //Helper method for modifying states + public static void ChangeState(ChildAnimatorState childState, string name) + { + ChangeState(childState.state, name); + return; + } + + //Helper method for modifying states + public static void ChangeState(AnimatorState state, Motion motion) + { + state.motion = motion; + return; + } + + //Reverts any changes made during the process in case of an error or exception + private void RevertChanges() + { + //Save Assets + AssetDatabase.SaveAssets(); + + //Restore original data to pre-existing files + if (backupManager != null && !backupManager.RestoreAssets()) + Debug.LogError("[Inventory Inventor] Failed to revert all changes."); + + //Delete any generated assets that didn't overwrite files + for (int i = 0; generated != null && i < generated.ToArray().Length; i++) + if (File.Exists(generated[i].path) && !AssetDatabase.DeleteAsset(generated[i].path)) + Debug.LogError("[Inventory Inventor] Failed to revert all changes."); + + //Save assets so folders will be seen as empty + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + //Delete created folders if now empty + if (AssetDatabase.IsValidFolder(outputPath + Path.DirectorySeparatorChar + "Animators") && AssetDatabase.FindAssets("", new string[] { outputPath + Path.DirectorySeparatorChar + "Animators" }).Length == 0) + if (!AssetDatabase.DeleteAsset(outputPath + Path.DirectorySeparatorChar + "Animators")) + Debug.LogError("[Inventory Inventor] Failed to revert all changes."); + if (AssetDatabase.IsValidFolder(outputPath + Path.DirectorySeparatorChar + "Menus") && AssetDatabase.FindAssets("", new string[] { outputPath + Path.DirectorySeparatorChar + "Menus" }).Length == 0) + if (!AssetDatabase.DeleteAsset(outputPath + Path.DirectorySeparatorChar + "Menus")) + Debug.LogError("[Inventory Inventor] Failed to revert all changes."); + + //Final asset save + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + + //Updates output and relative paths if the directory of this package changes + public void UpdatePaths() + { + string old = relativePath; + relativePath = AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("InventoryInventor")[0]).Substring(0, AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("InventoryInventor")[0]).LastIndexOf("Editor") - 1); + if (relativePath == old) + return; + else if (outputPath == null || !AssetDatabase.IsValidFolder(outputPath)) + { + outputPath = relativePath + Path.DirectorySeparatorChar + "Output"; + } + } + + //Blank MonoBehaviour for running network coroutines + private class NetworkManager : MonoBehaviour { } + + //Compares the VERSION file present to the one on GitHub to see if a newer version is available + public static void CheckForUpdates() + { + //Static, so path must be reobtained + string relativePath = AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("InventoryInventor")[0]).Substring(0, AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("InventoryInventor")[0]).LastIndexOf("Editor") - 1); + + //Read VERSION file + string installedVersion = (AssetDatabase.FindAssets("VERSION", new string[] { relativePath }).Length > 0) ? File.ReadAllText(AssetDatabase.GUIDToAssetPath(AssetDatabase.FindAssets("VERSION", new string[] { relativePath })[0])) : ""; + + //Create hidden object to run the coroutine. + GameObject netMan = new GameObject { hideFlags = HideFlags.HideInHierarchy }; + + //Run a coroutine to retrieve the GitHub data. + netMan.AddComponent().StartCoroutine(GetText("https://raw.githubusercontent.com/Joshuarox100/VRC-Inventory-Inventor/master/Editor/VERSION", latestVersion => { + //Network Error + if (latestVersion == "") + { + EditorUtility.DisplayDialog("Inventory Inventor", "Failed to fetch the latest version.\n(Check console for details.)", "Close"); + } + //VERSION file missing + else if (installedVersion == "") + { + EditorUtility.DisplayDialog("Inventory Inventor", "Failed to identify installed version.\n(VERSION file was not found.)", "Close"); + } + //Project has been archived + else if (latestVersion == "RIP") + { + EditorUtility.DisplayDialog("Inventory Inventor", "Project has been put on hold indefinitely.", "Close"); + } + //An update is available + else if (installedVersion != latestVersion) + { + if (EditorUtility.DisplayDialog("Inventory Inventor", "A new update is available! (" + latestVersion + ")\nOpen the Releases page?", "Yes", "No")) + { + Application.OpenURL("https://github.com/Joshuarox100/VRC-Inventory-Inventor"); + } + } + //Using latest version + else + { + EditorUtility.DisplayDialog("Inventory Inventor", "You are using the latest version.", "Close"); + } + DestroyImmediate(netMan); + })); + } + + //Retrieves text from a provided URL + private static IEnumerator GetText(string url, Action result) + { + UnityWebRequest www = UnityWebRequest.Get(url); + yield return www.SendWebRequest(); + + if (www.isNetworkError || www.isHttpError) + { + Debug.LogError(www.error); + result?.Invoke(""); + } + else + { + result?.Invoke(www.downloadHandler.text); + } + } +} diff --git a/Images/Step 1.png b/Images/Step 1.png new file mode 100644 index 0000000..4b251be Binary files /dev/null and b/Images/Step 1.png differ diff --git a/Images/Step 2.png b/Images/Step 2.png new file mode 100644 index 0000000..9cd6318 Binary files /dev/null and b/Images/Step 2.png differ diff --git a/Images/Step 3.png b/Images/Step 3.png new file mode 100644 index 0000000..5d23e85 Binary files /dev/null and b/Images/Step 3.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a28b21d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2020] [Joshuarox100] + +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. \ No newline at end of file diff --git a/README.md b/README.md index 52e5f3b..c5eb45e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,65 @@ -# VRC-AV3-Inventories - Make inventories fast with AV3 Inventories! +Inventory Inventor +============== + +Author: Joshuarox100 + +Description: Make inventories fast with Inventory Inventor! With it, you can create inventories with up to 64 synced toggles, all contained within a single Expression Parameter! + +Dependencies: +- [BMB Libraries](https://github.com/Joshuarox100/BMB-Libraries) (Included) +- VRCSDK3-AVATAR (Not Included) + +Setting Up +-------------- +Before following these steps, set up your avatar how you normally would and ensure that you have a basic understanding of how Avatars 3.0 works. + +1) Download and import the latest **Unity Package** from [**Releases**](https://github.com/Joshuarox100/VRC-Inventory-Inventor/releases) on GitHub **(You will have issues if you don't)**. + +

+ +

+ +2) Open the setup window located under Tools -> Avatars 3.0 -> Inventory Inventor. + +

+ +

+ +3) Next, configure how you want the inventory to be setup for your avatar. + >If you want to know what a particular option does, hover your mouse over its text for a brief explanation. + +

+ +

+ +4) Finally, click Create to generate the inventory for your avatar! + >The inventory will be added to or replaced in the FX Animator contained within your descriptor. If an Animator hasn't been used for the FX layer, it will clone the VRCSDK's FX template and add it there. + +Everything should now be fully set up! If you have any issues or questions, look in the [troubleshooting](#troubleshooting) and [questions](#common-questions) section below before [contacting me](#contacting-me). + +Common Questions +-------------- +**Can I make submenus using the UI?** +>Not yet! That feature is planned for the future though. + +**Can I have multiple inventories on a single avatar?** +>Let me answer your question with a another question: *Why do you need more than 64 toggles to begin with?* + Truthfully though, if you are seriously needing that much inventory space, perhaps you should consider splitting it up into multiple avatars for performance reasons alone or consider other ways to achieve what you're attempting to do. That said, once I do implement submenu creation in my UI, I will be raising the limit to 85 items to accomodate it. + +**How do those who join the world late see me?** +>If you leave Auto Sync on, the current state of your inventory will be synced to them over a short period of time while the system is idle. If Auto Sync is left off, late-joiners will only see the initial state of the objects until you toggle them again, a bit like how toggles work in Avatars 2.0. + +Troubleshooting +-------------- +**My Inventory isn't syncing correctly to people joining late.** +>Your Refresh Rate may be too fast for the network to handle. Try recreating your inventory using a slower time. + +**The Debug menu is just showing random numbers for each of the item layers.** +>This is a visual bug caused by having State Machines named differently than their originating Layer. This doesn't actually cause any problems remotely or locally so you don't need to worry about it too much. A bug report for it exists on the Feedback forum if you want to upvote it [here](https://feedback.vrchat.com/avatar-30/p/bug-debug-menu-fails-to-show-state-names-when-the-state-machine-is-named-differe). + +**"An exception occured!"** +>If this happens, ensure you have a clean install of Inventory Inventor, and if the problem persists, [let me know](#contacting-me)! + +Contacting Me +-------------- +If you still have some questions or recommendations you'd like to throw my way, you can ask me on Discord (Joshuarox100#5024) or leave a suggestion or issue on the [GitHub](https://github.com/Joshuarox100/VRC-Inventory-Inventor/issues) page.