331 lines
17 KiB
C#
331 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using UnityEditor;
|
|
using UnityEditor.Animations;
|
|
using UnityEngine;
|
|
using VF.Builder;
|
|
using VF.Builder.Exceptions;
|
|
using VF.Builder.Ogb;
|
|
using VF.Feature.Base;
|
|
using VF.Inspector;
|
|
using VF.Model;
|
|
using VF.Model.Feature;
|
|
using VF.Model.StateAction;
|
|
using VRC.Dynamics;
|
|
using VRC.SDK3.Dynamics.Contact.Components;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace VF.Feature {
|
|
public class BakeOGBBuilder : FeatureBuilder {
|
|
|
|
[FeatureBuilderAction(FeatureOrder.BakeOgbComponents)]
|
|
public void Apply() {
|
|
var usedNames = new List<string>();
|
|
var fakeHead = allBuildersInRun.OfType<FakeHeadBuilder>().First();
|
|
|
|
// When you first load into a world, contact receivers already touching a sender register as 0 proximity
|
|
// until they are removed and then reintroduced to each other.
|
|
var objectsToDisableTemporarily = new HashSet<GameObject>();
|
|
|
|
foreach (var c in avatarObject.GetComponentsInChildren<OGBPenetrator>(true)) {
|
|
var bakeInfo = OGBPenetratorEditor.Bake(c, usedNames);
|
|
|
|
if (c.configureTps) {
|
|
if (bakeInfo == null) {
|
|
throw new VRCFBuilderException("Failed to get size of penetrator to configure TPS");
|
|
}
|
|
var (name, bakeRoot, renderers, worldLength, worldRadius) = bakeInfo;
|
|
|
|
var configuredOne = false;
|
|
foreach (var renderer in renderers) {
|
|
var newRenderer = TpsConfigurer.ConfigureRenderer(renderer, bakeRoot.transform, tmpDir, worldLength);
|
|
if (newRenderer) {
|
|
configuredOne = true;
|
|
addOtherFeature(new BoundingBoxFix2() { singleRenderer = newRenderer });
|
|
}
|
|
}
|
|
|
|
if (!configuredOne) {
|
|
throw new VRCFBuilderException(
|
|
"OGB Penetrator has 'auto-configure TPS' enabled, but no renderer was found " +
|
|
"using Poiyomi Pro 8.1+ with the 'Penetrator' feature enabled in the Color & Normals tab.");
|
|
}
|
|
}
|
|
|
|
if (bakeInfo != null) {
|
|
var (name, bakeRoot, renderer, worldLength, worldRadius) = bakeInfo;
|
|
foreach (var r in bakeRoot.GetComponentsInChildren<VRCContactReceiver>(true)) {
|
|
objectsToDisableTemporarily.Add(r.gameObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
var holesMenu = "Holes";
|
|
var optionsFolder = $"{holesMenu}/<b>Hole Options";
|
|
|
|
var enableAuto = avatarObject.GetComponentsInChildren<OGBOrifice>(true)
|
|
.Where(o => o.addMenuItem && o.enableAuto)
|
|
.ToArray()
|
|
.Length >= 2;
|
|
VFABool autoOn = null;
|
|
AnimationClip autoOnClip = null;
|
|
if (enableAuto) {
|
|
autoOn = GetFx().NewBool("autoMode", synced: true);
|
|
manager.GetMenu().NewMenuToggle($"{optionsFolder}/<b>Auto Mode<\\/b>\n<size=20>Activates hole nearest to an OGB penetrator", autoOn);
|
|
autoOnClip = manager.GetClipStorage().NewClip("EnableAutoReceivers");
|
|
var autoReceiverLayer = GetFx().NewLayer("Auto - Enable Receivers");
|
|
var off = autoReceiverLayer.NewState("Off");
|
|
var on = autoReceiverLayer.NewState("On").WithAnimation(autoOnClip);
|
|
var whenOn = autoOn.IsTrue().And(GetFx().IsLocal().IsTrue());
|
|
off.TransitionsTo(on).When(whenOn);
|
|
on.TransitionsTo(off).When(whenOn.Not());
|
|
}
|
|
|
|
var enableStealth = avatarObject.GetComponentsInChildren<OGBOrifice>(true)
|
|
.Where(o => o.addMenuItem)
|
|
.ToArray()
|
|
.Length >= 1;
|
|
VFABool stealthOn = null;
|
|
if (enableStealth) {
|
|
stealthOn = GetFx().NewBool("stealth", synced: true);
|
|
manager.GetMenu().NewMenuToggle($"{optionsFolder}/<b>Stealth Mode<\\/b>\n<size=20>Only local haptics,\nInvisible to others", stealthOn);
|
|
}
|
|
|
|
var enableMulti = avatarObject.GetComponentsInChildren<OGBOrifice>(true)
|
|
.Where(o => o.addMenuItem)
|
|
.ToArray()
|
|
.Length >= 2;
|
|
VFABool multiOn = null;
|
|
if (enableMulti) {
|
|
multiOn = GetFx().NewBool("multi", synced: true);
|
|
var multiFolder = $"{optionsFolder}/<b>Dual Mode<\\/b>\n<size=20>Allows 2 active holes";
|
|
manager.GetMenu().NewMenuToggle($"{multiFolder}/Enable Dual Mode", multiOn);
|
|
manager.GetMenu().NewMenuButton($"{multiFolder}/<b>WARNING<\\/b>\n<size=20>Everyone else must use TPS, >NO DPS!<");
|
|
manager.GetMenu().NewMenuButton($"{multiFolder}/<b>WARNING<\\/b>\n<size=20>Nobody else can use a hole at the same time");
|
|
manager.GetMenu().NewMenuButton($"{multiFolder}/<b>WARNING<\\/b>\n<size=20>DO NOT ENABLE MORE THAN 2");
|
|
}
|
|
|
|
manager.GetMenu().SetIconGuid(optionsFolder, "16e0846165acaa1429417e757c53ef9b");
|
|
|
|
var autoOrifices = new List<Tuple<string, VFABool, VFANumber>>();
|
|
var exclusiveTriggers = new List<Tuple<VFABool, VFAState>>();
|
|
foreach (var c in avatarObject.GetComponentsInChildren<OGBOrifice>(true)) {
|
|
fakeHead.MarkEligible(c.gameObject);
|
|
var (name,bakeRoot) = OGBOrificeEditor.Bake(c, usedNames);
|
|
|
|
foreach (var r in bakeRoot.GetComponentsInChildren<VRCContactReceiver>(true)) {
|
|
objectsToDisableTemporarily.Add(r.gameObject);
|
|
}
|
|
|
|
if (c.addMenuItem) {
|
|
c.gameObject.SetActive(true);
|
|
|
|
ICollection<GameObject> FindChildren(params string[] names) {
|
|
return names.Select(n => bakeRoot.transform.Find(n))
|
|
.Where(t => t != null)
|
|
.Select(t => t.gameObject)
|
|
.ToArray();
|
|
}
|
|
|
|
foreach (var obj in FindChildren("Senders", "Receivers", "Lights", "VersionLocal", "VersionBeacon")) {
|
|
obj.SetActive(false);
|
|
}
|
|
var onLocalClip = manager.GetClipStorage().NewClip($"{name}_onLocal");
|
|
foreach (var obj in FindChildren("Senders", "Receivers", "Lights", "VersionLocal")) {
|
|
clipBuilder.Enable(onLocalClip, obj);
|
|
}
|
|
var onRemoteClip = manager.GetClipStorage().NewClip($"{name}_onRemote");
|
|
foreach (var obj in FindChildren("Senders", "Lights", "VersionBeacon")) {
|
|
clipBuilder.Enable(onRemoteClip, obj);
|
|
}
|
|
var onStealthClip = manager.GetClipStorage().NewClip($"{name}_stealth");
|
|
foreach (var obj in FindChildren("Receivers", "VersionLocal")) {
|
|
clipBuilder.Enable(onStealthClip, obj);
|
|
}
|
|
|
|
var holeOn = GetFx().NewBool(name, synced: true);
|
|
manager.GetMenu().NewMenuToggle($"Holes/{name}", holeOn);
|
|
|
|
var layer = GetFx().NewLayer(name);
|
|
var offState = layer.NewState("Off");
|
|
var stealthState = layer.NewState("On Local Stealth").WithAnimation(onStealthClip).Move(offState, 1, 0);
|
|
var onLocalMultiState = layer.NewState("On Local Multi").WithAnimation(onLocalClip);
|
|
var onLocalState = layer.NewState("On Local").WithAnimation(onLocalClip);
|
|
var onRemoteState = layer.NewState("On Remote").WithAnimation(onRemoteClip);
|
|
|
|
var whenOn = holeOn.IsTrue();
|
|
var whenLocal = GetFx().IsLocal().IsTrue();
|
|
var whenStealthEnabled = stealthOn?.IsTrue() ?? GetFx().Never();
|
|
var whenMultiEnabled = multiOn?.IsTrue() ?? GetFx().Never();
|
|
|
|
VFAState.FakeAnyState(
|
|
(stealthState, whenOn.And(whenLocal.And(whenStealthEnabled))),
|
|
(onLocalMultiState, whenOn.And(whenLocal.And(whenMultiEnabled))),
|
|
(onLocalState, whenOn.And(whenLocal)),
|
|
(onRemoteState, whenOn.And(whenStealthEnabled.Not())),
|
|
(offState, GetFx().Always())
|
|
);
|
|
|
|
exclusiveTriggers.Add(Tuple.Create(holeOn, onLocalState));
|
|
|
|
if (c.enableAuto && autoOnClip) {
|
|
var distParam = GetFx().NewFloat(name + "/AutoDistance");
|
|
var distReceiver = OGBUtils.AddReceiver(bakeRoot, Vector3.zero, distParam.Name(), "AutoDistance", 0.3f,
|
|
new[] { OGBUtils.CONTACT_PEN_MAIN });
|
|
distReceiver.SetActive(false);
|
|
clipBuilder.Enable(autoOnClip, distReceiver);
|
|
autoOrifices.Add(Tuple.Create(name, holeOn, distParam));
|
|
}
|
|
}
|
|
|
|
var actionNum = 0;
|
|
foreach (var depthAction in c.depthActions) {
|
|
actionNum++;
|
|
var prefix = name + actionNum;
|
|
if (depthAction.state == null || depthAction.state.IsEmpty()) continue;
|
|
|
|
var minDepth = depthAction.minDepth;
|
|
|
|
var maxDepth = depthAction.maxDepth;
|
|
if (maxDepth <= minDepth) maxDepth = 0.25f;
|
|
if (maxDepth <= minDepth) continue;
|
|
|
|
var length = maxDepth - minDepth;
|
|
|
|
var fx = GetFx();
|
|
|
|
var contactingRootParam = fx.NewBool(prefix + "/AnimContacting");
|
|
OGBUtils.AddReceiver(bakeRoot, Vector3.forward * -minDepth, contactingRootParam.Name(), "AnimRoot" + actionNum, 0.01f, new []{OGBUtils.CONTACT_PEN_MAIN}, allowSelf:depthAction.enableSelf, type: ContactReceiver.ReceiverType.Constant);
|
|
|
|
var depthParam = fx.NewFloat(prefix + "/AnimDepth");
|
|
OGBUtils.AddReceiver(bakeRoot, Vector3.forward * -(minDepth + length), depthParam.Name(), "AnimInside" + actionNum, length, new []{OGBUtils.CONTACT_PEN_MAIN}, allowSelf:depthAction.enableSelf);
|
|
|
|
var layer = fx.NewLayer("Depth Animation " + actionNum + " for " + name);
|
|
var off = layer.NewState("Off");
|
|
var on = layer.NewState("On");
|
|
|
|
var clip = LoadState(prefix, depthAction.state);
|
|
if (ClipBuilder.IsStaticMotion(clip)) {
|
|
var tree = manager.GetClipStorage().NewBlendTree(prefix + " tree");
|
|
tree.blendType = BlendTreeType.Simple1D;
|
|
tree.useAutomaticThresholds = false;
|
|
tree.blendParameter = depthParam.Name();
|
|
tree.AddChild(manager.GetClipStorage().GetNoopClip(), 0);
|
|
tree.AddChild(clip, 1);
|
|
on.WithAnimation(tree);
|
|
} else {
|
|
on.WithAnimation(clip).MotionTime(depthParam);
|
|
}
|
|
|
|
var onWhen = depthParam.IsGreaterThan(0).And(contactingRootParam.IsTrue());
|
|
off.TransitionsTo(on).When(onWhen);
|
|
on.TransitionsTo(off).When(onWhen.Not());
|
|
}
|
|
}
|
|
|
|
foreach (var i in Enumerable.Range(0, exclusiveTriggers.Count)) {
|
|
var (_, state) = exclusiveTriggers[i];
|
|
foreach (var j in Enumerable.Range(0, exclusiveTriggers.Count)) {
|
|
if (i == j) continue;
|
|
var (param, _) = exclusiveTriggers[j];
|
|
state.Drives(param, false);
|
|
}
|
|
}
|
|
|
|
if (autoOn != null) {
|
|
var fx = GetFx();
|
|
var layer = fx.NewLayer("Auto OGB Mode");
|
|
var remoteTrap = layer.NewState("Remote trap");
|
|
var stopped = layer.NewState("Stopped");
|
|
remoteTrap.TransitionsTo(stopped).When(fx.IsLocal().IsTrue());
|
|
var start = layer.NewState("Start").Move(stopped, 1, 0);
|
|
stopped.TransitionsTo(start).When(autoOn.IsTrue());
|
|
var stop = layer.NewState("Stop").Move(start, 1, 0);
|
|
start.TransitionsTo(stop).When(autoOn.IsFalse());
|
|
foreach (var auto in autoOrifices) {
|
|
var (name, enabled, dist) = auto;
|
|
stop.Drives(enabled, false);
|
|
}
|
|
stop.TransitionsTo(stopped).When(fx.Always());
|
|
|
|
var vsParam = fx.NewFloat("comparison");
|
|
var vs1 = manager.GetClipStorage().NewClip("vs1");
|
|
vs1.SetCurve("", typeof(Animator), vsParam.Name(), AnimationCurve.Constant(0, 0, 1f));
|
|
var vs0 = manager.GetClipStorage().NewClip("vs0");
|
|
vs0.SetCurve("", typeof(Animator), vsParam.Name(), AnimationCurve.Constant(0, 0, 0f));
|
|
|
|
var states = new Dictionary<Tuple<int, int>, VFAState>();
|
|
for (var i = 0; i < autoOrifices.Count; i++) {
|
|
var (aName, aEnabled, aDist) = autoOrifices[i];
|
|
var triggerOn = layer.NewState($"Start {aName}").Move(start, i, 2);
|
|
triggerOn.Drives(aEnabled, true);
|
|
states[Tuple.Create(i,-1)] = triggerOn;
|
|
var triggerOff = layer.NewState($"Stop {aName}");
|
|
triggerOff.Drives(aEnabled, false);
|
|
triggerOff.TransitionsTo(start).When(fx.Always());
|
|
states[Tuple.Create(i,-2)] = triggerOff;
|
|
for (var j = 0; j < autoOrifices.Count; j++) {
|
|
if (i == j) continue;
|
|
var (bName, bEnabled, bDist) = autoOrifices[j];
|
|
var vs = layer.NewState($"{aName} vs {bName}").Move(triggerOff, 0, j+1);
|
|
var tree = manager.GetClipStorage().NewBlendTree($"{aName} vs {bName}");
|
|
tree.useAutomaticThresholds = false;
|
|
tree.blendType = BlendTreeType.FreeformCartesian2D;
|
|
tree.AddChild(vs0, new Vector2(1f, 0));
|
|
tree.AddChild(vs1, new Vector2(0, 1f));
|
|
tree.blendParameter = aDist.Name();
|
|
tree.blendParameterY = bDist.Name();
|
|
vs.WithAnimation(tree);
|
|
states[Tuple.Create(i,j)] = vs;
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < autoOrifices.Count; i++) {
|
|
var (name, enabled, dist) = autoOrifices[i];
|
|
var triggerOn = states[Tuple.Create(i, -1)];
|
|
var triggerOff = states[Tuple.Create(i, -2)];
|
|
var firstComparison = states[Tuple.Create(i, i == 0 ? 1 : 0)];
|
|
start.TransitionsTo(firstComparison).When(enabled.IsTrue());
|
|
triggerOn.TransitionsTo(firstComparison).When(fx.Always());
|
|
|
|
for (var j = 0; j < autoOrifices.Count; j++) {
|
|
if (i == j) continue;
|
|
var current = states[Tuple.Create(i, j)];
|
|
var otherActivate = states[Tuple.Create(j, -1)];
|
|
|
|
current.TransitionsTo(otherActivate).When(vsParam.IsGreaterThan(0.51f));
|
|
|
|
var nextI = j + 1;
|
|
if (nextI == i) nextI++;
|
|
if (nextI == autoOrifices.Count) {
|
|
current.TransitionsTo(triggerOff).When(dist.IsGreaterThan(0).Not());
|
|
current.TransitionsTo(start).When(fx.Always());
|
|
} else {
|
|
var next = states[Tuple.Create(i, nextI)];
|
|
current.TransitionsTo(next).When(fx.Always());
|
|
}
|
|
}
|
|
}
|
|
|
|
start.TransitionsTo(states[Tuple.Create(0, 1)]).When(fx.Always());
|
|
}
|
|
|
|
if (objectsToDisableTemporarily.Count > 0) {
|
|
var fx = GetFx();
|
|
var layer = fx.NewLayer("OGB Off Temporarily Upon Load");
|
|
var off = layer.NewState("Off");
|
|
var on = layer.NewState("On");
|
|
off.TransitionsTo(on).When().WithTransitionExitTime(1);
|
|
|
|
var clip = manager.GetClipStorage().NewClip("ogbLoad");
|
|
foreach (var obj in objectsToDisableTemporarily) {
|
|
clipBuilder.Enable(clip, obj, false);
|
|
}
|
|
off.WithAnimation(clip);
|
|
}
|
|
}
|
|
}
|
|
}
|