From d599f5797378e75fe225cbc2a842779f0012eaf5 Mon Sep 17 00:00:00 2001 From: NotAKidoS <37721153+NotAKidOnSteam@users.noreply.github.com> Date: Wed, 20 Sep 2023 05:24:07 -0500 Subject: [PATCH] [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. --- AvatarScale/AvatarScaleGesture.cs | 67 ---- AvatarScale/AvatarScaleManager.cs | 342 ------------------ AvatarScale/AvatarScaleMod.csproj | 2 +- .../AvatarScaling/AvatarScaleManager.cs | 112 ++++++ AvatarScale/AvatarScaling/ScaledComponents.cs | 133 +++++++ .../AvatarScaling/UniversalAvatarScaler.cs | 231 ++++++++++++ .../GestureReconizer/ScaleReconizer.cs | 104 ++++++ AvatarScale/HarmonyPatches.cs | 71 ++-- AvatarScale/Main.cs | 15 +- AvatarScale/ModSettings.cs | 60 ++- AvatarScale/Networking/ModNetwork.cs | 140 +++++++ AvatarScale/Networking/ModNetworkDebugger.cs | 45 +++ AvatarScale/ScaledComponents.cs | 72 ---- 13 files changed, 850 insertions(+), 544 deletions(-) delete mode 100644 AvatarScale/AvatarScaleGesture.cs delete mode 100644 AvatarScale/AvatarScaleManager.cs create mode 100644 AvatarScale/AvatarScaling/AvatarScaleManager.cs create mode 100644 AvatarScale/AvatarScaling/ScaledComponents.cs create mode 100644 AvatarScale/AvatarScaling/UniversalAvatarScaler.cs create mode 100644 AvatarScale/GestureReconizer/ScaleReconizer.cs create mode 100644 AvatarScale/Networking/ModNetwork.cs create mode 100644 AvatarScale/Networking/ModNetworkDebugger.cs delete mode 100644 AvatarScale/ScaledComponents.cs diff --git a/AvatarScale/AvatarScaleGesture.cs b/AvatarScale/AvatarScaleGesture.cs deleted file mode 100644 index 4bc3165..0000000 --- a/AvatarScale/AvatarScaleGesture.cs +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/AvatarScale/AvatarScaleManager.cs b/AvatarScale/AvatarScaleManager.cs deleted file mode 100644 index 1630265..0000000 --- a/AvatarScale/AvatarScaleManager.cs +++ /dev/null @@ -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 _scaledLights = new List(); - private List _scaledAudioSources = new List(); - private List _scaledParentConstraints = new List(); - private List _scaledPositionConstraints = new List(); - private List _scaledScaleConstraints = new List(); - - 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(); - - if (_avatar == null) - { - AvatarScaleMod.Logger.Error("AvatarScaleManager should be attached to a GameObject with a CVRAvatar component."); - return; - } - - if (!_isLocalAvatar) - { - _animator = GetComponent(); - } - - // 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(); - var components = GetComponentsInChildren(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; - } - } -} \ No newline at end of file diff --git a/AvatarScale/AvatarScaleMod.csproj b/AvatarScale/AvatarScaleMod.csproj index 66a50a8..e94f9dc 100644 --- a/AvatarScale/AvatarScaleMod.csproj +++ b/AvatarScale/AvatarScaleMod.csproj @@ -1,2 +1,2 @@ - + diff --git a/AvatarScale/AvatarScaling/AvatarScaleManager.cs b/AvatarScale/AvatarScaling/AvatarScaleManager.cs new file mode 100644 index 0000000..c9972e9 --- /dev/null +++ b/AvatarScale/AvatarScaling/AvatarScaleManager.cs @@ -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 _networkedScalers; + private UniversalAvatarScaler _localAvatarScaler; + + #region Unity Methods + + private void Awake() + { + if (Instance != null) + { + DestroyImmediate(this); + return; + } + + Instance = this; + _networkedScalers = new Dictionary(); + } + + #endregion + + #region Local Methods + + public void OnAvatarInstantiated(PlayerSetup playerSetup) + { + if (playerSetup._avatar == null) + return; + + if (_localAvatarScaler != null) + Destroy(_localAvatarScaler); + + _localAvatarScaler = playerSetup._avatar.AddComponent(); + _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(); + 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 +} \ No newline at end of file diff --git a/AvatarScale/AvatarScaling/ScaledComponents.cs b/AvatarScale/AvatarScaling/ScaledComponents.cs new file mode 100644 index 0000000..a59cb19 --- /dev/null +++ b/AvatarScale/AvatarScaling/ScaledComponents.cs @@ -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 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; + } +} \ No newline at end of file diff --git a/AvatarScale/AvatarScaling/UniversalAvatarScaler.cs b/AvatarScale/AvatarScaling/UniversalAvatarScaler.cs new file mode 100644 index 0000000..25be860 --- /dev/null +++ b/AvatarScale/AvatarScaling/UniversalAvatarScaler.cs @@ -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().animatorManager + : GetComponentInParent()._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 _scaledLights = new List(); + private readonly List _scaledAudioSources = new List(); + private readonly List _scaledParentConstraints = new List(); + private readonly List _scaledPositionConstraints = new List(); + private readonly List _scaledScaleConstraints = new List(); + + private async Task FindComponentsOfTypeAsync(Type[] types) + { + var tasks = new List(); + var components = GetComponentsInChildren(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 +} \ No newline at end of file diff --git a/AvatarScale/GestureReconizer/ScaleReconizer.cs b/AvatarScale/GestureReconizer/ScaleReconizer.cs new file mode 100644 index 0000000..20c22b4 --- /dev/null +++ b/AvatarScale/GestureReconizer/ScaleReconizer.cs @@ -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; + } +} \ No newline at end of file diff --git a/AvatarScale/HarmonyPatches.cs b/AvatarScale/HarmonyPatches.cs index b5af811..453c225 100644 --- a/AvatarScale/HarmonyPatches.cs +++ b/AvatarScale/HarmonyPatches.cs @@ -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().Initialize(__instance._initialAvatarHeight, __instance.initialScale, true); - + AvatarScaleManager.Instance.OnAvatarInstantiated(__instance); } catch (Exception e) { @@ -35,11 +49,12 @@ internal class PuppetMasterPatches { try { - __instance.avatarObject.AddComponent().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(AvatarScaleGesture.OnScaleStart)); - gesture.onStay.AddListener(new UnityAction(AvatarScaleGesture.OnScaleStay)); - gesture.onEnd.AddListener(new UnityAction(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(AvatarScaleGesture.OnScaleStart)); - gesture.onStay.AddListener(new UnityAction(AvatarScaleGesture.OnScaleStay)); - gesture.onEnd.AddListener(new UnityAction(AvatarScaleGesture.OnScaleEnd)); - CVRGestureRecognizer.Instance.gestures.Add(gesture); + ScaleReconizer.Initialize(); } catch (Exception e) { diff --git a/AvatarScale/Main.cs b/AvatarScale/Main.cs index 03071c8..aad99e7 100644 --- a/AvatarScale/Main.cs +++ b/AvatarScale/Main.cs @@ -1,19 +1,28 @@ using MelonLoader; +using NAK.AvatarScaleMod.Networking; namespace NAK.AvatarScaleMod; public class AvatarScaleMod : MelonMod { internal static MelonLogger.Instance Logger; - + public override void OnInitializeMelon() { 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) diff --git a/AvatarScale/ModSettings.cs b/AvatarScale/ModSettings.cs index e07b7fa..a44f007 100644 --- a/AvatarScale/ModSettings.cs +++ b/AvatarScale/ModSettings.cs @@ -8,32 +8,68 @@ internal static class ModSettings { public static readonly MelonPreferences_Category Category = MelonPreferences.CreateCategory(nameof(AvatarScaleMod)); - + + // AvatarScaleTool supported scaling settings public static readonly MelonPreferences_Entry 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 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 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 EntryScaleConstraints = + Category.CreateEntry("Scale Constraints", false, description: "Should constraints be scaled with Universal Scaling?"); + public static readonly MelonPreferences_Entry EntryScaleLights = + Category.CreateEntry("Scale Lights", false, description: "Should lights be scaled with Universal Scaling?"); + public static readonly MelonPreferences_Entry EntryScaleAudioSources = + Category.CreateEntry("Scale Audio Sources", false, description: "Should audio sources be scaled with Universal Scaling?"); + + // General scaling settings public static readonly MelonPreferences_Entry EntryUseScaleGesture = Category.CreateEntry("Scale Gesture", false, description: "Use two fists to scale yourself easily."); - + + // Internal settings + public static readonly MelonPreferences_Entry 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; } } \ No newline at end of file diff --git a/AvatarScale/Networking/ModNetwork.cs b/AvatarScale/Networking/ModNetwork.cs new file mode 100644 index 0000000..dbc1bde --- /dev/null +++ b/AvatarScale/Networking/ModNetwork.cs @@ -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 InboundQueue = new Dictionary(); + private static readonly Dictionary LastReceivedTimes = new Dictionary(); + + private static readonly Dictionary UserWarnings = new Dictionary(); + private static readonly Dictionary UserTimeouts = new Dictionary(); + + #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 +} \ No newline at end of file diff --git a/AvatarScale/Networking/ModNetworkDebugger.cs b/AvatarScale/Networking/ModNetworkDebugger.cs new file mode 100644 index 0000000..d3f0be5 --- /dev/null +++ b/AvatarScale/Networking/ModNetworkDebugger.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/AvatarScale/ScaledComponents.cs b/AvatarScale/ScaledComponents.cs deleted file mode 100644 index 5b60fc8..0000000 --- a/AvatarScale/ScaledComponents.cs +++ /dev/null @@ -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 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; - } -} \ No newline at end of file