using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using UnityEditor; using UnityEditor.Animations; using UnityEngine; using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.ScriptableObjects; using VRC.SDK3.Dynamics.Contact.Components; using VRC.SDK3.Dynamics.PhysBone.Components; using Object = UnityEngine.Object; namespace nadena.dev.modular_avatar.core.editor { public class RenameParametersHook { private const string DEFAULT_EXP_PARAMS_ASSET_GUID = "03a6d797deb62f0429471c4e17ea99a7"; private int internalParamIndex = 0; private Dictionary _syncedParams = new Dictionary(); public void OnPreprocessAvatar(GameObject avatar) { _syncedParams.Clear(); WalkTree(avatar, ImmutableDictionary.Empty, ImmutableDictionary.Empty); SetExpressionParameters(avatar); } private void SetExpressionParameters(GameObject avatarRoot) { var avatar = avatarRoot.GetComponent(); var expParams = avatar.expressionParameters; if (expParams == null) { var path = AssetDatabase.GUIDToAssetPath(DEFAULT_EXP_PARAMS_ASSET_GUID); expParams = AssetDatabase.LoadAssetAtPath(path); } if (expParams == null) { // Can't find the defaults??? expParams = ScriptableObject.CreateInstance(); } expParams = Object.Instantiate(expParams); AssetDatabase.CreateAsset(expParams, Util.GenerateAssetPath()); var knownParams = expParams.parameters.Select(p => p.name).ToImmutableHashSet(); var parameters = expParams.parameters.ToList(); foreach (var kvp in _syncedParams) { var name = kvp.Key; var param = kvp.Value; if (!knownParams.Contains(name)) { parameters.Add(param); } } expParams.parameters = parameters.ToArray(); if (expParams.CalcTotalCost() > VRCExpressionParameters.MAX_PARAMETER_COST) { throw new Exception("Too many synced parameters: " + "Cost " + expParams.CalcTotalCost() + " > " + VRCExpressionParameters.MAX_PARAMETER_COST ); } avatar.expressionParameters = expParams; } private void WalkTree( GameObject obj, ImmutableDictionary remaps, ImmutableDictionary prefixRemaps ) { var p = obj.GetComponent(); if (p != null) { ApplyRemappings(p, ref remaps, ref prefixRemaps); } var willPurgeAnimators = false; foreach (var merger in obj.GetComponents()) { if (merger.deleteAttachedAnimator) { willPurgeAnimators = true; break; } } foreach (var component in obj.GetComponents()) { switch (component) { case VRCPhysBone bone: { if (bone.parameter != null && prefixRemaps.TryGetValue(bone.parameter, out var newVal)) { bone.parameter = newVal; } break; } case VRCContactReceiver contact: { if (contact.parameter != null && remaps.TryGetValue(contact.parameter, out var newVal)) { contact.parameter = newVal; } break; } case Animator anim: { if (willPurgeAnimators) break; // animator will be deleted in subsequent processing var controller = anim.runtimeAnimatorController as AnimatorController; if (controller != null) { ProcessAnimator(ref controller, remaps); anim.runtimeAnimatorController = controller; } break; } case ModularAvatarMergeAnimator merger: { var controller = merger.animator as AnimatorController; if (controller != null) { ProcessAnimator(ref controller, remaps); merger.animator = controller; } break; } case ModularAvatarMenuInstaller installer: { if (installer.menuToAppend != null && installer.enabled) { ProcessMenu(ref installer.menuToAppend, remaps); } break; } } } foreach (Transform child in obj.transform) { WalkTree(child.gameObject, remaps, prefixRemaps); } } private void ProcessMenu(ref VRCExpressionsMenu rootMenu, ImmutableDictionary remaps) { Dictionary remapped = new Dictionary(); rootMenu = Transform(rootMenu); VRCExpressionsMenu Transform(VRCExpressionsMenu menu) { if (remapped.TryGetValue(menu, out var newMenu)) return newMenu; newMenu = Object.Instantiate(menu); AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath()); remapped[menu] = newMenu; foreach (var control in newMenu.controls) { control.parameter.name = remap(remaps, control.parameter.name); foreach (var subParam in control.subParameters) { subParam.name = remap(remaps, subParam.name); } if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) { control.subMenu = Transform(control.subMenu); } } return newMenu; } } private void ProcessAnimator(ref AnimatorController controller, ImmutableDictionary remaps) { var visited = new HashSet(); var queue = new Queue(); // Deep clone the animator controller = Util.DeepCloneAnimator(controller); var parameters = controller.parameters; for (int i = 0; i < parameters.Length; i++) { if (remaps.TryGetValue(parameters[i].name, out var newName)) { parameters[i].name = newName; } } controller.parameters = parameters; foreach (var layer in controller.layers) { if (layer.stateMachine != null) { queue.Enqueue(layer.stateMachine); } } while (queue.Count > 0) { var sm = queue.Dequeue(); if (visited.Contains(sm)) continue; visited.Add(sm); foreach (var t in sm.anyStateTransitions) { ProcessTransition(t, remaps); } foreach (var t in sm.entryTransitions) { ProcessTransition(t, remaps); } foreach (var sub in sm.stateMachines) { queue.Enqueue(sub.stateMachine); } foreach (var st in sm.states) { ProcessState(st.state, remaps); } } } private void ProcessState(AnimatorState state, ImmutableDictionary remaps) { state.mirrorParameter = remap(remaps, state.mirrorParameter); state.timeParameter = remap(remaps, state.timeParameter); state.speedParameter = remap(remaps, state.speedParameter); state.cycleOffsetParameter = remap(remaps, state.cycleOffsetParameter); foreach (var t in state.transitions) { ProcessTransition(t, remaps); } foreach (var behavior in state.behaviours) { if (behavior is VRCAvatarParameterDriver driver) { ProcessDriver(driver, remaps); } } } private void ProcessDriver(VRCAvatarParameterDriver driver, ImmutableDictionary remaps) { var parameters = driver.parameters; for (int i = 0; i < parameters.Count; i++) { var p = parameters[i]; p.name = remap(remaps, p.name); p.source = remap(remaps, p.source); p.destParam = remap(remaps, p.destParam); p.sourceParam = remap(remaps, p.sourceParam); } } private void ProcessTransition(AnimatorStateTransition t, ImmutableDictionary remaps) { var conditions = t.conditions; for (int i = 0; i < conditions.Length; i++) { var cond = conditions[i]; cond.parameter = remap(remaps, cond.parameter); conditions[i] = cond; } t.conditions = conditions; } private void ProcessTransition(AnimatorTransition t, ImmutableDictionary remaps) { var conditions = t.conditions; for (int i = 0; i < conditions.Length; i++) { var cond = conditions[i]; cond.parameter = remap(remaps, cond.parameter); conditions[i] = cond; } t.conditions = conditions; } private void ApplyRemappings(ModularAvatarParameters p, ref ImmutableDictionary remaps, ref ImmutableDictionary prefixRemaps ) { foreach (var param in p.parameters) { bool doRemap = true; var remapTo = param.remapTo; if (param.internalParameter) { remapTo = param.nameOrPrefix + "$$Internal_" + internalParamIndex++; } else if (string.IsNullOrWhiteSpace(remapTo)) { doRemap = false; remapTo = param.nameOrPrefix; } // Apply outer scope remaps (only if not an internal parameter) // Note that this continues the else chain above. else if (param.isPrefix && prefixRemaps.TryGetValue(remapTo, out var outerScope)) { remapTo = outerScope; } else if (remaps.TryGetValue(remapTo, out outerScope)) { remapTo = outerScope; } if (doRemap) { if (param.isPrefix) { prefixRemaps = prefixRemaps.Add(param.nameOrPrefix, remapTo); foreach (var suffix in ParameterPolicy.PhysBoneSuffixes) { var suffixKey = param.nameOrPrefix + suffix; var suffixValue = remapTo + suffix; remaps = remaps.Add(suffixKey, suffixValue); } } else { remaps = remaps.Add(param.nameOrPrefix, remapTo); } } if (!param.isPrefix && param.syncType != ParameterSyncType.NotSynced) { AddSyncParam(param, remapTo); } } } private void AddSyncParam( ParameterConfig parameterConfig, string remapTo ) { if (_syncedParams.ContainsKey(remapTo)) return; VRCExpressionParameters.ValueType type; switch (parameterConfig.syncType) { case ParameterSyncType.Bool: type = VRCExpressionParameters.ValueType.Bool; break; case ParameterSyncType.Float: type = VRCExpressionParameters.ValueType.Float; break; case ParameterSyncType.Int: type = VRCExpressionParameters.ValueType.Int; break; default: throw new Exception("Unknown sync type " + parameterConfig.syncType); } _syncedParams[remapTo] = new VRCExpressionParameters.Parameter { name = remapTo, valueType = type, defaultValue = parameterConfig.defaultValue, saved = parameterConfig.saved, }; } // This is generic to simplify remapping parameter driver fields, some of which are 'object's. private T remap(ImmutableDictionary remaps, T x) where T : class { if (x is string s && remaps.TryGetValue(s, out var newS)) { return (T) (object) newS; } return x; } } }