313 lines
14 KiB
C#
313 lines
14 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.Animations;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
|
using VF.Builder;
|
|
using VF.Feature.Base;
|
|
using VF.Inspector;
|
|
using VF.Model;
|
|
using VF.Model.Feature;
|
|
using VF.Model.StateAction;
|
|
using VRC.SDK3.Avatars.Components;
|
|
using VRC.SDK3.Dynamics.Contact.Components;
|
|
using VRC.SDK3.Dynamics.PhysBone.Components;
|
|
using Toggle = VF.Model.Feature.Toggle;
|
|
|
|
namespace VF.Feature {
|
|
|
|
public class FullControllerBuilder : FeatureBuilder<FullController> {
|
|
|
|
[FeatureBuilderAction]
|
|
public void Apply() {
|
|
var toggleIsInt = false;
|
|
foreach (var p in model.prms) {
|
|
if (p.parameters == null) continue;
|
|
foreach (var param in p.parameters.parameters) {
|
|
if (param.name == model.toggleParam && param.valueType == VRCExpressionParameters.ValueType.Int)
|
|
toggleIsInt = true;
|
|
if (string.IsNullOrWhiteSpace(param.name)) continue;
|
|
var newParam = new VRCExpressionParameters.Parameter {
|
|
name = RewriteParamName(param.name),
|
|
valueType = param.valueType,
|
|
saved = param.saved && !model.ignoreSaved,
|
|
defaultValue = param.defaultValue
|
|
};
|
|
manager.GetParams().addSyncedParam(newParam);
|
|
}
|
|
}
|
|
|
|
var toMerge = new List<(VRCAvatarDescriptor.AnimLayerType, AnimatorController)>();
|
|
foreach (var c in model.controllers) {
|
|
var type = c.type;
|
|
var source = c.controller as AnimatorController;
|
|
if (source == null) continue;
|
|
var copy = mutableManager.CopyRecursive(source, saveFilename: "tmp");
|
|
toMerge.Add((type, copy));
|
|
}
|
|
|
|
// Record the offsets so we can fix them later
|
|
var offsetBuilder = allBuildersInRun.OfType<AnimatorLayerControlOffsetBuilder>().First();
|
|
offsetBuilder.RegisterControllerSet(toMerge);
|
|
|
|
foreach (var (type, from) in toMerge) {
|
|
var targetController = manager.GetController(type);
|
|
Merge(from, targetController);
|
|
}
|
|
|
|
foreach (var m in model.menus) {
|
|
if (m.menu == null) continue;
|
|
var prefix = MenuManager.SplitPath(m.prefix);
|
|
manager.GetMenu().MergeMenu(prefix, m.menu, RewriteParamName);
|
|
}
|
|
|
|
foreach (var receiver in GetBaseObject().GetComponentsInChildren<VRCContactReceiver>(true)) {
|
|
if (rewrittenParams.Contains(receiver.parameter)) {
|
|
receiver.parameter = RewriteParamName(receiver.parameter);
|
|
}
|
|
}
|
|
foreach (var physbone in GetBaseObject().GetComponentsInChildren<VRCPhysBone>(true)) {
|
|
if (rewrittenParams.Contains(physbone.parameter + "_IsGrabbed") || rewrittenParams.Contains(physbone.parameter + "_Angle") || rewrittenParams.Contains(physbone.parameter + "_Stretch")) {
|
|
physbone.parameter = RewriteParamName(physbone.parameter);
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(model.toggleParam)) {
|
|
addOtherFeature(new ObjectState {
|
|
states = {
|
|
new ObjectState.ObjState {
|
|
action = ObjectState.Action.DEACTIVATE,
|
|
obj = GetBaseObject()
|
|
}
|
|
}
|
|
});
|
|
var toggleParam = RewriteParamName(model.toggleParam);
|
|
addOtherFeature(new Toggle {
|
|
name = toggleParam,
|
|
state = new State {
|
|
actions = { new ObjectToggleAction { obj = GetBaseObject() } }
|
|
},
|
|
securityEnabled = model.useSecurityForToggle,
|
|
addMenuItem = false,
|
|
usePrefixOnParam = false,
|
|
paramOverride = toggleParam,
|
|
useInt = toggleIsInt
|
|
});
|
|
}
|
|
}
|
|
|
|
private readonly HashSet<string> rewrittenParams = new HashSet<string>();
|
|
|
|
string RewriteParamName(string name) {
|
|
if (string.IsNullOrWhiteSpace(name)) return name;
|
|
if (VRChatGlobalParams.Contains(name)) return name;
|
|
if (model.allNonsyncedAreGlobal) {
|
|
var synced = model.prms.Any(p => p.parameters.parameters.Any(param => param.name == name));
|
|
if (!synced) return name;
|
|
}
|
|
if (model.globalParams.Contains(name)) return name;
|
|
if (model.globalParams.Contains("*")) return name;
|
|
rewrittenParams.Add(name);
|
|
return ControllerManager.NewParamName(name, uniqueModelNum);
|
|
}
|
|
|
|
void RewriteClip(AnimationClip clip) {
|
|
if (clip == null) return;
|
|
if (AssetDatabase.GetAssetPath(clip).Contains("/proxy_")) return;
|
|
|
|
ClipCopier.Rewrite(
|
|
clip,
|
|
fromObj: GetBaseObject(),
|
|
fromRoot: avatarObject,
|
|
removePrefixes: model.removePrefixes,
|
|
addPrefix: model.addPrefix,
|
|
rootBindingsApplyToAvatar: model.rootBindingsApplyToAvatar,
|
|
rewriteParam: RewriteParamName
|
|
);
|
|
}
|
|
|
|
private void Merge(AnimatorController from, ControllerManager toMain) {
|
|
var to = toMain.GetRaw();
|
|
var type = toMain.GetType();
|
|
|
|
if (type == VRCAvatarDescriptor.AnimLayerType.Gesture && from.layers.Length > 0) {
|
|
toMain.UnionBaseMask(from.layers[0].avatarMask);
|
|
}
|
|
|
|
var newParams = from.parameters
|
|
.Concat(from.parameters.Select(p => {
|
|
p.name = RewriteParamName(p.name);
|
|
return p;
|
|
}))
|
|
.Where(p => {
|
|
var exists = to.parameters.Any(existing => existing.name == p.name);
|
|
return !exists;
|
|
});
|
|
to.parameters = to.parameters.Concat(newParams).ToArray();
|
|
|
|
foreach (var layer in from.layers) {
|
|
AnimatorIterator.ForEachState(layer.stateMachine, state => {
|
|
state.speedParameter = RewriteParamName(state.speedParameter);
|
|
state.cycleOffsetParameter = RewriteParamName(state.cycleOffsetParameter);
|
|
state.mirrorParameter = RewriteParamName(state.mirrorParameter);
|
|
state.timeParameter = RewriteParamName(state.timeParameter);
|
|
});
|
|
AnimatorIterator.ForEachBehaviour(layer.stateMachine, (b, add) => {
|
|
switch (b) {
|
|
case VRCAvatarParameterDriver oldB: {
|
|
foreach (var p in oldB.parameters) {
|
|
p.name = RewriteParamName(p.name);
|
|
p.source = RewriteParamName(p.source);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
AnimatorIterator.ForEachBlendTree(layer.stateMachine, tree => {
|
|
tree.blendParameter = RewriteParamName(tree.blendParameter);
|
|
tree.blendParameterY = RewriteParamName(tree.blendParameterY);
|
|
tree.children = tree.children.Select(child => {
|
|
child.directBlendParameter = RewriteParamName(child.directBlendParameter);
|
|
return child;
|
|
}).ToArray();
|
|
});
|
|
var allClips = new HashSet<AnimationClip>();
|
|
AnimatorIterator.ForEachClip(layer.stateMachine, clip => {
|
|
allClips.Add(clip);
|
|
});
|
|
foreach (var clip in allClips) {
|
|
RewriteClip(clip);
|
|
}
|
|
|
|
AnimatorIterator.ForEachTransition(layer.stateMachine, transition => {
|
|
transition.conditions = transition.conditions.Select(c => {
|
|
c.parameter = RewriteParamName(c.parameter);
|
|
return c;
|
|
}).ToArray();
|
|
EditorUtility.SetDirty(transition);
|
|
});
|
|
}
|
|
|
|
toMain.TakeLayersFrom(from);
|
|
AssetDatabase.RemoveObjectFromAsset(from);
|
|
}
|
|
|
|
GameObject GetBaseObject() {
|
|
if (model.rootObjOverride) return model.rootObjOverride;
|
|
return featureBaseObject;
|
|
}
|
|
|
|
public override string GetEditorTitle() {
|
|
return "Full Controller";
|
|
}
|
|
|
|
public override VisualElement CreateEditor(SerializedProperty prop) {
|
|
var content = new VisualElement();
|
|
|
|
content.Add(VRCFuryEditorUtils.Info(
|
|
"This feature will merge the given controller / menu / parameters into the avatar" +
|
|
" during the upload process."));
|
|
|
|
content.Add(VRCFuryEditorUtils.WrappedLabel("Controllers:"));
|
|
content.Add(VRCFuryEditorUtils.List(prop.FindPropertyRelative("controllers"),
|
|
(i, el) => {
|
|
var wrapper = new VisualElement();
|
|
wrapper.style.flexDirection = FlexDirection.Row;
|
|
var a = VRCFuryEditorUtils.Prop(el.FindPropertyRelative("controller"));
|
|
a.style.flexBasis = 0;
|
|
a.style.flexGrow = 1;
|
|
wrapper.Add(a);
|
|
var b = VRCFuryEditorUtils.Prop(el.FindPropertyRelative("type"));
|
|
b.style.flexBasis = 0;
|
|
b.style.flexGrow = 1;
|
|
wrapper.Add(b);
|
|
return wrapper;
|
|
}));
|
|
|
|
content.Add(VRCFuryEditorUtils.WrappedLabel("Menus + Path Prefix:"));
|
|
content.Add(VRCFuryEditorUtils.WrappedLabel("(If prefix is left empty, menu will be merged into avatar's root menu)"));
|
|
content.Add(VRCFuryEditorUtils.List(prop.FindPropertyRelative("menus"),
|
|
(i, el) => {
|
|
var wrapper = new VisualElement();
|
|
wrapper.style.flexDirection = FlexDirection.Row;
|
|
var a = VRCFuryEditorUtils.Prop(el.FindPropertyRelative("menu"));
|
|
a.style.flexBasis = 0;
|
|
a.style.flexGrow = 1;
|
|
wrapper.Add(a);
|
|
var b = VRCFuryEditorUtils.Prop(el.FindPropertyRelative("prefix"));
|
|
b.style.flexBasis = 0;
|
|
b.style.flexGrow = 1;
|
|
wrapper.Add(b);
|
|
return wrapper;
|
|
}));
|
|
|
|
content.Add(VRCFuryEditorUtils.WrappedLabel("Parameters:"));
|
|
content.Add(VRCFuryEditorUtils.List(prop.FindPropertyRelative("prms"),
|
|
(i, el) => VRCFuryEditorUtils.Prop(el.FindPropertyRelative("parameters"))));
|
|
|
|
content.Add(VRCFuryEditorUtils.WrappedLabel("Global Parameters:"));
|
|
content.Add(VRCFuryEditorUtils.WrappedLabel(
|
|
"Parameters in this list will have their name kept as is, allowing you to interact with " +
|
|
"parameters in the avatar itself or other instances of the prop. Note that VRChat global " +
|
|
"parameters (such as gestures) are included by default."));
|
|
content.Add(VRCFuryEditorUtils.List(prop.FindPropertyRelative("globalParams")));
|
|
|
|
content.Add(VRCFuryEditorUtils.WrappedLabel("Remove prefixes from clips:"));
|
|
content.Add(VRCFuryEditorUtils.WrappedLabel(
|
|
"Strings in this list will be removed from the start of every animated key, useful if the animations" +
|
|
" in the controller were originally written to be based from the avatar root, " +
|
|
"but you are now trying to use as a VRCFury prop."));
|
|
content.Add(VRCFuryEditorUtils.List(prop.FindPropertyRelative("removePrefixes")));
|
|
|
|
var adv = new Foldout {
|
|
text = "Advanced Options",
|
|
value = false
|
|
};
|
|
|
|
adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("allNonsyncedAreGlobal"), "Make all unsynced params global (Legacy mode)"));
|
|
adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("ignoreSaved"), "Force all synced parameters to be un-saved"));
|
|
adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("rootObjOverride"), "Root object override"));
|
|
adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("rootBindingsApplyToAvatar"), "Root bindings always apply to avatar (Basically only for gogoloco)"));
|
|
adv.Add(VRCFuryEditorUtils.WrappedLabel(
|
|
"Parameter name for prop toggling. If set, this entire prop will be de-activated whenever" +
|
|
" this boolean parameter within the Full Controller is false."));
|
|
adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("toggleParam")));
|
|
adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("useSecurityForToggle"), "Use security for toggle"));
|
|
adv.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("addPrefix"),
|
|
"Add prefix to clips"));
|
|
|
|
content.Add(adv);
|
|
|
|
return content;
|
|
}
|
|
|
|
private static HashSet<string> VRChatGlobalParams = new HashSet<string> {
|
|
"IsLocal",
|
|
"Viseme",
|
|
"Voice",
|
|
"GestureLeft",
|
|
"GestureRight",
|
|
"GestureLeftWeight",
|
|
"GestureRightWeight",
|
|
"AngularY",
|
|
"VelocityX",
|
|
"VelocityY",
|
|
"VelocityZ",
|
|
"Upright",
|
|
"Grounded",
|
|
"Seated",
|
|
"AFK",
|
|
"TrackingType",
|
|
"VRMode",
|
|
"MuteSelf",
|
|
"InStation",
|
|
"AvatarVersion",
|
|
"GroundProximity"
|
|
};
|
|
}
|
|
|
|
}
|