519 lines
21 KiB
C#
519 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using VF.Builder;
|
|
using VF.Feature.Base;
|
|
using VF.Inspector;
|
|
using VF.Model;
|
|
using VF.Model.Feature;
|
|
using VF.Model.StateAction;
|
|
using Object = UnityEngine.Object;
|
|
using Toggle = VF.Model.Feature.Toggle;
|
|
|
|
namespace VF.Feature {
|
|
|
|
public class ToggleBuilder : FeatureBuilder<Toggle> {
|
|
private List<VFAState> exclusiveTagTriggeringStates = new List<VFAState>();
|
|
private VFABool param;
|
|
private AnimationClip restingClip;
|
|
|
|
public ISet<string> GetExclusiveTags() {
|
|
if (model.enableExclusiveTag) {
|
|
return model.exclusiveTag.Split(',')
|
|
.Select(tag => tag.Trim())
|
|
.Where(tag => !string.IsNullOrWhiteSpace(tag))
|
|
.ToImmutableHashSet();
|
|
}
|
|
return new HashSet<string>();
|
|
}
|
|
public VFABool GetParam() {
|
|
return param;
|
|
}
|
|
|
|
[FeatureBuilderAction]
|
|
public void Apply() {
|
|
// If the toggle is setup to /actually/ toggle something (and it's not an off state for just an exclusive tag or something)
|
|
// Then don't even bother adding it. The user probably removed the object, so the toggle shouldn't be present.
|
|
if (model.state.IsEmpty() && model.state.actions.Count > 0) {
|
|
return;
|
|
}
|
|
|
|
if (model.slider) {
|
|
var stops = new List<Puppet.Stop> {
|
|
new Puppet.Stop(1, 0, model.state)
|
|
};
|
|
var puppet = new Puppet {
|
|
name = model.name,
|
|
saved = model.saved,
|
|
slider = true,
|
|
stops = stops,
|
|
defaultX = model.slider && model.defaultOn ? model.defaultSliderValue : 0,
|
|
enableIcon = model.enableIcon,
|
|
icon = model.icon,
|
|
};
|
|
addOtherFeature(puppet);
|
|
return;
|
|
}
|
|
|
|
var physBoneResetter = CreatePhysBoneResetter(model.resetPhysbones, model.name);
|
|
|
|
var layerName = model.name;
|
|
var fx = GetFx();
|
|
var layer = fx.NewLayer(layerName);
|
|
var off = layer.NewState("Off");
|
|
|
|
VFACondition onCase;
|
|
var paramName = model.paramOverride ?? model.name;
|
|
if (model.useInt) {
|
|
var numParam = fx.NewInt(paramName, synced: true, saved: model.saved, def: model.defaultOn ? 1 : 0, usePrefix: model.usePrefixOnParam);
|
|
onCase = numParam.IsNotEqualTo(0);
|
|
} else {
|
|
var boolParam = fx.NewBool(paramName, synced: true, saved: model.saved, def: model.defaultOn, usePrefix: model.usePrefixOnParam);
|
|
param = boolParam;
|
|
onCase = boolParam.IsTrue();
|
|
}
|
|
|
|
if (model.separateLocal) {
|
|
var isLocal = fx.IsLocal().IsTrue();
|
|
Apply(fx, layer, off, onCase.And(isLocal.Not()), "On Remote", model.state, model.transitionStateIn, model.transitionStateOut, physBoneResetter);
|
|
Apply(fx, layer, off, onCase.And(isLocal), "On Local", model.localState, model.localTransitionStateIn, model.localTransitionStateOut, physBoneResetter);
|
|
} else {
|
|
Apply(fx, layer, off, onCase, "On", model.state, model.transitionStateIn, model.transitionStateOut, physBoneResetter);
|
|
}
|
|
|
|
if (model.addMenuItem) {
|
|
manager.GetMenu().NewMenuToggle(
|
|
model.name,
|
|
param,
|
|
icon: model.enableIcon ? model.icon : null
|
|
);
|
|
}
|
|
}
|
|
|
|
private void Apply(
|
|
ControllerManager fx,
|
|
VFALayer layer,
|
|
VFAState off,
|
|
VFACondition onCase,
|
|
string onName,
|
|
State action,
|
|
State inAction,
|
|
State outAction,
|
|
VFABool physBoneResetter
|
|
) {
|
|
var clip = LoadState(model.name + " " + onName, action);
|
|
|
|
if (restingClip == null && model.includeInRest) {
|
|
restingClip = clip;
|
|
var defaultsManager = allBuildersInRun
|
|
.OfType<FixWriteDefaultsBuilder>()
|
|
.First();
|
|
defaultsManager.forceRecordBindings.UnionWith(AnimationUtility.GetCurveBindings(clip));
|
|
defaultsManager.forceRecordBindings.UnionWith(AnimationUtility.GetObjectReferenceCurveBindings(clip));
|
|
}
|
|
|
|
if (model.securityEnabled) {
|
|
var securityLockUnlocked = allBuildersInRun
|
|
.OfType<SecurityLockBuilder>()
|
|
.Select(f => f.GetEnabled())
|
|
.FirstOrDefault();
|
|
if (securityLockUnlocked != null) {
|
|
onCase = onCase.And(securityLockUnlocked);
|
|
}
|
|
}
|
|
|
|
VFAState inState;
|
|
VFAState onState;
|
|
if (model.hasTransition && inAction != null && !inAction.IsEmpty()) {
|
|
var transitionClipIn = LoadState(model.name + onName + " In", inAction);
|
|
inState = layer.NewState(onName + " In").WithAnimation(transitionClipIn);
|
|
onState = layer.NewState(onName).WithAnimation(clip);
|
|
inState.TransitionsTo(onState).When().WithTransitionExitTime(1);
|
|
} else {
|
|
inState = onState = layer.NewState(onName).WithAnimation(clip);
|
|
}
|
|
exclusiveTagTriggeringStates.Add(inState);
|
|
off.TransitionsTo(inState).When(onCase);
|
|
|
|
if (model.simpleOutTransition) outAction = inAction;
|
|
if (model.hasTransition && outAction != null && !outAction.IsEmpty()) {
|
|
var transitionClipOut = LoadState(model.name + onName + " Out", outAction);
|
|
var outState = layer.NewState(onName + " Out").WithAnimation(transitionClipOut).Speed(model.simpleOutTransition ? -1 : 1);
|
|
onState.TransitionsTo(outState).When(onCase.Not());
|
|
outState.TransitionsToExit().When().WithTransitionExitTime(1);
|
|
} else {
|
|
onState.TransitionsToExit().When(onCase.Not());
|
|
}
|
|
|
|
if (physBoneResetter != null) {
|
|
off.Drives(physBoneResetter, true);
|
|
inState.Drives(physBoneResetter, true);
|
|
}
|
|
|
|
if (model.enableDriveGlobalParam && !string.IsNullOrWhiteSpace(model.driveGlobalParam)) {
|
|
var driveGlobal = fx.NewBool(
|
|
model.driveGlobalParam,
|
|
synced: false,
|
|
saved: false,
|
|
def: false,
|
|
usePrefix: false
|
|
);
|
|
off.Drives(driveGlobal, false);
|
|
inState.Drives(driveGlobal, true);
|
|
}
|
|
}
|
|
|
|
[FeatureBuilderAction(FeatureOrder.CollectToggleExclusiveTags)]
|
|
public void ApplyExclusiveTags() {
|
|
if (exclusiveTagTriggeringStates.Count == 0) return;
|
|
|
|
var fx = GetFx();
|
|
var allOthersOffCondition = fx.Always();
|
|
|
|
var myTags = GetExclusiveTags();
|
|
foreach (var other in allBuildersInRun
|
|
.OfType<ToggleBuilder>()
|
|
.Where(b => b != this)) {
|
|
var otherTags = other.GetExclusiveTags();
|
|
var conflictsWithOther = myTags.Any(myTag => otherTags.Contains(myTag));
|
|
if (conflictsWithOther) {
|
|
var otherParam = other.GetParam();
|
|
if (otherParam != null) {
|
|
foreach (var state in exclusiveTagTriggeringStates) {
|
|
state.Drives(otherParam, false);
|
|
}
|
|
allOthersOffCondition = allOthersOffCondition.And(otherParam.IsFalse());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (model.exclusiveOffState && param != null) {
|
|
var layer = fx.NewLayer(model.name + " - Off Trigger");
|
|
var off = layer.NewState("Idle");
|
|
var on = layer.NewState("Trigger");
|
|
off.TransitionsTo(on).When(allOthersOffCondition);
|
|
on.TransitionsTo(off).When(allOthersOffCondition.Not().Or(param.IsFalse()));
|
|
on.Drives(param, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method is needed, because:
|
|
* 1. If you clip.SampleAnimation on the avatar while it has a humanoid Avatar set on its Animator, it'll
|
|
* bake into motorcycle pose.
|
|
* 2. If you change the avatar or controller on the Animator, the Animator will reset all transforms of all
|
|
* children objects back to the way they were at the start of the frame.
|
|
* Only destroying the animator then recreating it seems to "reset" this "start of frame" state.
|
|
*/
|
|
public static void WithoutAnimator(GameObject obj, System.Action func) {
|
|
var animator = obj.GetComponent<Animator>();
|
|
if (!animator) {
|
|
func();
|
|
return;
|
|
}
|
|
|
|
var controller = animator.runtimeAnimatorController;
|
|
var avatar = animator.avatar;
|
|
var applyRootMotion = animator.applyRootMotion;
|
|
var updateMode = animator.updateMode;
|
|
var cullingMode = animator.cullingMode;
|
|
Object.DestroyImmediate(animator);
|
|
animator = obj.AddComponent<Animator>();
|
|
animator.applyRootMotion = applyRootMotion;
|
|
animator.updateMode = updateMode;
|
|
animator.cullingMode = cullingMode;
|
|
func();
|
|
animator.runtimeAnimatorController = controller;
|
|
animator.avatar = avatar;
|
|
}
|
|
|
|
[FeatureBuilderAction(FeatureOrder.ApplyToggleRestingState)]
|
|
public void ApplyRestingState() {
|
|
if (restingClip != null) {
|
|
WithoutAnimator(avatarObject, () => { restingClip.SampleAnimation(avatarObject, 0); });
|
|
}
|
|
}
|
|
|
|
public override string GetEditorTitle() {
|
|
return "Toggle";
|
|
}
|
|
|
|
public override VisualElement CreateEditor(SerializedProperty prop) {
|
|
return CreateEditor(prop, content => content.Add(VRCFuryStateEditor.render(prop.FindPropertyRelative("state"))));
|
|
}
|
|
|
|
private static VisualElement CreateEditor(SerializedProperty prop, Action<VisualElement> renderBody) {
|
|
var content = new VisualElement();
|
|
|
|
var savedProp = prop.FindPropertyRelative("saved");
|
|
var sliderProp = prop.FindPropertyRelative("slider");
|
|
var securityEnabledProp = prop.FindPropertyRelative("securityEnabled");
|
|
var defaultOnProp = prop.FindPropertyRelative("defaultOn");
|
|
var includeInRestProp = prop.FindPropertyRelative("includeInRest");
|
|
var exclusiveOffStateProp = prop.FindPropertyRelative("exclusiveOffState");
|
|
var enableExclusiveTagProp = prop.FindPropertyRelative("enableExclusiveTag");
|
|
var resetPhysboneProp = prop.FindPropertyRelative("resetPhysbones");
|
|
var enableIconProp = prop.FindPropertyRelative("enableIcon");
|
|
var enableDriveGlobalParamProp = prop.FindPropertyRelative("enableDriveGlobalParam");
|
|
var separateLocalProp = prop.FindPropertyRelative("separateLocal");
|
|
var hasTransitionProp = prop.FindPropertyRelative("hasTransition");
|
|
var simpleOutTransitionProp = prop.FindPropertyRelative("simpleOutTransition");
|
|
var defaultSliderProp = prop.FindPropertyRelative("defaultSliderValue");
|
|
|
|
var flex = new VisualElement {
|
|
style = {
|
|
flexDirection = FlexDirection.Row,
|
|
alignItems = Align.FlexStart
|
|
}
|
|
};
|
|
content.Add(flex);
|
|
|
|
var name = VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("name"), "Menu Path");
|
|
name.style.flexGrow = 1;
|
|
flex.Add(name);
|
|
|
|
var button = VRCFuryEditorUtils.Button("Options", () => {
|
|
var advMenu = new GenericMenu();
|
|
if (savedProp != null) {
|
|
advMenu.AddItem(new GUIContent("Saved Between Worlds"), savedProp.boolValue, () => {
|
|
savedProp.boolValue = !savedProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
if (sliderProp != null) {
|
|
advMenu.AddItem(new GUIContent("Use Slider Wheel"), sliderProp.boolValue, () => {
|
|
sliderProp.boolValue = !sliderProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
if (securityEnabledProp != null) {
|
|
advMenu.AddItem(new GUIContent("Protect with Security"), securityEnabledProp.boolValue, () => {
|
|
securityEnabledProp.boolValue = !securityEnabledProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
if (defaultOnProp != null) {
|
|
advMenu.AddItem(new GUIContent("Default On"), defaultOnProp.boolValue, () => {
|
|
defaultOnProp.boolValue = !defaultOnProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
if (includeInRestProp != null) {
|
|
advMenu.AddItem(new GUIContent("Show in Rest Pose"), includeInRestProp.boolValue, () => {
|
|
includeInRestProp.boolValue = !includeInRestProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
if (resetPhysboneProp != null) {
|
|
advMenu.AddItem(new GUIContent("Add PhysBone to Reset"), false, () => {
|
|
VRCFuryEditorUtils.AddToList(resetPhysboneProp);
|
|
});
|
|
}
|
|
|
|
if (enableExclusiveTagProp != null) {
|
|
advMenu.AddItem(new GUIContent("Enable Exclusive Tags"), enableExclusiveTagProp.boolValue, () => {
|
|
enableExclusiveTagProp.boolValue = !enableExclusiveTagProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
if (exclusiveOffStateProp != null) {
|
|
advMenu.AddItem(new GUIContent("This is Exclusive Off State"), exclusiveOffStateProp.boolValue, () => {
|
|
exclusiveOffStateProp.boolValue = !exclusiveOffStateProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
if (enableIconProp != null) {
|
|
advMenu.AddItem(new GUIContent("Set Custom Menu Icon"), enableIconProp.boolValue, () => {
|
|
enableIconProp.boolValue = !enableIconProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
if (enableDriveGlobalParamProp != null) {
|
|
advMenu.AddItem(new GUIContent("Drive a Global Parameter"), enableDriveGlobalParamProp.boolValue, () => {
|
|
enableDriveGlobalParamProp.boolValue = !enableDriveGlobalParamProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
if (separateLocalProp != null)
|
|
{
|
|
advMenu.AddItem(new GUIContent("Separate Local State"), separateLocalProp.boolValue, () => {
|
|
separateLocalProp.boolValue = !separateLocalProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
if (hasTransitionProp != null)
|
|
{
|
|
advMenu.AddItem(new GUIContent("Enable Transition State"), hasTransitionProp.boolValue, () => {
|
|
hasTransitionProp.boolValue = !hasTransitionProp.boolValue;
|
|
prop.serializedObject.ApplyModifiedProperties();
|
|
});
|
|
}
|
|
|
|
advMenu.ShowAsContext();
|
|
});
|
|
button.style.flexGrow = 0;
|
|
flex.Add(button);
|
|
|
|
renderBody(content);
|
|
|
|
if (resetPhysboneProp != null) {
|
|
content.Add(VRCFuryEditorUtils.RefreshOnChange(() => {
|
|
var c = new VisualElement();
|
|
if (resetPhysboneProp.arraySize > 0) {
|
|
c.Add(VRCFuryEditorUtils.WrappedLabel("Reset PhysBones:"));
|
|
c.Add(VRCFuryEditorUtils.List(prop.FindPropertyRelative("resetPhysbones")));
|
|
}
|
|
return c;
|
|
}, resetPhysboneProp));
|
|
}
|
|
|
|
if (enableExclusiveTagProp != null) {
|
|
content.Add(VRCFuryEditorUtils.RefreshOnChange(() => {
|
|
var c = new VisualElement();
|
|
if (enableExclusiveTagProp.boolValue) {
|
|
c.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("exclusiveTag"), "Exclusive Tags"));
|
|
}
|
|
return c;
|
|
}, enableExclusiveTagProp));
|
|
}
|
|
|
|
content.Add(VRCFuryEditorUtils.RefreshOnChange(() => {
|
|
var c = new VisualElement();
|
|
if (sliderProp.boolValue && defaultOnProp.boolValue) {
|
|
c.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("defaultSliderValue"), "Default Value"));
|
|
}
|
|
return c;
|
|
}, sliderProp, defaultOnProp));
|
|
|
|
if (enableIconProp != null) {
|
|
content.Add(VRCFuryEditorUtils.RefreshOnChange(() => {
|
|
var c = new VisualElement();
|
|
if (enableIconProp.boolValue) {
|
|
c.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("icon"), "Menu Icon"));
|
|
}
|
|
return c;
|
|
}, enableIconProp));
|
|
}
|
|
|
|
if (enableDriveGlobalParamProp != null) {
|
|
content.Add(VRCFuryEditorUtils.RefreshOnChange(() => {
|
|
var c = new VisualElement();
|
|
if (enableDriveGlobalParamProp.boolValue) {
|
|
c.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("driveGlobalParam"), "Drive Global Param"));
|
|
c.Add(VRCFuryEditorUtils.Warn(
|
|
"Warning, Drive Global Param is an advanced feature. The driven parameter should not be placed in a menu " +
|
|
"or controlled by any other driver or shared with any other toggle. It should only be used as an input to " +
|
|
"manually-created state transitions in your avatar. This should NEVER be used on vrcfury props, as any merged " +
|
|
"full controllers will have their parameters rewritten."));
|
|
}
|
|
return c;
|
|
}, enableDriveGlobalParamProp));
|
|
}
|
|
|
|
if (separateLocalProp != null)
|
|
{
|
|
content.Add(VRCFuryEditorUtils.RefreshOnChange(() => {
|
|
var c = new VisualElement();
|
|
if (separateLocalProp.boolValue)
|
|
{
|
|
c.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("localState"), "Local State"));
|
|
}
|
|
return c;
|
|
}, separateLocalProp));
|
|
}
|
|
|
|
content.Add(VRCFuryEditorUtils.RefreshOnChange(() => {
|
|
var c = new VisualElement();
|
|
if (hasTransitionProp.boolValue)
|
|
{
|
|
c.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("transitionStateIn"), "Transition In"));
|
|
|
|
if (!simpleOutTransitionProp.boolValue)
|
|
c.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("transitionStateOut"), "Transition Out"));
|
|
}
|
|
return c;
|
|
}, hasTransitionProp, simpleOutTransitionProp));
|
|
|
|
content.Add(VRCFuryEditorUtils.RefreshOnChange(() => {
|
|
var c = new VisualElement();
|
|
if (separateLocalProp.boolValue && hasTransitionProp.boolValue)
|
|
{
|
|
c.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("localTransitionStateIn"), "Local Trans. In"));
|
|
|
|
if (!simpleOutTransitionProp.boolValue)
|
|
c.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("localTransitionStateOut"), "Local Trans. Out"));
|
|
|
|
}
|
|
return c;
|
|
}, separateLocalProp, hasTransitionProp, simpleOutTransitionProp));
|
|
|
|
content.Add(VRCFuryEditorUtils.RefreshOnChange(() => {
|
|
var c = new VisualElement();
|
|
if (hasTransitionProp.boolValue)
|
|
{
|
|
c.Add(VRCFuryEditorUtils.Prop(prop.FindPropertyRelative("simpleOutTransition"), "Transition Out is reverse of Transition In"));
|
|
}
|
|
return c;
|
|
}, hasTransitionProp));
|
|
|
|
// Tags
|
|
content.Add(VRCFuryEditorUtils.RefreshOnChange(() => {
|
|
var tags = new List<string>();
|
|
if (savedProp != null && savedProp.boolValue)
|
|
tags.Add("Saved");
|
|
if (sliderProp != null && sliderProp.boolValue)
|
|
tags.Add("Slider");
|
|
if (securityEnabledProp != null && securityEnabledProp.boolValue)
|
|
tags.Add("Security");
|
|
if (defaultOnProp != null && defaultOnProp.boolValue)
|
|
tags.Add("Default On");
|
|
if (includeInRestProp != null && includeInRestProp.boolValue)
|
|
tags.Add("Shown in Rest Pose");
|
|
if (exclusiveOffStateProp != null && exclusiveOffStateProp.boolValue)
|
|
tags.Add("This is the Exclusive Off State");
|
|
|
|
var row = new VisualElement();
|
|
row.style.flexWrap = Wrap.Wrap;
|
|
row.style.flexDirection = FlexDirection.Row;
|
|
foreach (var tag in tags) {
|
|
var flag = new Label(tag);
|
|
flag.style.width = StyleKeyword.Auto;
|
|
flag.style.backgroundColor = new Color(1f, 1f, 1f, 0.1f);
|
|
flag.style.borderTopRightRadius = 5;
|
|
flag.style.marginRight = 5;
|
|
VRCFuryEditorUtils.Padding(flag, 2, 4);
|
|
row.Add(flag);
|
|
}
|
|
|
|
return row;
|
|
},
|
|
savedProp,
|
|
sliderProp,
|
|
securityEnabledProp,
|
|
defaultOnProp,
|
|
includeInRestProp,
|
|
exclusiveOffStateProp
|
|
));
|
|
|
|
return content;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|