mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-02 14:29:25 +00:00
[AvatarScaleMod] Mod Network attempt 2.
I lost the first attempt when switching branches. Lot of this still needs to be redone, as it is just quickly slapped together atm.
This commit is contained in:
parent
69cb3b7804
commit
d599f57973
13 changed files with 850 additions and 544 deletions
|
@ -1,67 +0,0 @@
|
||||||
using ABI_RC.Core.Savior;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace NAK.AvatarScaleMod;
|
|
||||||
|
|
||||||
public static class AvatarScaleGesture
|
|
||||||
{
|
|
||||||
// Toggle for scale gesture
|
|
||||||
public static bool GestureEnabled;
|
|
||||||
|
|
||||||
// Require triggers to be down while doing fist? - Exteratta
|
|
||||||
public static bool RequireTriggers = true;
|
|
||||||
|
|
||||||
// Initial values when scale gesture is started
|
|
||||||
public static float InitialModifier;
|
|
||||||
public static float InitialTargetHeight;
|
|
||||||
|
|
||||||
public static void OnScaleStart(float modifier, Transform transform1, Transform transform2)
|
|
||||||
{
|
|
||||||
if (!GestureEnabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (AvatarScaleManager.LocalAvatar != null)
|
|
||||||
{
|
|
||||||
// Store initial modifier so we can get difference later
|
|
||||||
InitialModifier = modifier;
|
|
||||||
InitialTargetHeight = AvatarScaleManager.LocalAvatar.TargetHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void OnScaleStay(float modifier, Transform transform1, Transform transform2)
|
|
||||||
{
|
|
||||||
if (!GestureEnabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Allow user to release triggers to reset "world grip"
|
|
||||||
if (RequireTriggers && !AreBothTriggersDown())
|
|
||||||
{
|
|
||||||
InitialModifier = modifier;
|
|
||||||
InitialTargetHeight = AvatarScaleManager.LocalAvatar.TargetHeight;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AvatarScaleManager.LocalAvatar != null)
|
|
||||||
{
|
|
||||||
// Invert so the gesture is more of a world squish instead of happy hug
|
|
||||||
float modifierRatio = 1f / (modifier / InitialModifier);
|
|
||||||
|
|
||||||
// Determine the adjustment factor for the height, this will be >1 if scaling up, <1 if scaling down.
|
|
||||||
float heightAdjustmentFactor = (modifierRatio > 1) ? 1 + (modifierRatio - 1) : 1 - (1 - modifierRatio);
|
|
||||||
|
|
||||||
// Apply the adjustment to the target height
|
|
||||||
AvatarScaleManager.LocalAvatar.SetTargetHeight(InitialTargetHeight * heightAdjustmentFactor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void OnScaleEnd(float modifier, Transform transform1, Transform transform2)
|
|
||||||
{
|
|
||||||
// Unused, needed for mod network?
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maybe it should be one trigger? Imagine XSOverlay scaling but for player.
|
|
||||||
public static bool AreBothTriggersDown()
|
|
||||||
{
|
|
||||||
return CVRInputManager.Instance.interactLeftValue > 0.75f && CVRInputManager.Instance.interactRightValue > 0.75f;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,342 +0,0 @@
|
||||||
using ABI.CCK.Components;
|
|
||||||
using ABI_RC.Core.Player;
|
|
||||||
using NAK.AvatarScaleMod.ScaledComponents;
|
|
||||||
using System.Collections;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.Animations;
|
|
||||||
|
|
||||||
namespace NAK.AvatarScaleMod;
|
|
||||||
|
|
||||||
public class AvatarScaleManager : MonoBehaviour
|
|
||||||
{
|
|
||||||
// Constants
|
|
||||||
public const float MinHeight = 0.1f; // TODO: Make into Setting
|
|
||||||
public const float MaxHeight = 10f; // TODO: Make into Setting
|
|
||||||
public const string ScaleFactorParameterName = "ScaleFactor";
|
|
||||||
public const string ScaleFactorParameterNameLocal = "#ScaleFactor";
|
|
||||||
|
|
||||||
private static readonly System.Type[] scalableComponentTypes =
|
|
||||||
{
|
|
||||||
typeof(Light),
|
|
||||||
typeof(AudioSource),
|
|
||||||
typeof(ParticleSystem),
|
|
||||||
typeof(ParentConstraint),
|
|
||||||
typeof(PositionConstraint),
|
|
||||||
typeof(ScaleConstraint)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Public properties
|
|
||||||
public static bool GlobalEnabled { get; set; }
|
|
||||||
public static AvatarScaleManager LocalAvatar { get; private set; }
|
|
||||||
|
|
||||||
public float TargetHeight { get; private set; }
|
|
||||||
public float InitialHeight { get; private set; }
|
|
||||||
public Vector3 InitialScale { get; private set; }
|
|
||||||
public float ScaleFactor { get; private set; }
|
|
||||||
|
|
||||||
// Private properties
|
|
||||||
private bool _isLocalAvatar;
|
|
||||||
private Animator _animator;
|
|
||||||
private CVRAvatar _avatar;
|
|
||||||
|
|
||||||
private List<ScaledLight> _scaledLights = new List<ScaledLight>();
|
|
||||||
private List<ScaledAudioSource> _scaledAudioSources = new List<ScaledAudioSource>();
|
|
||||||
private List<ScaledParentConstraint> _scaledParentConstraints = new List<ScaledParentConstraint>();
|
|
||||||
private List<ScaledPositionConstraint> _scaledPositionConstraints = new List<ScaledPositionConstraint>();
|
|
||||||
private List<ScaledScaleConstraint> _scaledScaleConstraints = new List<ScaledScaleConstraint>();
|
|
||||||
|
|
||||||
public void Initialize(float initialHeight, Vector3 initialScale, bool isLocalAvatar)
|
|
||||||
{
|
|
||||||
if (Math.Abs(initialHeight) < 1E-6)
|
|
||||||
{
|
|
||||||
AvatarScaleMod.Logger.Warning("Cannot initialize with a height of zero!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLocalAvatar && LocalAvatar == null)
|
|
||||||
{
|
|
||||||
_isLocalAvatar = true;
|
|
||||||
LocalAvatar = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.TargetHeight = initialHeight;
|
|
||||||
this.InitialHeight = initialHeight;
|
|
||||||
this.InitialScale = initialScale;
|
|
||||||
this.ScaleFactor = 1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void Start()
|
|
||||||
{
|
|
||||||
_avatar = GetComponent<CVRAvatar>();
|
|
||||||
|
|
||||||
if (_avatar == null)
|
|
||||||
{
|
|
||||||
AvatarScaleMod.Logger.Error("AvatarScaleManager should be attached to a GameObject with a CVRAvatar component.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_isLocalAvatar)
|
|
||||||
{
|
|
||||||
_animator = GetComponent<Animator>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// I am unsure if this reduces the hitch or not.
|
|
||||||
// I do not want to patch where the game already does scanning though.
|
|
||||||
await FindComponentsOfTypeAsync(scalableComponentTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
|
||||||
{
|
|
||||||
// TODO: Test with Avatar Distance Hider
|
|
||||||
ResetAllToInitialScale();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
ClearLists();
|
|
||||||
|
|
||||||
if (LocalAvatar == this)
|
|
||||||
{
|
|
||||||
LocalAvatar = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearLists()
|
|
||||||
{
|
|
||||||
_scaledAudioSources.Clear();
|
|
||||||
_scaledLights.Clear();
|
|
||||||
_scaledParentConstraints.Clear();
|
|
||||||
_scaledPositionConstraints.Clear();
|
|
||||||
_scaledScaleConstraints.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetTargetHeight(float newHeight)
|
|
||||||
{
|
|
||||||
TargetHeight = Mathf.Clamp(newHeight, MinHeight, MaxHeight);
|
|
||||||
UpdateScaleFactor();
|
|
||||||
UpdateAnimatorParameter();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetTargetHeightOverTime(float newHeight, float duration)
|
|
||||||
{
|
|
||||||
StartCoroutine(SetTargetHeightOverTimeCoroutine(newHeight, duration));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateScaleFactor()
|
|
||||||
{
|
|
||||||
if (Math.Abs(InitialHeight) < 1E-6)
|
|
||||||
{
|
|
||||||
AvatarScaleMod.Logger.Warning("InitialHeight is zero, cannot calculate ScaleFactor.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScaleFactor = TargetHeight / InitialHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAnimatorParameter()
|
|
||||||
{
|
|
||||||
if (_isLocalAvatar)
|
|
||||||
{
|
|
||||||
// Set synced and local parameters for Local Player
|
|
||||||
PlayerSetup.Instance.animatorManager.SetAnimatorParameter(ScaleFactorParameterName, ScaleFactor);
|
|
||||||
PlayerSetup.Instance.animatorManager.SetAnimatorParameter(ScaleFactorParameterNameLocal, ScaleFactor);
|
|
||||||
}
|
|
||||||
else if (_animator != null)
|
|
||||||
{
|
|
||||||
// Set local parameter for Remote Player
|
|
||||||
_animator.SetFloat(ScaleFactorParameterNameLocal, ScaleFactor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vector3 CalculateNewScale()
|
|
||||||
{
|
|
||||||
return InitialScale * ScaleFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator SetTargetHeightOverTimeCoroutine(float newHeight, float duration)
|
|
||||||
{
|
|
||||||
float startTime = Time.time;
|
|
||||||
float startHeight = TargetHeight;
|
|
||||||
|
|
||||||
// Clamping the newHeight to be between MinHeight and MaxHeight
|
|
||||||
newHeight = Mathf.Clamp(newHeight, MinHeight, MaxHeight);
|
|
||||||
|
|
||||||
while (Time.time < startTime + duration)
|
|
||||||
{
|
|
||||||
float t = (Time.time - startTime) / duration;
|
|
||||||
TargetHeight = Mathf.Lerp(startHeight, newHeight, t);
|
|
||||||
UpdateScaleFactor();
|
|
||||||
yield return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final setting of the TargetHeight after the loop is done.
|
|
||||||
TargetHeight = newHeight;
|
|
||||||
UpdateScaleFactor();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: actually profile this
|
|
||||||
private async Task FindComponentsOfTypeAsync(Type[] types)
|
|
||||||
{
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
var components = GetComponentsInChildren<Component>(true);
|
|
||||||
|
|
||||||
foreach (var component in components)
|
|
||||||
{
|
|
||||||
if (this == null) break;
|
|
||||||
if (component == null) continue;
|
|
||||||
|
|
||||||
tasks.Add(Task.Run(() =>
|
|
||||||
{
|
|
||||||
var componentType = component.GetType();
|
|
||||||
if (types.Contains(componentType))
|
|
||||||
{
|
|
||||||
AddScaledComponent(componentType, component);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddScaledComponent(Type type, Component component)
|
|
||||||
{
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case Type _ when type == typeof(AudioSource):
|
|
||||||
_scaledAudioSources.Add(new ScaledAudioSource((AudioSource)component));
|
|
||||||
break;
|
|
||||||
case Type _ when type == typeof(Light):
|
|
||||||
_scaledLights.Add(new ScaledLight((Light)component));
|
|
||||||
break;
|
|
||||||
case Type _ when type == typeof(ParentConstraint):
|
|
||||||
_scaledParentConstraints.Add(new ScaledParentConstraint((ParentConstraint)component));
|
|
||||||
break;
|
|
||||||
case Type _ when type == typeof(PositionConstraint):
|
|
||||||
_scaledPositionConstraints.Add(new ScaledPositionConstraint((PositionConstraint)component));
|
|
||||||
break;
|
|
||||||
case Type _ when type == typeof(ScaleConstraint):
|
|
||||||
_scaledScaleConstraints.Add(new ScaledScaleConstraint((ScaleConstraint)component));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
|
||||||
{
|
|
||||||
ApplyAvatarScaling();
|
|
||||||
ApplyComponentScaling();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LateUpdate()
|
|
||||||
{
|
|
||||||
ApplyAvatarScaling();
|
|
||||||
ApplyComponentScaling();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyAvatarScaling()
|
|
||||||
{
|
|
||||||
if (!GlobalEnabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
transform.localScale = CalculateNewScale();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyComponentScaling()
|
|
||||||
{
|
|
||||||
if (!GlobalEnabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
UpdateLightScales();
|
|
||||||
UpdateAudioSourceScales();
|
|
||||||
UpdateParentConstraintScales();
|
|
||||||
UpdatePositionConstraintScales();
|
|
||||||
UpdateScaleConstraintScales();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateLightScales()
|
|
||||||
{
|
|
||||||
// Update range of each light component
|
|
||||||
foreach (var light in _scaledLights)
|
|
||||||
{
|
|
||||||
light.Component.range = light.InitialRange * ScaleFactor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAudioSourceScales()
|
|
||||||
{
|
|
||||||
// Update min and max distance of each audio source component
|
|
||||||
foreach (var audioSource in _scaledAudioSources)
|
|
||||||
{
|
|
||||||
audioSource.Component.minDistance = audioSource.InitialMinDistance * ScaleFactor;
|
|
||||||
audioSource.Component.maxDistance = audioSource.InitialMaxDistance * ScaleFactor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateParentConstraintScales()
|
|
||||||
{
|
|
||||||
// Update translationAtRest and translationOffsets of each parent constraint component
|
|
||||||
foreach (var parentConstraint in _scaledParentConstraints)
|
|
||||||
{
|
|
||||||
parentConstraint.Component.translationAtRest = parentConstraint.InitialTranslationAtRest * ScaleFactor;
|
|
||||||
|
|
||||||
for (int i = 0; i < parentConstraint.InitialTranslationOffsets.Count; i++)
|
|
||||||
{
|
|
||||||
parentConstraint.Component.translationOffsets[i] = parentConstraint.InitialTranslationOffsets[i] * ScaleFactor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePositionConstraintScales()
|
|
||||||
{
|
|
||||||
// Update translationAtRest and translationOffset of each position constraint component
|
|
||||||
foreach (var positionConstraint in _scaledPositionConstraints)
|
|
||||||
{
|
|
||||||
positionConstraint.Component.translationAtRest = positionConstraint.InitialTranslationAtRest * ScaleFactor;
|
|
||||||
positionConstraint.Component.translationOffset = positionConstraint.InitialTranslationOffset * ScaleFactor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateScaleConstraintScales()
|
|
||||||
{
|
|
||||||
// Update scaleAtRest and scaleOffset of each scale constraint component
|
|
||||||
foreach (var scaleConstraint in _scaledScaleConstraints)
|
|
||||||
{
|
|
||||||
scaleConstraint.Component.scaleAtRest = scaleConstraint.InitialScaleAtRest * ScaleFactor;
|
|
||||||
scaleConstraint.Component.scaleOffset = scaleConstraint.InitialScaleOffset * ScaleFactor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetAllToInitialScale()
|
|
||||||
{
|
|
||||||
// Reset transform scale and each component to their initial scales
|
|
||||||
transform.localScale = InitialScale;
|
|
||||||
|
|
||||||
foreach (var light in _scaledLights)
|
|
||||||
{
|
|
||||||
light.Component.range = light.InitialRange;
|
|
||||||
}
|
|
||||||
foreach (var audioSource in _scaledAudioSources)
|
|
||||||
{
|
|
||||||
audioSource.Component.minDistance = audioSource.InitialMinDistance;
|
|
||||||
audioSource.Component.maxDistance = audioSource.InitialMaxDistance;
|
|
||||||
}
|
|
||||||
foreach (var parentConstraint in _scaledParentConstraints)
|
|
||||||
{
|
|
||||||
parentConstraint.Component.translationAtRest = parentConstraint.InitialTranslationAtRest;
|
|
||||||
|
|
||||||
for (int i = 0; i < parentConstraint.InitialTranslationOffsets.Count; i++)
|
|
||||||
{
|
|
||||||
parentConstraint.Component.translationOffsets[i] = parentConstraint.InitialTranslationOffsets[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (var positionConstraint in _scaledPositionConstraints)
|
|
||||||
{
|
|
||||||
positionConstraint.Component.translationAtRest = positionConstraint.InitialTranslationAtRest;
|
|
||||||
positionConstraint.Component.translationOffset = positionConstraint.InitialTranslationOffset;
|
|
||||||
}
|
|
||||||
foreach (var scaleConstraint in _scaledScaleConstraints)
|
|
||||||
{
|
|
||||||
scaleConstraint.Component.scaleAtRest = scaleConstraint.InitialScaleAtRest;
|
|
||||||
scaleConstraint.Component.scaleOffset = scaleConstraint.InitialScaleOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +1,2 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Microsoft.NET.Sdk"/>
|
<Project Sdk="Microsoft.NET.Sdk" />
|
||||||
|
|
112
AvatarScale/AvatarScaling/AvatarScaleManager.cs
Normal file
112
AvatarScale/AvatarScaling/AvatarScaleManager.cs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
using ABI_RC.Core.Player;
|
||||||
|
using NAK.AvatarScaleMod.Networking;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NAK.AvatarScaleMod.AvatarScaling;
|
||||||
|
|
||||||
|
public class AvatarScaleManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static AvatarScaleManager Instance;
|
||||||
|
|
||||||
|
private Dictionary<string, UniversalAvatarScaler> _networkedScalers;
|
||||||
|
private UniversalAvatarScaler _localAvatarScaler;
|
||||||
|
|
||||||
|
#region Unity Methods
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null)
|
||||||
|
{
|
||||||
|
DestroyImmediate(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instance = this;
|
||||||
|
_networkedScalers = new Dictionary<string, UniversalAvatarScaler>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Local Methods
|
||||||
|
|
||||||
|
public void OnAvatarInstantiated(PlayerSetup playerSetup)
|
||||||
|
{
|
||||||
|
if (playerSetup._avatar == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_localAvatarScaler != null)
|
||||||
|
Destroy(_localAvatarScaler);
|
||||||
|
|
||||||
|
_localAvatarScaler = playerSetup._avatar.AddComponent<UniversalAvatarScaler>();
|
||||||
|
_localAvatarScaler.Initialize(playerSetup._initialAvatarHeight, playerSetup.initialScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnAvatarDestroyed()
|
||||||
|
{
|
||||||
|
if (_localAvatarScaler != null)
|
||||||
|
Destroy(_localAvatarScaler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHeight(float targetHeight)
|
||||||
|
{
|
||||||
|
if (_localAvatarScaler == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_localAvatarScaler.SetHeight(targetHeight);
|
||||||
|
ModNetwork.SendNetworkHeight(targetHeight);
|
||||||
|
|
||||||
|
// immediately update play space scale
|
||||||
|
PlayerSetup.Instance.CheckUpdateAvatarScaleToPlaySpaceRelation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetHeight()
|
||||||
|
{
|
||||||
|
if (_localAvatarScaler != null)
|
||||||
|
_localAvatarScaler.ResetHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetHeight()
|
||||||
|
{
|
||||||
|
return (_localAvatarScaler != null) ? _localAvatarScaler.GetHeight() : -1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Network Methods
|
||||||
|
|
||||||
|
public void OnNetworkAvatarInstantiated(PuppetMaster puppetMaster)
|
||||||
|
{
|
||||||
|
if (puppetMaster.avatarObject == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string playerId = puppetMaster._playerDescriptor.ownerId;
|
||||||
|
|
||||||
|
if (_networkedScalers.ContainsKey(playerId))
|
||||||
|
_networkedScalers.Remove(playerId);
|
||||||
|
|
||||||
|
UniversalAvatarScaler scaler = puppetMaster.avatarObject.AddComponent<UniversalAvatarScaler>();
|
||||||
|
scaler.Initialize(puppetMaster._initialAvatarHeight, puppetMaster.initialAvatarScale);
|
||||||
|
_networkedScalers[playerId] = scaler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnNetworkAvatarDestroyed(string playerId)
|
||||||
|
{
|
||||||
|
if (_networkedScalers.ContainsKey(playerId))
|
||||||
|
_networkedScalers.Remove(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnNetworkHeightUpdateReceived(string playerId, float targetHeight)
|
||||||
|
{
|
||||||
|
if (_networkedScalers.TryGetValue(playerId, out UniversalAvatarScaler scaler))
|
||||||
|
scaler.SetHeight(targetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetNetworkHeight(string playerId)
|
||||||
|
{
|
||||||
|
if (_networkedScalers.TryGetValue(playerId, out UniversalAvatarScaler scaler))
|
||||||
|
return scaler.GetHeight();
|
||||||
|
return -1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
133
AvatarScale/AvatarScaling/ScaledComponents.cs
Normal file
133
AvatarScale/AvatarScaling/ScaledComponents.cs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Animations;
|
||||||
|
|
||||||
|
namespace NAK.AvatarScaleMod.ScaledComponents;
|
||||||
|
|
||||||
|
public class ScaledAudioSource
|
||||||
|
{
|
||||||
|
private readonly AudioSource Component;
|
||||||
|
private readonly float InitialMinDistance;
|
||||||
|
private readonly float InitialMaxDistance;
|
||||||
|
|
||||||
|
public ScaledAudioSource(AudioSource component)
|
||||||
|
{
|
||||||
|
Component = component;
|
||||||
|
InitialMinDistance = component.minDistance;
|
||||||
|
InitialMaxDistance = component.maxDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Scale(float scaleFactor)
|
||||||
|
{
|
||||||
|
Component.minDistance = InitialMinDistance * scaleFactor;
|
||||||
|
Component.maxDistance = InitialMaxDistance * scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Component.minDistance = InitialMinDistance;
|
||||||
|
Component.maxDistance = InitialMaxDistance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScaledLight
|
||||||
|
{
|
||||||
|
private readonly Light Component;
|
||||||
|
private readonly float InitialRange;
|
||||||
|
|
||||||
|
public ScaledLight(Light component)
|
||||||
|
{
|
||||||
|
Component = component;
|
||||||
|
InitialRange = component.range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Scale(float scaleFactor)
|
||||||
|
{
|
||||||
|
Component.range = InitialRange * scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Component.range = InitialRange;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScaledPositionConstraint
|
||||||
|
{
|
||||||
|
private readonly PositionConstraint Component;
|
||||||
|
private readonly Vector3 InitialTranslationAtRest;
|
||||||
|
private readonly Vector3 InitialTranslationOffset;
|
||||||
|
|
||||||
|
public ScaledPositionConstraint(PositionConstraint component)
|
||||||
|
{
|
||||||
|
Component = component;
|
||||||
|
InitialTranslationAtRest = component.translationAtRest;
|
||||||
|
InitialTranslationOffset = component.translationOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Scale(float scaleFactor)
|
||||||
|
{
|
||||||
|
Component.translationAtRest = InitialTranslationAtRest * scaleFactor;
|
||||||
|
Component.translationOffset = InitialTranslationOffset * scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Component.translationAtRest = InitialTranslationAtRest;
|
||||||
|
Component.translationOffset = InitialTranslationOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScaledParentConstraint
|
||||||
|
{
|
||||||
|
private readonly ParentConstraint Component;
|
||||||
|
private readonly Vector3 InitialTranslationAtRest;
|
||||||
|
private readonly List<Vector3> InitialTranslationOffsets;
|
||||||
|
|
||||||
|
public ScaledParentConstraint(ParentConstraint component)
|
||||||
|
{
|
||||||
|
Component = component;
|
||||||
|
InitialTranslationAtRest = component.translationAtRest;
|
||||||
|
InitialTranslationOffsets = component.translationOffsets.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Scale(float scaleFactor)
|
||||||
|
{
|
||||||
|
Component.translationAtRest = InitialTranslationAtRest * scaleFactor;
|
||||||
|
for (int i = 0; i < InitialTranslationOffsets.Count; i++)
|
||||||
|
Component.translationOffsets[i] = InitialTranslationOffsets[i] * scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Component.translationAtRest = InitialTranslationAtRest;
|
||||||
|
for (int i = 0; i < InitialTranslationOffsets.Count; i++)
|
||||||
|
Component.translationOffsets[i] = InitialTranslationOffsets[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScaledScaleConstraint
|
||||||
|
{
|
||||||
|
private readonly ScaleConstraint Component;
|
||||||
|
private readonly Vector3 InitialScaleAtRest;
|
||||||
|
private readonly Vector3 InitialScaleOffset;
|
||||||
|
|
||||||
|
public ScaledScaleConstraint(ScaleConstraint component)
|
||||||
|
{
|
||||||
|
Component = component;
|
||||||
|
InitialScaleAtRest = component.scaleAtRest;
|
||||||
|
InitialScaleOffset = component.scaleOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Scale(float scaleFactor)
|
||||||
|
{
|
||||||
|
Component.scaleAtRest = InitialScaleAtRest * scaleFactor;
|
||||||
|
Component.scaleOffset = InitialScaleOffset * scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Component.scaleAtRest = InitialScaleAtRest;
|
||||||
|
Component.scaleOffset = InitialScaleOffset;
|
||||||
|
}
|
||||||
|
}
|
231
AvatarScale/AvatarScaling/UniversalAvatarScaler.cs
Normal file
231
AvatarScale/AvatarScaling/UniversalAvatarScaler.cs
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
using ABI_RC.Core;
|
||||||
|
using ABI_RC.Core.Player;
|
||||||
|
using NAK.AvatarScaleMod.ScaledComponents;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Animations;
|
||||||
|
|
||||||
|
namespace NAK.AvatarScaleMod.AvatarScaling;
|
||||||
|
|
||||||
|
[DefaultExecutionOrder(-99999)] // before playersetup/puppetmaster but after animator
|
||||||
|
public class UniversalAvatarScaler : MonoBehaviour
|
||||||
|
{
|
||||||
|
#region Constants
|
||||||
|
|
||||||
|
// Universal Scaling Limits
|
||||||
|
private const float MinHeight = 0.1f;
|
||||||
|
private const float MaxHeight = 10f;
|
||||||
|
|
||||||
|
private const string ScaleFactorParameterName = "ScaleFactor";
|
||||||
|
private const string ScaleFactorParameterNameLocal = "#ScaleFactor";
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Variables
|
||||||
|
|
||||||
|
private CVRAnimatorManager _animatorManager;
|
||||||
|
|
||||||
|
private float _initialHeight;
|
||||||
|
private Vector3 _initialScale;
|
||||||
|
|
||||||
|
private float _targetHeight;
|
||||||
|
private float _scaleFactor = 1f;
|
||||||
|
|
||||||
|
private bool _isLocalAvatar;
|
||||||
|
private bool _heightWasUpdated;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Unity Methods
|
||||||
|
|
||||||
|
private async void Start()
|
||||||
|
{
|
||||||
|
await FindComponentsOfTypeAsync(scalableComponentTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LateUpdate()
|
||||||
|
{
|
||||||
|
ScaleAvatarRoot(); // override animation-based scaling
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Methods
|
||||||
|
|
||||||
|
public void Initialize(float initialHeight, Vector3 initialScale)
|
||||||
|
{
|
||||||
|
_initialHeight = _targetHeight = initialHeight;
|
||||||
|
_initialScale = initialScale;
|
||||||
|
_scaleFactor = 1f;
|
||||||
|
|
||||||
|
_isLocalAvatar = gameObject.layer == 8;
|
||||||
|
|
||||||
|
_animatorManager = _isLocalAvatar
|
||||||
|
? GetComponentInParent<PlayerSetup>().animatorManager
|
||||||
|
: GetComponentInParent<PuppetMaster>()._animatorManager;
|
||||||
|
|
||||||
|
_heightWasUpdated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHeight(float height)
|
||||||
|
{
|
||||||
|
_targetHeight = Mathf.Clamp(height, MinHeight, MaxHeight);
|
||||||
|
_scaleFactor = _targetHeight / _initialHeight;
|
||||||
|
_heightWasUpdated = true;
|
||||||
|
ApplyScaling();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetHeight()
|
||||||
|
{
|
||||||
|
_targetHeight = _initialHeight;
|
||||||
|
_scaleFactor = 1f;
|
||||||
|
_heightWasUpdated = true;
|
||||||
|
ApplyScaling();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetHeight()
|
||||||
|
{
|
||||||
|
return _targetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private Methods
|
||||||
|
|
||||||
|
private void ScaleAvatarRoot()
|
||||||
|
{
|
||||||
|
transform.localScale = _initialScale * _scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAnimatorParameter()
|
||||||
|
{
|
||||||
|
if (_animatorManager == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// synced parameter
|
||||||
|
if (_isLocalAvatar) _animatorManager.SetAnimatorParameter(ScaleFactorParameterName, _scaleFactor);
|
||||||
|
// local parameter
|
||||||
|
_animatorManager.SetAnimatorParameter(ScaleFactorParameterNameLocal, _scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyScaling()
|
||||||
|
{
|
||||||
|
if (!_heightWasUpdated)
|
||||||
|
return;
|
||||||
|
_heightWasUpdated = false;
|
||||||
|
|
||||||
|
ScaleAvatarRoot();
|
||||||
|
UpdateAnimatorParameter();
|
||||||
|
ApplyComponentScaling();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Component Scaling
|
||||||
|
|
||||||
|
private static readonly Type[] scalableComponentTypes =
|
||||||
|
{
|
||||||
|
typeof(Light),
|
||||||
|
typeof(AudioSource),
|
||||||
|
typeof(ParticleSystem),
|
||||||
|
typeof(ParentConstraint),
|
||||||
|
typeof(PositionConstraint),
|
||||||
|
typeof(ScaleConstraint)
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly List<ScaledLight> _scaledLights = new List<ScaledLight>();
|
||||||
|
private readonly List<ScaledAudioSource> _scaledAudioSources = new List<ScaledAudioSource>();
|
||||||
|
private readonly List<ScaledParentConstraint> _scaledParentConstraints = new List<ScaledParentConstraint>();
|
||||||
|
private readonly List<ScaledPositionConstraint> _scaledPositionConstraints = new List<ScaledPositionConstraint>();
|
||||||
|
private readonly List<ScaledScaleConstraint> _scaledScaleConstraints = new List<ScaledScaleConstraint>();
|
||||||
|
|
||||||
|
private async Task FindComponentsOfTypeAsync(Type[] types)
|
||||||
|
{
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
var components = GetComponentsInChildren<Component>(true);
|
||||||
|
|
||||||
|
foreach (Component component in components)
|
||||||
|
{
|
||||||
|
if (this == null) break;
|
||||||
|
if (component == null) continue;
|
||||||
|
|
||||||
|
tasks.Add(Task.Run(() =>
|
||||||
|
{
|
||||||
|
Type componentType = component.GetType();
|
||||||
|
if (types.Contains(componentType))
|
||||||
|
{
|
||||||
|
AddScaledComponent(componentType, component);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddScaledComponent(Type type, Component component)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case not null when type == typeof(AudioSource):
|
||||||
|
_scaledAudioSources.Add(new ScaledAudioSource((AudioSource)component));
|
||||||
|
break;
|
||||||
|
case not null when type == typeof(Light):
|
||||||
|
_scaledLights.Add(new ScaledLight((Light)component));
|
||||||
|
break;
|
||||||
|
case not null when type == typeof(ParentConstraint):
|
||||||
|
_scaledParentConstraints.Add(new ScaledParentConstraint((ParentConstraint)component));
|
||||||
|
break;
|
||||||
|
case not null when type == typeof(PositionConstraint):
|
||||||
|
_scaledPositionConstraints.Add(new ScaledPositionConstraint((PositionConstraint)component));
|
||||||
|
break;
|
||||||
|
case not null when type == typeof(ScaleConstraint):
|
||||||
|
_scaledScaleConstraints.Add(new ScaledScaleConstraint((ScaleConstraint)component));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyComponentScaling()
|
||||||
|
{
|
||||||
|
// UpdateLightScales(); // might break dps
|
||||||
|
UpdateAudioSourceScales();
|
||||||
|
UpdateParentConstraintScales();
|
||||||
|
UpdatePositionConstraintScales();
|
||||||
|
UpdateScaleConstraintScales();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateLightScales()
|
||||||
|
{
|
||||||
|
// Update range of each light component
|
||||||
|
foreach (ScaledLight light in _scaledLights)
|
||||||
|
light.Scale(_scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAudioSourceScales()
|
||||||
|
{
|
||||||
|
// Update min and max distance of each audio source component
|
||||||
|
foreach (ScaledAudioSource audioSource in _scaledAudioSources)
|
||||||
|
audioSource.Scale(_scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateParentConstraintScales()
|
||||||
|
{
|
||||||
|
// Update translationAtRest and translationOffsets of each parent constraint component
|
||||||
|
foreach (ScaledParentConstraint parentConstraint in _scaledParentConstraints)
|
||||||
|
parentConstraint.Scale(_scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePositionConstraintScales()
|
||||||
|
{
|
||||||
|
// Update translationAtRest and translationOffset of each position constraint component
|
||||||
|
foreach (ScaledPositionConstraint positionConstraint in _scaledPositionConstraints)
|
||||||
|
positionConstraint.Scale(_scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateScaleConstraintScales()
|
||||||
|
{
|
||||||
|
// Update scaleAtRest and scaleOffset of each scale constraint component
|
||||||
|
foreach (ScaledScaleConstraint scaleConstraint in _scaledScaleConstraints)
|
||||||
|
scaleConstraint.Scale(_scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
104
AvatarScale/GestureReconizer/ScaleReconizer.cs
Normal file
104
AvatarScale/GestureReconizer/ScaleReconizer.cs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
using ABI_RC.Core.Savior;
|
||||||
|
using ABI_RC.Systems.InputManagement;
|
||||||
|
using NAK.AvatarScaleMod.AvatarScaling;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NAK.AvatarScaleMod.GestureReconizer;
|
||||||
|
|
||||||
|
public static class ScaleReconizer
|
||||||
|
{
|
||||||
|
public static bool Enabled = true;
|
||||||
|
|
||||||
|
// Require triggers to be down while doing fist - Exteratta
|
||||||
|
public static bool RequireTriggers = true;
|
||||||
|
|
||||||
|
// Initial values when scale gesture is started
|
||||||
|
private static float _initialModifier;
|
||||||
|
private static float _initialTargetHeight;
|
||||||
|
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
// This requires arms far outward- pull inward with fist and triggers.
|
||||||
|
// Release triggers while still holding fist to readjust.
|
||||||
|
|
||||||
|
CVRGesture gesture = new CVRGesture
|
||||||
|
{
|
||||||
|
name = "avatarScaleIn",
|
||||||
|
type = CVRGesture.GestureType.Hold
|
||||||
|
};
|
||||||
|
gesture.steps.Add(new CVRGestureStep
|
||||||
|
{
|
||||||
|
firstGesture = CVRGestureStep.Gesture.Fist,
|
||||||
|
secondGesture = CVRGestureStep.Gesture.Fist,
|
||||||
|
startDistance = 1f,
|
||||||
|
endDistance = 0.25f,
|
||||||
|
direction = CVRGestureStep.GestureDirection.MovingIn
|
||||||
|
});
|
||||||
|
gesture.onStart.AddListener(OnScaleStart);
|
||||||
|
gesture.onStay.AddListener(OnScaleStay);
|
||||||
|
gesture.onEnd.AddListener(OnScaleEnd);
|
||||||
|
CVRGestureRecognizer.Instance.gestures.Add(gesture);
|
||||||
|
|
||||||
|
gesture = new CVRGesture
|
||||||
|
{
|
||||||
|
name = "avatarScaleOut",
|
||||||
|
type = CVRGesture.GestureType.Hold
|
||||||
|
};
|
||||||
|
gesture.steps.Add(new CVRGestureStep
|
||||||
|
{
|
||||||
|
firstGesture = CVRGestureStep.Gesture.Fist,
|
||||||
|
secondGesture = CVRGestureStep.Gesture.Fist,
|
||||||
|
startDistance = 0.25f,
|
||||||
|
endDistance = 1f,
|
||||||
|
direction = CVRGestureStep.GestureDirection.MovingOut
|
||||||
|
});
|
||||||
|
gesture.onStart.AddListener(OnScaleStart);
|
||||||
|
gesture.onStay.AddListener(OnScaleStay);
|
||||||
|
gesture.onEnd.AddListener(OnScaleEnd);
|
||||||
|
CVRGestureRecognizer.Instance.gestures.Add(gesture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnScaleStart(float modifier, Transform transform1, Transform transform2)
|
||||||
|
{
|
||||||
|
if (!Enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Store initial modifier so we can get difference later
|
||||||
|
_initialModifier = modifier;
|
||||||
|
_initialTargetHeight = AvatarScaleManager.Instance.GetHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnScaleStay(float modifier, Transform transform1, Transform transform2)
|
||||||
|
{
|
||||||
|
if (!Enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Allow user to release triggers to reset "world grip"
|
||||||
|
if (RequireTriggers && !AreBothTriggersDown())
|
||||||
|
{
|
||||||
|
_initialModifier = modifier;
|
||||||
|
_initialTargetHeight = AvatarScaleManager.Instance.GetHeight();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert so the gesture is more of a world squish instead of happy hug
|
||||||
|
float modifierRatio = 1f / (modifier / _initialModifier);
|
||||||
|
|
||||||
|
// Determine the adjustment factor for the height, this will be >1 if scaling up, <1 if scaling down.
|
||||||
|
float heightAdjustmentFactor = (modifierRatio > 1) ? 1 + (modifierRatio - 1) : 1 - (1 - modifierRatio);
|
||||||
|
|
||||||
|
// Apply the adjustment to the target height
|
||||||
|
AvatarScaleManager.Instance.SetHeight(_initialTargetHeight * heightAdjustmentFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnScaleEnd(float modifier, Transform transform1, Transform transform2)
|
||||||
|
{
|
||||||
|
// Unused, needed for mod network?
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AreBothTriggersDown()
|
||||||
|
{
|
||||||
|
// Maybe it should be one trigger? Imagine XSOverlay scaling but for player.
|
||||||
|
return CVRInputManager.Instance.interactLeftValue > 0.75f && CVRInputManager.Instance.interactRightValue > 0.75f;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,37 @@
|
||||||
using ABI_RC.Core.Player;
|
using ABI_RC.Core.Player;
|
||||||
using ABI_RC.Core.Savior;
|
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
|
using NAK.AvatarScaleMod.AvatarScaling;
|
||||||
|
using NAK.AvatarScaleMod.GestureReconizer;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Events;
|
using Object = UnityEngine.Object;
|
||||||
using ABI_RC.Systems.IK;
|
|
||||||
using RootMotion.FinalIK;
|
|
||||||
|
|
||||||
namespace NAK.AvatarScaleMod.HarmonyPatches;
|
namespace NAK.AvatarScaleMod.HarmonyPatches;
|
||||||
|
|
||||||
internal class PlayerSetupPatches
|
internal class PlayerSetupPatches
|
||||||
{
|
{
|
||||||
|
[HarmonyPostfix]
|
||||||
|
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))]
|
||||||
|
private static void Postfix_PlayerSetup_Start()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GameObject scaleManager = new(nameof(AvatarScaleManager), typeof(AvatarScaleManager));
|
||||||
|
Object.DontDestroyOnLoad(scaleManager);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_Start)}");
|
||||||
|
AvatarScaleMod.Logger.Error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HarmonyPostfix]
|
[HarmonyPostfix]
|
||||||
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupAvatar))]
|
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupAvatar))]
|
||||||
private static void Postfix_PlayerSetup_SetupAvatar(ref PlayerSetup __instance)
|
private static void Postfix_PlayerSetup_SetupAvatar(ref PlayerSetup __instance)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
__instance._avatar.AddComponent<AvatarScaleManager>().Initialize(__instance._initialAvatarHeight, __instance.initialScale, true);
|
AvatarScaleManager.Instance.OnAvatarInstantiated(__instance);
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -35,11 +49,12 @@ internal class PuppetMasterPatches
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
__instance.avatarObject.AddComponent<AvatarScaleManager>().Initialize(__instance._initialAvatarHeight, __instance.initialAvatarScale, false);
|
AvatarScaleManager.Instance.OnNetworkAvatarInstantiated(__instance);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_PuppetMaster_AvatarInstantiated)}");
|
AvatarScaleMod.Logger.Error(
|
||||||
|
$"Error during the patched method {nameof(Postfix_PuppetMaster_AvatarInstantiated)}");
|
||||||
AvatarScaleMod.Logger.Error(e);
|
AvatarScaleMod.Logger.Error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,45 +69,7 @@ internal class GesturePlaneTestPatches
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// nicked from Kafe >:))))
|
// nicked from Kafe >:))))
|
||||||
|
ScaleReconizer.Initialize();
|
||||||
// This requires arms far outward- pull inward with fist and triggers.
|
|
||||||
// Release triggers while still holding fist to readjust.
|
|
||||||
|
|
||||||
var gesture = new CVRGesture
|
|
||||||
{
|
|
||||||
name = "avatarScaleIn",
|
|
||||||
type = CVRGesture.GestureType.Hold,
|
|
||||||
};
|
|
||||||
gesture.steps.Add(new CVRGestureStep
|
|
||||||
{
|
|
||||||
firstGesture = CVRGestureStep.Gesture.Fist,
|
|
||||||
secondGesture = CVRGestureStep.Gesture.Fist,
|
|
||||||
startDistance = 1f,
|
|
||||||
endDistance = 0.25f,
|
|
||||||
direction = CVRGestureStep.GestureDirection.MovingIn,
|
|
||||||
});
|
|
||||||
gesture.onStart.AddListener(new UnityAction<float, Transform, Transform>(AvatarScaleGesture.OnScaleStart));
|
|
||||||
gesture.onStay.AddListener(new UnityAction<float, Transform, Transform>(AvatarScaleGesture.OnScaleStay));
|
|
||||||
gesture.onEnd.AddListener(new UnityAction<float, Transform, Transform>(AvatarScaleGesture.OnScaleEnd));
|
|
||||||
CVRGestureRecognizer.Instance.gestures.Add(gesture);
|
|
||||||
|
|
||||||
gesture = new CVRGesture
|
|
||||||
{
|
|
||||||
name = "avatarScaleOut",
|
|
||||||
type = CVRGesture.GestureType.Hold,
|
|
||||||
};
|
|
||||||
gesture.steps.Add(new CVRGestureStep
|
|
||||||
{
|
|
||||||
firstGesture = CVRGestureStep.Gesture.Fist,
|
|
||||||
secondGesture = CVRGestureStep.Gesture.Fist,
|
|
||||||
startDistance = 0.25f,
|
|
||||||
endDistance = 1f,
|
|
||||||
direction = CVRGestureStep.GestureDirection.MovingOut,
|
|
||||||
});
|
|
||||||
gesture.onStart.AddListener(new UnityAction<float, Transform, Transform>(AvatarScaleGesture.OnScaleStart));
|
|
||||||
gesture.onStay.AddListener(new UnityAction<float, Transform, Transform>(AvatarScaleGesture.OnScaleStay));
|
|
||||||
gesture.onEnd.AddListener(new UnityAction<float, Transform, Transform>(AvatarScaleGesture.OnScaleEnd));
|
|
||||||
CVRGestureRecognizer.Instance.gestures.Add(gesture);
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,19 +1,28 @@
|
||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
|
using NAK.AvatarScaleMod.Networking;
|
||||||
|
|
||||||
namespace NAK.AvatarScaleMod;
|
namespace NAK.AvatarScaleMod;
|
||||||
|
|
||||||
public class AvatarScaleMod : MelonMod
|
public class AvatarScaleMod : MelonMod
|
||||||
{
|
{
|
||||||
internal static MelonLogger.Instance Logger;
|
internal static MelonLogger.Instance Logger;
|
||||||
|
|
||||||
public override void OnInitializeMelon()
|
public override void OnInitializeMelon()
|
||||||
{
|
{
|
||||||
Logger = LoggerInstance;
|
Logger = LoggerInstance;
|
||||||
|
|
||||||
ModSettings.InitializeModSettings();
|
|
||||||
ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches));
|
ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches));
|
||||||
//ApplyPatches(typeof(HarmonyPatches.PuppetMasterPatches));
|
ApplyPatches(typeof(HarmonyPatches.PuppetMasterPatches));
|
||||||
ApplyPatches(typeof(HarmonyPatches.GesturePlaneTestPatches));
|
ApplyPatches(typeof(HarmonyPatches.GesturePlaneTestPatches));
|
||||||
|
|
||||||
|
ModNetwork.Subscribe();
|
||||||
|
ModSettings.InitializeModSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnUpdate()
|
||||||
|
{
|
||||||
|
ModNetwork.Update();
|
||||||
|
ModNetworkDebugger.DoDebugInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyPatches(Type type)
|
private void ApplyPatches(Type type)
|
||||||
|
|
|
@ -8,32 +8,68 @@ internal static class ModSettings
|
||||||
{
|
{
|
||||||
public static readonly MelonPreferences_Category Category =
|
public static readonly MelonPreferences_Category Category =
|
||||||
MelonPreferences.CreateCategory(nameof(AvatarScaleMod));
|
MelonPreferences.CreateCategory(nameof(AvatarScaleMod));
|
||||||
|
|
||||||
|
// AvatarScaleTool supported scaling settings
|
||||||
public static readonly MelonPreferences_Entry<bool> EntryEnabled =
|
public static readonly MelonPreferences_Entry<bool> EntryEnabled =
|
||||||
Category.CreateEntry("Enabled", true, description: "Toggle AvatarScaleMod entirely for Local user. Kinda.");
|
Category.CreateEntry("AvatarScaleTool Scaling", true, description: "Should there be persistant avatar scaling? This only works properly across supported avatars.");
|
||||||
|
public static readonly MelonPreferences_Entry<bool> EntryPersistAnyways =
|
||||||
|
Category.CreateEntry("Persist From Unsupported", true, description: "Should avatar scale persist even from unsupported avatars?");
|
||||||
|
|
||||||
|
// Universal scaling settings (Mod Network, requires others to have mod)
|
||||||
|
public static readonly MelonPreferences_Entry<bool> EntryUniversalScaling =
|
||||||
|
Category.CreateEntry("Force Universal Scaling", false, description: "Should the mod use Mod Network for scaling? This makes it work on all avatars, but others need the mod.");
|
||||||
|
public static readonly MelonPreferences_Entry<bool> EntryScaleConstraints =
|
||||||
|
Category.CreateEntry("Scale Constraints", false, description: "Should constraints be scaled with Universal Scaling?");
|
||||||
|
public static readonly MelonPreferences_Entry<bool> EntryScaleLights =
|
||||||
|
Category.CreateEntry("Scale Lights", false, description: "Should lights be scaled with Universal Scaling?");
|
||||||
|
public static readonly MelonPreferences_Entry<bool> EntryScaleAudioSources =
|
||||||
|
Category.CreateEntry("Scale Audio Sources", false, description: "Should audio sources be scaled with Universal Scaling?");
|
||||||
|
|
||||||
|
// General scaling settings
|
||||||
public static readonly MelonPreferences_Entry<bool> EntryUseScaleGesture =
|
public static readonly MelonPreferences_Entry<bool> EntryUseScaleGesture =
|
||||||
Category.CreateEntry("Scale Gesture", false, description: "Use two fists to scale yourself easily.");
|
Category.CreateEntry("Scale Gesture", false, description: "Use two fists to scale yourself easily.");
|
||||||
|
|
||||||
|
// Internal settings
|
||||||
|
public static readonly MelonPreferences_Entry<float> HiddenLastAvatarScale =
|
||||||
|
Category.CreateEntry("Last Avatar Scale", -1f, is_hidden: true);
|
||||||
|
|
||||||
static ModSettings()
|
static ModSettings()
|
||||||
{
|
{
|
||||||
EntryEnabled.OnEntryValueChanged.Subscribe(OnEntryEnabledChanged);
|
EntryEnabled.OnEntryValueChanged.Subscribe(OnEntryEnabledChanged);
|
||||||
EntryUseScaleGesture.OnEntryValueChanged.Subscribe(OnEntryUseScaleGestureChanged);
|
EntryUseScaleGesture.OnEntryValueChanged.Subscribe(OnEntryUseScaleGestureChanged);
|
||||||
}
|
EntryPersistAnyways.OnEntryValueChanged.Subscribe(OnEntryPersistAnywaysChanged);
|
||||||
|
EntryUniversalScaling.OnEntryValueChanged.Subscribe(OnEntryUniversalScalingChanged);
|
||||||
public static void InitializeModSettings()
|
EntryScaleConstraints.OnEntryValueChanged.Subscribe(OnEntryScaleConstraintsChanged);
|
||||||
{
|
|
||||||
AvatarScaleManager.GlobalEnabled = EntryEnabled.Value;
|
|
||||||
AvatarScaleGesture.GestureEnabled = EntryUseScaleGesture.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnEntryEnabledChanged(bool oldValue, bool newValue)
|
private static void OnEntryEnabledChanged(bool oldValue, bool newValue)
|
||||||
{
|
{
|
||||||
AvatarScaleManager.GlobalEnabled = newValue;
|
//AvatarScaleManager.UseUniversalScaling = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnEntryUseScaleGestureChanged(bool oldValue, bool newValue)
|
private static void OnEntryUseScaleGestureChanged(bool oldValue, bool newValue)
|
||||||
{
|
{
|
||||||
AvatarScaleGesture.GestureEnabled = newValue;
|
//AvatarScaleGesture.GestureEnabled = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnEntryPersistAnywaysChanged(bool oldValue, bool newValue)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnEntryUniversalScalingChanged(bool oldValue, bool newValue)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnEntryScaleConstraintsChanged(bool oldValue, bool newValue)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void InitializeModSettings()
|
||||||
|
{
|
||||||
|
//AvatarScaleManager.UseUniversalScaling = EntryEnabled.Value;
|
||||||
|
//AvatarScaleGesture.GestureEnabled = EntryUseScaleGesture.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
140
AvatarScale/Networking/ModNetwork.cs
Normal file
140
AvatarScale/Networking/ModNetwork.cs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
using ABI_RC.Systems.ModNetwork;
|
||||||
|
using MelonLoader;
|
||||||
|
using NAK.AvatarScaleMod.AvatarScaling;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NAK.AvatarScaleMod.Networking;
|
||||||
|
|
||||||
|
public static class ModNetwork
|
||||||
|
{
|
||||||
|
#region Constants
|
||||||
|
|
||||||
|
private const string ModId = "MelonMod.NAK.AvatarScaleMod";
|
||||||
|
private const float SendRateLimit = 0.25f;
|
||||||
|
private const float ReceiveRateLimit = 0.2f;
|
||||||
|
private const int MaxWarnings = 2;
|
||||||
|
private const float TimeoutDuration = 10f;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private State
|
||||||
|
|
||||||
|
private static float? OutboundQueue;
|
||||||
|
private static float LastSentTime;
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, float> InboundQueue = new Dictionary<string, float>();
|
||||||
|
private static readonly Dictionary<string, float> LastReceivedTimes = new Dictionary<string, float>();
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, int> UserWarnings = new Dictionary<string, int>();
|
||||||
|
private static readonly Dictionary<string, float> UserTimeouts = new Dictionary<string, float>();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mod Network Internals
|
||||||
|
|
||||||
|
internal static void Subscribe()
|
||||||
|
{
|
||||||
|
ModNetworkManager.Subscribe(ModId, OnMessageReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Update()
|
||||||
|
{
|
||||||
|
ProcessOutboundQueue();
|
||||||
|
ProcessInboundQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SendMessageToAll(float height)
|
||||||
|
{
|
||||||
|
using ModNetworkMessage modMsg = new ModNetworkMessage(ModId);
|
||||||
|
modMsg.Write(height);
|
||||||
|
modMsg.Send();
|
||||||
|
//MelonLogger.Msg($"Sending height: {height}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnMessageReceived(ModNetworkMessage msg)
|
||||||
|
{
|
||||||
|
msg.Read(out float receivedHeight);
|
||||||
|
ProcessReceivedHeight(msg.Sender, receivedHeight);
|
||||||
|
//MelonLogger.Msg($"Received height from {msg.Sender}: {receivedHeight}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public Methods
|
||||||
|
|
||||||
|
public static void SendNetworkHeight(float newHeight)
|
||||||
|
{
|
||||||
|
OutboundQueue = newHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Outbound Height Queue
|
||||||
|
|
||||||
|
private static void ProcessOutboundQueue()
|
||||||
|
{
|
||||||
|
if (!OutboundQueue.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!(Time.time - LastSentTime >= SendRateLimit))
|
||||||
|
return;
|
||||||
|
|
||||||
|
SendMessageToAll(OutboundQueue.Value);
|
||||||
|
LastSentTime = Time.time;
|
||||||
|
OutboundQueue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Inbound Height Queue
|
||||||
|
|
||||||
|
private static void ProcessReceivedHeight(string userId, float receivedHeight)
|
||||||
|
{
|
||||||
|
// User is in timeout
|
||||||
|
if (UserTimeouts.TryGetValue(userId, out float timeoutEnd) && Time.time < timeoutEnd)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Rate-limit checking
|
||||||
|
if (LastReceivedTimes.TryGetValue(userId, out float lastReceivedTime) &&
|
||||||
|
Time.time - lastReceivedTime < ReceiveRateLimit)
|
||||||
|
{
|
||||||
|
if (UserWarnings.TryGetValue(userId, out int warnings))
|
||||||
|
{
|
||||||
|
warnings++;
|
||||||
|
UserWarnings[userId] = warnings;
|
||||||
|
|
||||||
|
if (warnings >= MaxWarnings)
|
||||||
|
{
|
||||||
|
UserTimeouts[userId] = Time.time + TimeoutDuration;
|
||||||
|
MelonLogger.Msg($"User is sending height updates too fast! Applying 10s timeout... : {userId}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UserWarnings[userId] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LastReceivedTimes[userId] = Time.time;
|
||||||
|
UserWarnings.Remove(userId); // Reset warnings
|
||||||
|
// MelonLogger.Msg($"Clearing timeout from user : {userId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
InboundQueue[userId] = receivedHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProcessInboundQueue()
|
||||||
|
{
|
||||||
|
foreach (var (userId, height) in InboundQueue)
|
||||||
|
{
|
||||||
|
MelonLogger.Msg($"Applying inbound queued height {height} from : {userId}");
|
||||||
|
AvatarScaleManager.Instance.OnNetworkHeightUpdateReceived(userId, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
InboundQueue.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
45
AvatarScale/Networking/ModNetworkDebugger.cs
Normal file
45
AvatarScale/Networking/ModNetworkDebugger.cs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
using NAK.AvatarScaleMod.AvatarScaling;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NAK.AvatarScaleMod.Networking;
|
||||||
|
|
||||||
|
public static class ModNetworkDebugger
|
||||||
|
{
|
||||||
|
public static void DoDebugInput()
|
||||||
|
{
|
||||||
|
// if (NetworkManager.Instance == null || NetworkManager.Instance.GameNetwork.ConnectionState != ConnectionState.Connected)
|
||||||
|
// {
|
||||||
|
// MelonLogger.Warning("Attempted to send a game network message without being connected to an online instance...");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (AvatarScaleManager.Instance == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float currentHeight = AvatarScaleManager.Instance.GetHeight();
|
||||||
|
const float step = 0.1f;
|
||||||
|
|
||||||
|
if (Input.GetKeyDown(KeyCode.Equals) || Input.GetKeyDown(KeyCode.KeypadPlus))
|
||||||
|
{
|
||||||
|
currentHeight += step;
|
||||||
|
AvatarScaleMod.Logger.Msg($"Networking height: {currentHeight}");
|
||||||
|
ModNetwork.SendNetworkHeight(currentHeight);
|
||||||
|
AvatarScaleManager.Instance.SetHeight(currentHeight);
|
||||||
|
}
|
||||||
|
else if (Input.GetKeyDown(KeyCode.Minus) || Input.GetKeyDown(KeyCode.KeypadMinus))
|
||||||
|
{
|
||||||
|
currentHeight -= step;
|
||||||
|
AvatarScaleMod.Logger.Msg($"Networking height: {currentHeight}");
|
||||||
|
ModNetwork.SendNetworkHeight(currentHeight);
|
||||||
|
AvatarScaleManager.Instance.SetHeight(currentHeight);
|
||||||
|
}
|
||||||
|
else if (Input.GetKeyDown(KeyCode.Backspace))
|
||||||
|
{
|
||||||
|
AvatarScaleManager.Instance.ResetHeight();
|
||||||
|
currentHeight = AvatarScaleManager.Instance.GetHeight();
|
||||||
|
|
||||||
|
AvatarScaleMod.Logger.Msg($"Networking height: {currentHeight}");
|
||||||
|
ModNetwork.SendNetworkHeight(currentHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,72 +0,0 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.Animations;
|
|
||||||
|
|
||||||
namespace NAK.AvatarScaleMod.ScaledComponents;
|
|
||||||
|
|
||||||
public class ScaledAudioSource
|
|
||||||
{
|
|
||||||
public AudioSource Component { get; }
|
|
||||||
public float InitialMinDistance { get; }
|
|
||||||
public float InitialMaxDistance { get; }
|
|
||||||
|
|
||||||
public ScaledAudioSource(AudioSource component)
|
|
||||||
{
|
|
||||||
Component = component;
|
|
||||||
InitialMinDistance = component.minDistance;
|
|
||||||
InitialMaxDistance = component.maxDistance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ScaledLight
|
|
||||||
{
|
|
||||||
public Light Component { get; }
|
|
||||||
public float InitialRange { get; }
|
|
||||||
|
|
||||||
public ScaledLight(Light component)
|
|
||||||
{
|
|
||||||
Component = component;
|
|
||||||
InitialRange = component.range;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ScaledPositionConstraint
|
|
||||||
{
|
|
||||||
public PositionConstraint Component { get; }
|
|
||||||
public Vector3 InitialTranslationAtRest { get; }
|
|
||||||
public Vector3 InitialTranslationOffset { get; }
|
|
||||||
|
|
||||||
public ScaledPositionConstraint(PositionConstraint component)
|
|
||||||
{
|
|
||||||
Component = component;
|
|
||||||
InitialTranslationAtRest = component.translationAtRest;
|
|
||||||
InitialTranslationOffset = component.translationOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ScaledParentConstraint
|
|
||||||
{
|
|
||||||
public ParentConstraint Component { get; }
|
|
||||||
public Vector3 InitialTranslationAtRest { get; }
|
|
||||||
public List<Vector3> InitialTranslationOffsets { get; }
|
|
||||||
|
|
||||||
public ScaledParentConstraint(ParentConstraint component)
|
|
||||||
{
|
|
||||||
Component = component;
|
|
||||||
InitialTranslationAtRest = component.translationAtRest;
|
|
||||||
InitialTranslationOffsets = component.translationOffsets.ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ScaledScaleConstraint
|
|
||||||
{
|
|
||||||
public ScaleConstraint Component { get; }
|
|
||||||
public Vector3 InitialScaleAtRest { get; }
|
|
||||||
public Vector3 InitialScaleOffset { get; }
|
|
||||||
|
|
||||||
public ScaledScaleConstraint(ScaleConstraint component)
|
|
||||||
{
|
|
||||||
Component = component;
|
|
||||||
InitialScaleAtRest = component.scaleAtRest;
|
|
||||||
InitialScaleOffset = component.scaleOffset;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue