using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Animations; using UnityEngine; using VF.Model.Feature; using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.ScriptableObjects; namespace VF.Builder { public class AvatarManager { private readonly GameObject avatarObject; private readonly VRCAvatarDescriptor avatar; private readonly string tmpDir; private readonly Func currentFeatureNumProvider; private readonly Func currentFeatureNameProvider; private readonly Func currentMenuSortPosition; private readonly MutableManager mutableManager; public AvatarManager( GameObject avatarObject, string tmpDir, Func currentFeatureNumProvider, Func currentFeatureNameProvider, Func currentMenuSortPosition, MutableManager mutableManager ) { this.avatarObject = avatarObject; this.avatar = avatarObject.GetComponent(); this.tmpDir = tmpDir; this.currentFeatureNumProvider = currentFeatureNumProvider; this.currentFeatureNameProvider = currentFeatureNameProvider; this.currentMenuSortPosition = currentMenuSortPosition; this.mutableManager = mutableManager; } private MenuManager _menu; public MenuManager GetMenu() { if (_menu == null) { var menu = ScriptableObject.CreateInstance(); VRCFuryAssetDatabase.SaveAsset(menu, tmpDir, "VRCFury Menu for " + avatarObject.name); var initializing = true; _menu = new MenuManager(menu, tmpDir, () => initializing ? 0 : currentMenuSortPosition()); var origMenu = VRCAvatarUtils.GetAvatarMenu(avatar); if (origMenu != null) _menu.MergeMenu(origMenu); VRCAvatarUtils.SetAvatarMenu(avatar, menu); initializing = false; } return _menu; } private readonly Dictionary _controllers = new Dictionary(); public ControllerManager GetController(VRCAvatarDescriptor.AnimLayerType type) { if (!_controllers.TryGetValue(type, out var output)) { var existingController = VRCAvatarUtils.GetAvatarController(avatar, type); var filename = "VRCFury " + type + " for " + avatarObject.name; AnimatorController ctrl; if (existingController != null) { ctrl = mutableManager.CopyRecursive(existingController, filename); } else { ctrl = new AnimatorController(); VRCFuryAssetDatabase.SaveAsset(ctrl, tmpDir, filename); } output = new ControllerManager(ctrl, GetParams, type, currentFeatureNumProvider, currentFeatureNameProvider, tmpDir, GetClipStorage()); _controllers[type] = output; VRCAvatarUtils.SetAvatarController(avatar, type, ctrl); } return output; } public IEnumerable GetAllTouchedControllers() { return _controllers.Values; } public IEnumerable GetAllUsedControllers() { return VRCAvatarUtils.GetAllControllers(avatar) .Where(c => c.Item1 != null) .Select(c => GetController(c.Item3)); } public IEnumerable> GetAllUsedControllersRaw() { return VRCAvatarUtils.GetAllControllers(avatar) .Where(c => c.Item1 != null) .Select(c => Tuple.Create(c.Item3, c.Item1)); } private ParamManager _params; public ParamManager GetParams() { if (_params == null) { var origParams = VRCAvatarUtils.GetAvatarParams(avatar); var filename = "VRCFury Params for " + avatarObject.name; VRCExpressionParameters prms; if (origParams != null) { prms = mutableManager.CopyRecursive(origParams, filename); } else { prms = ScriptableObject.CreateInstance(); prms.parameters = new VRCExpressionParameters.Parameter[]{}; VRCFuryAssetDatabase.SaveAsset(prms, tmpDir, filename); } VRCAvatarUtils.SetAvatarParams(avatar, prms); _params = new ParamManager(prms); } return _params; } private ClipStorageManager _clipStorage; public ClipStorageManager GetClipStorage() { if (_clipStorage == null) { _clipStorage = new ClipStorageManager(tmpDir, currentFeatureNumProvider); } return _clipStorage; } public void Finish(OverrideMenuSettings menuSettings) { if (_menu != null) { _menu.SortMenu(); MenuSplitter.SplitMenus(_menu.GetRaw(), menuSettings); MenuSplitter.FixNulls(_menu.GetRaw()); } // The VRCSDK usually builds the debug window name lookup before the avatar is built, so we have // to update it with our newly-added states foreach (var c in _controllers.Values) { EditorUtility.SetDirty(c.GetRaw()); RebuildDebugHashes(c); } // The VRCSDK usually does this before the avatar is built var layers = avatar.baseAnimationLayers; for (var i = 0; i < layers.Length; i++) { var layer = layers[i]; if (layer.type == VRCAvatarDescriptor.AnimLayerType.Gesture) { var c = layer.animatorController as AnimatorController; if (c && c.layers.Length > 0) { layer.mask = c.layers[0].avatarMask; layers[i] = layer; } } } if (_menu != null) EditorUtility.SetDirty(_menu.GetRaw()); if (_params != null) EditorUtility.SetDirty(_params.GetRaw()); if (_clipStorage != null) _clipStorage.Finish(); if (_params != null) { var maxParams = VRCExpressionParameters.MAX_PARAMETER_COST; if (maxParams > 9999) { // Some versions of the VRChat SDK have a broken value for this maxParams = 256; } if (_params.GetRaw().CalcTotalCost() > maxParams) { throw new Exception( "Avatar is out of space for parameters! Used " + _params.GetRaw().CalcTotalCost() + "/" + maxParams + ". Delete some params from your avatar's param file, or disable some VRCFury features."); } } } /** * VRC calculates the animator debug map before vrcfury is invoked, so if we want our states to show up in the * debug panel, we need to add them to the map ourselves. */ private void RebuildDebugHashes(ControllerManager manager) { foreach (var layer in manager.GetManagedLayers()) { ProcessStateMachine(layer, ""); void ProcessStateMachine(AnimatorStateMachine stateMachine, string prefix) { //Update prefix prefix = prefix + stateMachine.name + "."; //States foreach (var state in stateMachine.states) { VRCAvatarDescriptor.DebugHash hash = new VRCAvatarDescriptor.DebugHash(); string fullName = prefix + state.state.name; hash.hash = Animator.StringToHash(fullName); hash.name = fullName.Remove(0, layer.name.Length + 1); avatar.animationHashSet.Add(hash); } foreach (var subMachine in stateMachine.stateMachines) ProcessStateMachine(subMachine.stateMachine, prefix); } } EditorUtility.SetDirty(avatar); } } }