[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:
NotAKidoS 2023-09-20 05:24:07 -05:00
parent 69cb3b7804
commit d599f57973
13 changed files with 850 additions and 544 deletions

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View 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
}

View 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;
}
}

View 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
}

View 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;
}
}

View file

@ -1,23 +1,37 @@
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using HarmonyLib;
using NAK.AvatarScaleMod.AvatarScaling;
using NAK.AvatarScaleMod.GestureReconizer;
using UnityEngine;
using UnityEngine.Events;
using ABI_RC.Systems.IK;
using RootMotion.FinalIK;
using Object = UnityEngine.Object;
namespace NAK.AvatarScaleMod.HarmonyPatches;
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]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupAvatar))]
private static void Postfix_PlayerSetup_SetupAvatar(ref PlayerSetup __instance)
{
try
{
__instance._avatar.AddComponent<AvatarScaleManager>().Initialize(__instance._initialAvatarHeight, __instance.initialScale, true);
AvatarScaleManager.Instance.OnAvatarInstantiated(__instance);
}
catch (Exception e)
{
@ -35,11 +49,12 @@ internal class PuppetMasterPatches
{
try
{
__instance.avatarObject.AddComponent<AvatarScaleManager>().Initialize(__instance._initialAvatarHeight, __instance.initialAvatarScale, false);
AvatarScaleManager.Instance.OnNetworkAvatarInstantiated(__instance);
}
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);
}
}
@ -54,45 +69,7 @@ internal class GesturePlaneTestPatches
try
{
// nicked from Kafe >:))))
// 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);
ScaleReconizer.Initialize();
}
catch (Exception e)
{

View file

@ -1,4 +1,5 @@
using MelonLoader;
using NAK.AvatarScaleMod.Networking;
namespace NAK.AvatarScaleMod;
@ -10,10 +11,18 @@ public class AvatarScaleMod : MelonMod
{
Logger = LoggerInstance;
ModSettings.InitializeModSettings();
ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches));
//ApplyPatches(typeof(HarmonyPatches.PuppetMasterPatches));
ApplyPatches(typeof(HarmonyPatches.PuppetMasterPatches));
ApplyPatches(typeof(HarmonyPatches.GesturePlaneTestPatches));
ModNetwork.Subscribe();
ModSettings.InitializeModSettings();
}
public override void OnUpdate()
{
ModNetwork.Update();
ModNetworkDebugger.DoDebugInput();
}
private void ApplyPatches(Type type)

View file

@ -9,31 +9,67 @@ internal static class ModSettings
public static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(nameof(AvatarScaleMod));
// AvatarScaleTool supported scaling settings
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 =
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()
{
EntryEnabled.OnEntryValueChanged.Subscribe(OnEntryEnabledChanged);
EntryUseScaleGesture.OnEntryValueChanged.Subscribe(OnEntryUseScaleGestureChanged);
}
public static void InitializeModSettings()
{
AvatarScaleManager.GlobalEnabled = EntryEnabled.Value;
AvatarScaleGesture.GestureEnabled = EntryUseScaleGesture.Value;
EntryPersistAnyways.OnEntryValueChanged.Subscribe(OnEntryPersistAnywaysChanged);
EntryUniversalScaling.OnEntryValueChanged.Subscribe(OnEntryUniversalScalingChanged);
EntryScaleConstraints.OnEntryValueChanged.Subscribe(OnEntryScaleConstraintsChanged);
}
private static void OnEntryEnabledChanged(bool oldValue, bool newValue)
{
AvatarScaleManager.GlobalEnabled = newValue;
//AvatarScaleManager.UseUniversalScaling = 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;
}
}

View 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
}

View 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);
}
}
}

View file

@ -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;
}
}