using System.Collections; using ABI_RC.Core.IO; using ABI_RC.Core.Player; using ABI_RC.Core.Player.AvatarTracking; using ABI_RC.Core.UI; using ABI_RC.Systems.GameEventSystem; using NAK.AvatarScaleMod.Components; using NAK.AvatarScaleMod.Networking; using UnityEngine; namespace NAK.AvatarScaleMod.AvatarScaling; public class AvatarScaleManager : MonoBehaviour { public static AvatarScaleManager Instance { get; private set; } private LocalScaler _localAvatarScaler; private Dictionary _networkedScalers; private Coroutine _heightUpdateCoroutine; private readonly YieldInstruction _heightUpdateYield = new WaitForEndOfFrame(); #region Universal Scaling Limits // ReSharper disable MemberCanBePrivate.Global // To match AvatarScaleTool: https://github.com/NotAKidOnSteam/AvatarScaleTool/tree/main public const float DefaultMinHeight = 0.25f; public const float DefaultMaxHeight = 2.50f; // ReSharper restore MemberCanBePrivate.Global // Universal Scaling Limits public static float MinHeight { get; private set; } = DefaultMinHeight; public static float MaxHeight { get; private set; } = DefaultMaxHeight; #endregion #region Settings private bool _settingUniversalScaling; public bool Setting_UniversalScaling { get => _settingUniversalScaling; set { if (_settingUniversalScaling == value) return; _settingUniversalScaling = value; if (_localAvatarScaler != null) _localAvatarScaler.UseTargetHeight = value; } } private bool _settingAnimationClipScalingOverride; public bool Setting_AnimationClipScalingOverride { get => _settingAnimationClipScalingOverride; set { if (_settingAnimationClipScalingOverride == value) return; _settingAnimationClipScalingOverride = value; if (_localAvatarScaler != null) _localAvatarScaler.overrideAnimationHeight = value; } } private bool _settingPersistentHeight; public bool Setting_PersistentHeight { get => _settingPersistentHeight; set { if (_settingPersistentHeight == value) return; _settingPersistentHeight = value; // if (_localAvatarScaler != null) // _localAvatarScaler.persistHeight = value; } } private float _lastTargetHeight = -1f; public float LastTargetHeight { get => _lastTargetHeight; set { _lastTargetHeight = value; ModSettings.EntryHiddenAvatarHeight.Value = _lastTargetHeight; } } #endregion #region Unity Events private void Awake() { if (Instance != null && Instance != this) { DestroyImmediate(this); return; } Instance = this; _networkedScalers = new Dictionary(); } private void Start() { _localAvatarScaler = PlayerSetup.Instance.gameObject.AddComponent(); _localAvatarScaler.Initialize(); _settingUniversalScaling = ModSettings.EntryUseUniversalScaling.Value; Setting_AnimationClipScalingOverride = ModSettings.EntryAnimationScalingOverride.Value; Setting_PersistentHeight = ModSettings.EntryPersistentHeight.Value; _lastTargetHeight = ModSettings.EntryPersistThroughRestart.Value ? ModSettings.EntryHiddenAvatarHeight.Value : -1f; // -1f is default // listen for events _localAvatarScaler.OnAnimatedHeightOverride += OnAnimationHeightOverride; CVRGameEventSystem.Instance.OnConnected.AddListener(OnInstanceConnected); } private void OnEnable() { if (_heightUpdateCoroutine != null) StopCoroutine(_heightUpdateCoroutine); _heightUpdateCoroutine = StartCoroutine(HeightUpdateCoroutine()); } private void OnDisable() { if (_heightUpdateCoroutine != null) StopCoroutine(_heightUpdateCoroutine); _heightUpdateCoroutine = null; } private void OnDestroy() { _heightUpdateCoroutine = null; CVRGameEventSystem.Instance.OnConnected.RemoveListener(OnInstanceConnected); if (_localAvatarScaler != null) Destroy(_localAvatarScaler); _localAvatarScaler = null; foreach (NetworkScaler scaler in _networkedScalers.Values) Destroy(scaler); _networkedScalers.Clear(); if (Instance == this) Instance = null; } // only update the height per scaler once per frame, to prevent spam & jitter // this is to ensure that the height is also set at correct time during frame, no matter when it is called private IEnumerator HeightUpdateCoroutine() { while (enabled) yield return _heightUpdateYield; // ReSharper disable once IteratorNeverReturns } #endregion #region Game Events public void OnInstanceConnected(string instanceId) { // TODO: need to know if this causes issues when in a reconnection loop SchedulerSystem.AddJob(ModNetwork.RequestHeightSync, 2f, 1f, 1); } #endregion #region Local Methods public void OnAvatarInstantiated(PlayerSetup playerSetup) { if (playerSetup._avatar == null) return; _localAvatarScaler.OnAvatarInstantiated(playerSetup._avatar, playerSetup._initialAvatarHeight, playerSetup.initialScale); if (!_settingUniversalScaling) return; SetTargetHeight(_lastTargetHeight); } public void OnAvatarDestroyed(PlayerSetup playerSetup) { if (_localAvatarScaler != null) _localAvatarScaler.OnAvatarDestroyed(); } public void SetTargetHeight(float targetHeight) { _lastTargetHeight = targetHeight; // save for persistent height if (_localAvatarScaler == null) return; _localAvatarScaler.TargetHeight = _lastTargetHeight; } public float GetHeight() { if (_localAvatarScaler == null) return PlayerAvatarPoint.defaultAvatarHeight; if (!_localAvatarScaler.IsForcingHeight()) return PlayerSetup.Instance.GetAvatarHeight(); return _localAvatarScaler.GetTargetHeight(); } public float GetAnimationClipHeight() { if (_localAvatarScaler == null) return PlayerAvatarPoint.defaultAvatarHeight; if (!_localAvatarScaler.IsForcingHeight()) return PlayerSetup.Instance.GetAvatarHeight(); return _localAvatarScaler.GetAnimatedHeight(); } public float GetHeightForNetwork() { if (!_settingUniversalScaling) return -1f; if (_localAvatarScaler == null) return -1f; if (!_localAvatarScaler.IsForcingHeight()) return -1f; return _localAvatarScaler.GetTargetHeight(); } public float GetInitialHeight() { if (_localAvatarScaler == null) return -1f; return _localAvatarScaler.GetInitialHeight(); } public bool IsHeightAdjustedFromInitial() { return _localAvatarScaler != null && _localAvatarScaler.IsForcingHeight(); } #endregion #region Network Methods public bool DoesNetworkHeightScalerExist(string playerId) => _networkedScalers.ContainsKey(playerId); public int GetNetworkHeightScalerCount() => _networkedScalers.Count; public float GetNetworkHeight(string playerId) { if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler)) if (scaler.IsForcingHeight()) return scaler.GetTargetHeight(); //doesn't have mod or has no custom height, get from player avatar directly CVRPlayerEntity playerEntity = CVRPlayerManager.Instance.NetworkPlayers.Find((players) => players.Uuid == playerId); if (playerEntity != null && playerEntity.PuppetMaster != null) return playerEntity.PuppetMaster.GetAvatarHeight(); // player is invalid??? return -1f; } // we will create a Universal Scaler for only users that send a height update // this is sent at a rate of 10s locally! internal void OnNetworkHeightUpdateReceived(string playerId, float targetHeight) { if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler)) scaler.TargetHeight = targetHeight; else SetupHeightScalerForNetwork(playerId, targetHeight); } internal void OnNetworkAvatarInstantiated(PuppetMaster puppetMaster) { var playerId = puppetMaster._playerDescriptor.ownerId; if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler)) scaler.OnAvatarInstantiated(puppetMaster.avatarObject, puppetMaster._initialAvatarHeight, puppetMaster.initialAvatarScale); } internal void OnNetworkAvatarDestroyed(PuppetMaster puppetMaster) { // on disconnect if (puppetMaster == null || puppetMaster._playerDescriptor == null) return; var playerId = puppetMaster._playerDescriptor.ownerId; if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler)) scaler.OnAvatarDestroyed(); } internal void RemoveNetworkHeightScaler(string playerId) { if (_networkedScalers.ContainsKey(playerId)) { AvatarScaleMod.Logger.Msg( $"Removed user height scaler! This is hopefully due to a disconnect or block. : {playerId}"); _networkedScalers.Remove(playerId); return; } AvatarScaleMod.Logger.Msg( $"Failed to remove a user height scaler! This shouldn't happen. : {playerId}"); } private void SetupHeightScalerForNetwork(string playerId, float targetHeight) { CVRPlayerEntity playerEntity = CVRPlayerManager.Instance.NetworkPlayers.Find(players => players.Uuid == playerId); PuppetMaster puppetMaster = playerEntity?.PuppetMaster; if (playerEntity == null || puppetMaster == null) { AvatarScaleMod.Logger.Error( $"Attempted to set up height scaler for user which does not exist! : {playerId}"); return; } AvatarScaleMod.Logger.Msg( $"Setting up new height scaler for user which has sent a height update! : {playerId}"); if (_networkedScalers.ContainsKey(playerId)) _networkedScalers.Remove(playerId); // ?? NetworkScaler scaler = puppetMaster.gameObject.AddComponent(); scaler.Initialize(playerId); scaler.OnAvatarInstantiated(puppetMaster.avatarObject, puppetMaster._initialAvatarHeight, puppetMaster.initialAvatarScale); _networkedScalers[playerId] = scaler; scaler.TargetHeight = targetHeight; } #endregion #region Manager Methods // sometimes fun to play with via UE public void SetUniversalScalingLimit(float min, float max) { const float HardCodedMinLimit = 0.01f; const float HardCodedMaxLimit = 100f; MinHeight = Mathf.Clamp(min, HardCodedMinLimit, HardCodedMaxLimit); MaxHeight = Mathf.Clamp(max, HardCodedMinLimit, HardCodedMaxLimit); AvatarScaleMod.Logger.Msg($"Universal Scaling Limits changed: {min} - {max}"); AvatarScaleMod.Logger.Warning("This will not network to other users unless they also have the same limits set!"); } public void ResetUniversalScalingLimit() { MinHeight = DefaultMinHeight; MaxHeight = DefaultMaxHeight; AvatarScaleMod.Logger.Msg("Universal Scaling Limits reset to default!"); } #endregion #region Event Listeners private static void OnAnimationHeightOverride(BaseScaler scaler) { AvatarScaleMod.Logger.Msg("AnimationClip-based avatar scaling detected. Disabling Universal Scaling."); CohtmlHud.Instance.ViewDropTextImmediate("(Local) AvatarScaleMod", "Avatar Scale Changed!", "Universal Scaling is now disabled in favor of built-in avatar scaling."); } #endregion }