#if MA_VRCSDK3_AVATARS #region using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.ndmf; using UnityEditor; using UnityEditor.Animations; using UnityEngine; using UnityEngine.Profiling; 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; using UnityObject = UnityEngine.Object; #endregion namespace nadena.dev.modular_avatar.core.editor { internal class ParameterRenameMappings { public static ParameterRenameMappings Get(ndmf.BuildContext ctx) { return ctx.GetState(); } public Dictionary<(ModularAvatarParameters, ParameterNamespace, string), string> Remappings = new Dictionary<(ModularAvatarParameters, ParameterNamespace, string), string>(); private int internalParamIndex; public string Remap(ModularAvatarParameters p, ParameterNamespace ns, string s) { var tuple = (p, ns, s); if (Remappings.TryGetValue(tuple, out var mapping)) return mapping; mapping = s + "$$Internal_" + internalParamIndex++; Remappings[tuple] = mapping; return mapping; } } internal class DefaultValues { public ImmutableDictionary InitialValueOverrides; } internal class RenameParametersHook { private const string DEFAULT_EXP_PARAMS_ASSET_GUID = "03a6d797deb62f0429471c4e17ea99a7"; private BuildContext _context; // TODO: Move into NDMF private ImmutableList PhysBoneSuffixes = ImmutableList.Empty .Add("_IsGrabbed") .Add("_IsPosed") .Add("_Angle") .Add("_Stretch") .Add("_Squish"); class ParameterInfo { private static long encounterOrderCounter; public ParameterConfig ResolvedParameter; public List TypeSources = new List(); public List DefaultSources = new List(); public ImmutableHashSet ConflictingValues = ImmutableHashSet.Empty; public ImmutableHashSet ConflictingSyncTypes = ImmutableHashSet.Empty; public bool TypeConflict, DefaultValueConflict; public long encounterOrder = encounterOrderCounter++; public VRCExpressionParameters.ValueType? ValueType { get { switch (ResolvedParameter.syncType) { case ParameterSyncType.Bool: return VRCExpressionParameters.ValueType.Bool; case ParameterSyncType.Float: return VRCExpressionParameters.ValueType.Float; case ParameterSyncType.Int: return VRCExpressionParameters.ValueType.Int; default: return null; } } } public void MergeSibling(ParameterInfo info) { MergeCommon(info); ResolvedParameter.m_overrideAnimatorDefaults = (ResolvedParameter.m_overrideAnimatorDefaults && ResolvedParameter.HasDefaultValue) || (info.ResolvedParameter.m_overrideAnimatorDefaults && info.ResolvedParameter.HasDefaultValue); if (ResolvedParameter.HasDefaultValue && info.ResolvedParameter.HasDefaultValue) { if (Math.Abs(ResolvedParameter.defaultValue - info.ResolvedParameter.defaultValue) > ParameterConfig.VALUE_EPSILON) { DefaultValueConflict = true; ConflictingValues = ConflictingValues.Add(ResolvedParameter.defaultValue); ConflictingValues = ConflictingValues.Add(info.ResolvedParameter.defaultValue); } } ResolvedParameter.saved |= info.ResolvedParameter.saved; ResolvedParameter.localOnly &= info.ResolvedParameter.localOnly; } public void MergeChild(ParameterInfo info) { MergeCommon(info); if (!ResolvedParameter.HasDefaultValue && info.ResolvedParameter.HasDefaultValue) { ResolvedParameter.defaultValue = info.ResolvedParameter.defaultValue; ResolvedParameter.hasExplicitDefaultValue = info.ResolvedParameter.hasExplicitDefaultValue; ResolvedParameter.m_overrideAnimatorDefaults = info.ResolvedParameter.m_overrideAnimatorDefaults; } } void MergeCommon(ParameterInfo info) { if (ResolvedParameter.syncType == ParameterSyncType.NotSynced) { ResolvedParameter.syncType = info.ResolvedParameter.syncType; } else if (ResolvedParameter.syncType != info.ResolvedParameter.syncType && info.ResolvedParameter.syncType != ParameterSyncType.NotSynced) { TypeConflict = true; ConflictingSyncTypes = ConflictingSyncTypes .Add(ResolvedParameter.syncType) .Add(info.ResolvedParameter.syncType); } TypeSources.AddRange(info.TypeSources); DefaultSources.AddRange(info.DefaultSources); TypeConflict = TypeConflict || info.TypeConflict; DefaultValueConflict = DefaultValueConflict || info.DefaultValueConflict; ConflictingValues = ConflictingValues.Union(info.ConflictingValues); ConflictingSyncTypes = ConflictingSyncTypes.Union(info.ConflictingSyncTypes); encounterOrder = Math.Min(encounterOrder, info.encounterOrder); } } public void OnPreprocessAvatar(GameObject avatar, BuildContext context) { if (!context.AvatarDescriptor) return; _context = context; var syncParams = WalkTree(avatar); SetExpressionParameters(avatar, syncParams); _context.PluginBuildContext.GetState().InitialValueOverrides = syncParams.Where(p => p.Value.ResolvedParameter.HasDefaultValue && p.Value.ResolvedParameter.OverrideAnimatorDefaults) .ToImmutableDictionary(p => p.Key, p => p.Value.ResolvedParameter.defaultValue); // clean up all parameters objects before the ParameterAssignerPass runs foreach (var p in avatar.GetComponentsInChildren()) UnityObject.DestroyImmediate(p); } private void SetExpressionParameters(GameObject avatarRoot, ImmutableDictionary allParams) { var syncParams = allParams.Where(kvp => kvp.Value.ResolvedParameter.syncType != ParameterSyncType.NotSynced) .ToImmutableDictionary(); 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); _context.SaveAsset(expParams); var knownParams = expParams.parameters.Select(p => p.name).ToImmutableHashSet(); var parameters = expParams.parameters .Select(p => ResolveParameter(p, syncParams)) .ToList(); foreach (var kvp in syncParams.OrderBy(kvp => kvp.Value.encounterOrder)) { var name = kvp.Key; var param = kvp.Value; if (param.TypeConflict) { var t1 = param.ConflictingSyncTypes.First(); var t2 = param.ConflictingSyncTypes.Skip(1).First(); List paramList = new List { name, t1, t2 }; paramList.AddRange(param.TypeSources.Cast()); BuildReport.Log(ErrorSeverity.Error, "error.rename_params.type_conflict", paramList.ToArray()); } if (param.DefaultValueConflict) { var v1 = param.ConflictingValues.First(); var v2 = param.ConflictingValues.Skip(1).First(); List paramList = new List { name, v1, v2 }; paramList.AddRange(param.DefaultSources.Cast()); BuildReport.Log(ErrorSeverity.NonFatal, "error.rename_params.default_value_conflict", paramList.ToArray()); } if (!knownParams.Contains(name) && param.ResolvedParameter.syncType != ParameterSyncType.NotSynced) { var converted = new VRCExpressionParameters.Parameter(); converted.name = name; switch (param.ResolvedParameter.syncType) { case ParameterSyncType.Bool: converted.valueType = VRCExpressionParameters.ValueType.Bool; break; case ParameterSyncType.Float: converted.valueType = VRCExpressionParameters.ValueType.Float; break; case ParameterSyncType.Int: converted.valueType = VRCExpressionParameters.ValueType.Int; break; default: throw new ArgumentException("Unknown parameter sync type " + param.ResolvedParameter.syncType); } converted.networkSynced = !param.ResolvedParameter.localOnly; converted.saved = param.ResolvedParameter.saved; converted.defaultValue = param.ResolvedParameter.defaultValue; parameters.Add(converted); } } expParams.parameters = parameters.ToArray(); /* if (expParams.CalcTotalCost() > VRCExpressionParameters.MAX_PARAMETER_COST) { BuildReport.LogFatal("error.rename_params.too_many_synced_params", new[] { "" + expParams.CalcTotalCost(), "" + VRCExpressionParameters.MAX_PARAMETER_COST, } ); } */ avatar.expressionParameters = expParams; } private VRCExpressionParameters.Parameter ResolveParameter( VRCExpressionParameters.Parameter parameter, ImmutableDictionary syncParams ) { if (!syncParams.TryGetValue(parameter.name, out var info)) { return parameter; } if (parameter.valueType != info.ValueType && info.ValueType != null) { var list = new List { parameter.name, parameter.valueType, info.ValueType, _context.AvatarDescriptor.expressionParameters, }; list.AddRange(info.TypeSources); BuildReport.Log(ErrorSeverity.Error, "error.rename_params.type_conflict", parameter.name, list ); } var newParameter = new VRCExpressionParameters.Parameter(); newParameter.defaultValue = info.ResolvedParameter.HasDefaultValue ? info.ResolvedParameter.defaultValue : parameter.defaultValue; newParameter.name = parameter.name; newParameter.valueType = parameter.valueType; newParameter.networkSynced = parameter.networkSynced || !info.ResolvedParameter.localOnly; newParameter.saved = parameter.saved || info.ResolvedParameter.saved; return newParameter; } private ImmutableDictionary WalkTree( GameObject obj ) { var paramInfo = ndmf.ParameterInfo.ForContext(_context.PluginBuildContext); ImmutableDictionary rv = ImmutableDictionary.Empty; var p = obj.GetComponent(); if (p != null) { rv = BuildReport.ReportingObject(p, () => CollectParameters(p, paramInfo.GetParameterRemappingsAt(p, true))); } foreach (var merger in obj.GetComponents()) { if (merger.deleteAttachedAnimator) { break; } } // Note: To match prior behavior, we use all mappings that apply to this gameobject when updating components // other than MA Parameters, not just ones from components listed prior. foreach (var component in obj.GetComponents()) { BuildReport.ReportingObject(component, () => { switch (component) { case VRCPhysBone bone: { var remaps = paramInfo.GetParameterRemappingsAt(obj); if (bone.parameter != null && remaps.TryGetValue((ParameterNamespace.PhysBonesPrefix, bone.parameter), out var newVal)) { bone.parameter = newVal.ParameterName; } break; } case VRCContactReceiver contact: { if (contact.parameter != null && paramInfo.GetParameterRemappingsAt(obj) .TryGetValue((ParameterNamespace.Animator, contact.parameter), out var newVal)) { contact.parameter = newVal.ParameterName; } break; } case ModularAvatarMergeAnimator merger: { // RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController if (merger.animator is AnimatorOverrideController overrideController) { merger.animator = _context.ConvertAnimatorController(overrideController); } var mappings = paramInfo.GetParameterRemappingsAt(obj); var remap = mappings.SelectMany(item => { if (item.Key.Item1 == ParameterNamespace.Animator) return new[] { item }; return PhysBoneSuffixes.Select(suffix => new KeyValuePair<(ParameterNamespace, string), ParameterMapping>( (ParameterNamespace.Animator, item.Key.Item2 + suffix), new ParameterMapping(item.Value.ParameterName + suffix, item.Value.IsHidden) ) ); }).ToImmutableDictionary(); if (merger.animator != null) { Profiler.BeginSample("DeepCloneAnimator"); merger.animator = new DeepClone(_context.PluginBuildContext).DoClone(merger.animator); Profiler.EndSample(); ProcessRuntimeAnimatorController(merger.animator, remap); } break; } case ModularAvatarMergeBlendTree merger: { var bt = merger.BlendTree as BlendTree; if (bt != null) { merger.BlendTree = bt = new DeepClone(_context.PluginBuildContext).DoClone(bt); ProcessBlendtree(bt, paramInfo.GetParameterRemappingsAt(obj)); } break; } case ModularAvatarMenuInstaller installer: { if (installer.menuToAppend != null && installer.enabled) { ProcessMenuInstaller(installer, paramInfo.GetParameterRemappingsAt(obj)); } break; } case ModularAvatarMenuItem menuItem: { var remaps = paramInfo.GetParameterRemappingsAt(obj); if (menuItem.Control.parameter?.name != null && remaps.TryGetValue((ParameterNamespace.Animator, menuItem.Control.parameter.name), out var newVal)) { menuItem.Control.parameter.name = newVal.ParameterName; } foreach (var subParam in menuItem.Control.subParameters ?? Array.Empty()) { if (subParam?.name != null && remaps.TryGetValue((ParameterNamespace.Animator, subParam.name), out var subNewVal)) { subParam.name = subNewVal.ParameterName; } } break; } } }); } var mergedChildParams = ImmutableDictionary.Empty; foreach (Transform child in obj.transform) { var childParams = WalkTree(child.gameObject); foreach (var kvp in childParams) { var name = kvp.Key; var info = kvp.Value; if (mergedChildParams.TryGetValue(name, out var priorInfo)) { priorInfo.MergeSibling(info); } else { mergedChildParams = mergedChildParams.SetItem(name, info); } } } foreach (var kvp in mergedChildParams) { var name = kvp.Key; var info = kvp.Value; info.ResolvedParameter.nameOrPrefix = name; if (rv.TryGetValue(name, out var priorInfo)) { priorInfo.MergeChild(info); } else { rv = rv.SetItem(name, info); } } return rv; } private void ProcessRuntimeAnimatorController(RuntimeAnimatorController controller, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remap) { if (controller is AnimatorController ac) { ProcessAnimator(ac, remap); } else if (controller is AnimatorOverrideController aoc) { var list = new List>(); aoc.GetOverrides(list); for (var i = 0; i < list.Count; i++) { var kvp = list[i]; if (kvp.Value != null) ProcessClip(kvp.Value, remap); } ProcessRuntimeAnimatorController(aoc.runtimeAnimatorController, remap); } } private void ProcessMenuInstaller(ModularAvatarMenuInstaller installer, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { Dictionary remapped = new Dictionary(); if (installer.menuToAppend == null) return; _context.PostProcessControls.Add(installer, control => { control.parameter.name = remap(remaps, control.parameter.name); foreach (var subParam in control.subParameters) { subParam.name = remap(remaps, subParam.name); } }); } private void ProcessAnimator(AnimatorController controller, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { if (remaps.IsEmpty) return; var visited = new HashSet(); var queue = new Queue(); var parameters = controller.parameters; for (int i = 0; i < parameters.Length; i++) { if (remaps.TryGetValue((ParameterNamespace.Animator, parameters[i].name), out var newName)) { parameters[i].name = newName.ParameterName; } } controller.parameters = parameters; foreach (var layer in controller.layers) { if (layer.stateMachine != null) { queue.Enqueue(layer.stateMachine); } } Profiler.BeginSample("Walk animator graph"); while (queue.Count > 0) { var sm = queue.Dequeue(); if (visited.Contains(sm)) continue; visited.Add(sm); foreach (var behavior in sm.behaviours) { if (behavior is VRCAvatarParameterDriver driver) { ProcessDriver(driver, remaps); } } 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 t in sm.GetStateMachineTransitions(sub.stateMachine)) { ProcessTransition(t, remaps); } } foreach (var st in sm.states) { ProcessState(st.state, remaps); } } Profiler.EndSample(); } private void ProcessState(AnimatorState state, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> 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); } } ProcessMotion(state.motion, remaps); } private void ProcessMotion(Motion motion, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { if (motion is BlendTree blendTree) ProcessBlendtree(blendTree, remaps); if (motion is AnimationClip clip) ProcessClip(clip, remaps); } private void ProcessClip(AnimationClip clip, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { var curveBindings = AnimationUtility.GetCurveBindings(clip); var bindingsToUpdate = new List(); var newCurves = new List(); foreach (var binding in curveBindings) { if (binding.path != "" || binding.type != typeof(Animator)) continue; if (remaps.TryGetValue((ParameterNamespace.Animator, binding.propertyName), out var newBinding)) { var curCurve = AnimationUtility.GetEditorCurve(clip, binding); bindingsToUpdate.Add(binding); newCurves.Add(null); bindingsToUpdate.Add(new EditorCurveBinding { path = "", type = typeof(Animator), propertyName = newBinding.ParameterName }); newCurves.Add(curCurve); } } if (bindingsToUpdate.Any()) { AnimationUtility.SetEditorCurves(clip, bindingsToUpdate.ToArray(), newCurves.ToArray()); // Workaround apparent unity bug where the clip's curves are not deleted for (var i = 0; i < bindingsToUpdate.Count; i++) if (newCurves[i] == null && AnimationUtility.GetEditorCurve(clip, bindingsToUpdate[i]) != null) AnimationUtility.SetEditorCurve(clip, bindingsToUpdate[i], newCurves[i]); } } private void ProcessBlendtree(BlendTree blendTree, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { blendTree.blendParameter = remap(remaps, blendTree.blendParameter); blendTree.blendParameterY = remap(remaps, blendTree.blendParameterY); var children = blendTree.children; for (int i = 0; i < children.Length; i++) { var childMotion = children[i]; ProcessMotion(childMotion.motion, remaps); childMotion.directBlendParameter = remap(remaps, childMotion.directBlendParameter); children[i] = childMotion; } blendTree.children = children; } private void ProcessDriver(VRCAvatarParameterDriver driver, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> 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(AnimatorTransitionBase t, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps) { bool dirty = false; var conditions = t.conditions; for (int i = 0; i < conditions.Length; i++) { var cond = conditions[i]; cond.parameter = remap(remaps, cond.parameter, ref dirty); conditions[i] = cond; } if (dirty) t.conditions = conditions; } private ImmutableDictionary CollectParameters(ModularAvatarParameters p, ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps ) { var remapper = ParameterRenameMappings.Get(_context.PluginBuildContext); ImmutableDictionary parameterInfos = ImmutableDictionary.Empty; foreach (var param in p.parameters) { if (param.isPrefix) continue; var remapTo = param.nameOrPrefix; if (remaps.TryGetValue((ParameterNamespace.Animator, param.nameOrPrefix), out var mapping)) { remapTo = mapping.ParameterName; } ParameterConfig parameterConfig = param; parameterConfig.nameOrPrefix = remapTo; parameterConfig.remapTo = remapTo; var info = new ParameterInfo() { ResolvedParameter = parameterConfig, }; if (parameterConfig.syncType != ParameterSyncType.NotSynced) { info.TypeSources.Add(p); } if (parameterConfig.HasDefaultValue) { info.DefaultSources.Add(p); } if (parameterInfos.TryGetValue(remapTo, out var existing)) { existing.MergeSibling(info); } else { parameterInfos = parameterInfos.SetItem(remapTo, info); } } return parameterInfos; } // This is generic to simplify remapping parameter driver fields, some of which are 'object's. private T remap(ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps, T x) where T : class { bool tmp = false; return remap(remaps, x, ref tmp); } private T remap(ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> remaps, T x, ref bool anyRemapped) where T : class { if (x is string s && remaps.TryGetValue((ParameterNamespace.Animator, s), out var newS)) { anyRemapped = true; return (T) (object) newS.ParameterName; } return x; } } } #endif