using System.Diagnostics; using ABI_RC.Core; using ABI_RC.Core.Util.AnimatorManager; using NAK.AvatarScaleMod.AvatarScaling; using NAK.AvatarScaleMod.ScaledComponents; using UnityEngine; using UnityEngine.Animations; namespace NAK.AvatarScaleMod.Components; [DefaultExecutionOrder(-99999)] // before playersetup/puppetmaster but after animator public class BaseScaler : MonoBehaviour { #region Constants protected const string ScaleFactorParameterName = "ScaleFactor"; protected const string ScaleFactorParameterNameLocal = "#" + ScaleFactorParameterName; #endregion #region Events // OnAnimatedHeightChanged public delegate void AnimatedHeightChangedDelegate(BaseScaler scaler); public event AnimatedHeightChangedDelegate OnAnimatedHeightChanged; // OnAnimatedHeightOverride public delegate void AnimatedHeightOverrideDelegate(BaseScaler scaler); public event AnimatedHeightOverrideDelegate OnAnimatedHeightOverride; // OnTargetHeightChanged public delegate void TargetHeightChangedDelegate(BaseScaler scaler); public event TargetHeightChangedDelegate OnTargetHeightChanged; // OnHeightReset public delegate void HeightResetDelegate(BaseScaler scaler); public event HeightResetDelegate OnTargetHeightReset; // ------------------------------------------------ protected void InvokeAnimatedHeightChanged() => OnAnimatedHeightChanged?.Invoke(this); protected void InvokeAnimatedHeightOverride() => OnAnimatedHeightOverride?.Invoke(this); protected void InvokeTargetHeightChanged() => OnTargetHeightChanged?.Invoke(this); protected void InvokeTargetHeightReset() => OnTargetHeightReset?.Invoke(this); #endregion #region Variables private float _targetHeight; public float TargetHeight { get => _targetHeight; set { if (value < float.Epsilon) { // reset to animated height _targetHeight = _animatedHeight; _targetScale = _animatedScale; _scaleFactor = _animatedScaleFactor; _targetHeightChanged = true; return; } _targetHeight = Mathf.Clamp(value, AvatarScaleManager.MinHeight, AvatarScaleManager.MaxHeight); _scaleFactor = Mathf.Max(_targetHeight / _initialHeight, 0.01f); //safety _targetScale = _initialScale * _scaleFactor; _targetHeightChanged = true; } } protected bool _useTargetHeight; public bool UseTargetHeight { get => _useTargetHeight; set { if (_useTargetHeight == value) return; _useTargetHeight = value; _targetHeightChanged = true; } } // Config variables public bool avatarIsHidden { get; set; } public bool overrideAnimationHeight { get; set; } // State variables internal bool _isAvatarInstantiated; private bool _shouldForceHeight => _useTargetHeight || avatarIsHidden; private bool _targetHeightChanged; // Avatar info internal Transform _avatarTransform; internal CVRAnimatorManager _animatorManager; // Initial scaling internal float _initialHeight; internal Vector3 _initialScale; // Forced scaling (Universal & Hidden Avatar) internal Vector3 _targetScale = Vector3.one; internal float _scaleFactor = 1f; // AnimationClip-based scaling (Local Avatar) internal float _animatedHeight; internal Vector3 _animatedScale; internal float _animatedScaleFactor = 1f; #endregion #region Avatar Events public virtual void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale) { if (_isAvatarInstantiated) return; _isAvatarInstantiated = true; _initialHeight = _animatedHeight = Mathf.Clamp(initialHeight, 0.01f, 100f); _initialScale = _animatedScale = initialScale; _animatedScaleFactor = 1f; if (!_shouldForceHeight) // not universal or hidden avatar { _targetHeight = _initialHeight; _targetScale = _initialScale; _scaleFactor = 1f; } _avatarTransform = avatarObject.transform; Stopwatch stopwatch = new(); stopwatch.Start(); FindComponentsOfType(scalableComponentTypes); stopwatch.Stop(); if (ModSettings.Debug_ComponentSearchTime.Value) AvatarScaleMod.Logger.Msg($"({typeof(LocalScaler)}) Component search time for {avatarObject}: {stopwatch.ElapsedMilliseconds}ms"); } public void OnAvatarDestroyed() { if (!_isAvatarInstantiated) return; _isAvatarInstantiated = false; _avatarTransform = null; ClearComponentLists(); } #endregion #region Public Methods public float GetInitialHeight() => _initialHeight; public float GetTargetHeight() => _targetHeight; public float GetAnimatedHeight() => _animatedHeight; public bool IsForcingHeight() => _shouldForceHeight; #endregion #region Private Methods public bool ApplyTargetHeight() { if (!_isAvatarInstantiated || _initialHeight == 0) return false; if (_avatarTransform == null) return false; _targetHeightChanged = false; ScaleAvatarRoot(); UpdateAnimatorParameter(); ApplyComponentScaling(); InvokeTargetHeightChanged(); return true; } protected void ResetTargetHeight() { if (!_isAvatarInstantiated) return; if (_avatarTransform == null) return; _targetHeight = _initialHeight; _targetScale = _initialScale; _scaleFactor = 1f; _targetHeightChanged = false; ScaleAvatarRoot(); UpdateAnimatorParameter(); ApplyComponentScaling(); InvokeTargetHeightReset(); } private void ScaleAvatarRoot() { if (_avatarTransform == null) return; _avatarTransform.localScale = _targetScale; } protected virtual void UpdateAnimatorParameter() { // empty } #endregion #region Unity Events public virtual void LateUpdate() { if (!_isAvatarInstantiated) return; // no avatar if (!_shouldForceHeight) return; // using built-in scaling // called on state change if (_targetHeightChanged) { if (_useTargetHeight) ApplyTargetHeight(); else ResetTargetHeight(); _targetHeightChanged = false; InvokeTargetHeightChanged(); } // called constantly when forcing change if (_shouldForceHeight) ScaleAvatarRoot(); } internal virtual void OnDestroy() { ClearComponentLists(); } #endregion #region Component Scaling internal static readonly Type[] scalableComponentTypes = { typeof(Light), typeof(AudioSource), typeof(ParticleSystem), typeof(ParentConstraint), typeof(PositionConstraint), typeof(ScaleConstraint) }; private readonly List _scaledLights = new(); private readonly List _scaledAudioSources = new(); private readonly List _scaledParentConstraints = new(); private readonly List _scaledPositionConstraints = new(); private readonly List _scaledScaleConstraints = new(); private void ClearComponentLists() { _scaledLights.Clear(); _scaledAudioSources.Clear(); _scaledParentConstraints.Clear(); _scaledPositionConstraints.Clear(); _scaledScaleConstraints.Clear(); } internal void FindComponentsOfType(Type[] types) { var components = _avatarTransform.gameObject.GetComponentsInChildren(true); foreach (Component component in components) { if (this == null) break; if (component == null) continue; Type componentType = component.GetType(); if (types.Contains(componentType)) AddScaledComponent(componentType, component); } } 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 }