diff --git a/ASTExtension/ASTExtension.csproj b/ASTExtension/ASTExtension.csproj index 78195e6..c58cf25 100644 --- a/ASTExtension/ASTExtension.csproj +++ b/ASTExtension/ASTExtension.csproj @@ -3,4 +3,12 @@ ASTExtension + + + ..\.ManagedLibs\BTKUILib.dll + + + + + diff --git a/ASTExtension/Extensions/PlayerSetupExtensions.cs b/ASTExtension/Extensions/PlayerSetupExtensions.cs index dd8dd5c..241cd1e 100644 --- a/ASTExtension/Extensions/PlayerSetupExtensions.cs +++ b/ASTExtension/Extensions/PlayerSetupExtensions.cs @@ -8,6 +8,12 @@ public static class PlayerSetupExtensions // immediate measurement of the player's avatar height public static float GetCurrentAvatarHeight(this PlayerSetup playerSetup) { + if (playerSetup._avatar == null) + { + ASTExtensionMod.Logger.Error("GetCurrentAvatarHeight: Avatar is null"); + return 0f; + } + Vector3 localScale = playerSetup._avatar.transform.localScale; Vector3 initialScale = playerSetup.initialScale; float initialHeight = playerSetup._initialAvatarHeight; diff --git a/ASTExtension/Integrations/BTKUI/BtkUiAddon.cs b/ASTExtension/Integrations/BTKUI/BtkUiAddon.cs new file mode 100644 index 0000000..8c57cb7 --- /dev/null +++ b/ASTExtension/Integrations/BTKUI/BtkUiAddon.cs @@ -0,0 +1,56 @@ +using ABI_RC.Core.Player; +using BTKUILib; +using BTKUILib.UIObjects; +using BTKUILib.UIObjects.Components; + +namespace NAK.ASTExtension.Integrations +{ + public static partial class BtkUiAddon + { + public static void Initialize() + { + Prepare_Icons(); + Setup_PlayerSelectPage(); + } + + private static void Prepare_Icons() + { + QuickMenuAPI.PrepareIcon(ASTExtensionMod.ModName, "ASM_Icon_AvatarHeightCopy", + GetIconStream("ASM_Icon_AvatarHeightCopy.png")); + } + + #region Player Select Page + + private static string _selectedPlayer; + + private static void Setup_PlayerSelectPage() + { + QuickMenuAPI.OnPlayerSelected += OnPlayerSelected; + Category category = QuickMenuAPI.PlayerSelectPage.AddCategory(ASTExtensionMod.ModName, ASTExtensionMod.ModName); + Button button = category.AddButton("Copy Height", "ASM_Icon_AvatarHeightCopy", "Copy selected players Eye Height."); + button.OnPress += OnCopyPlayerHeight; + } + + private static void OnPlayerSelected(string _, string id) + { + _selectedPlayer = id; + } + + private static void OnCopyPlayerHeight() + { + if (string.IsNullOrEmpty(_selectedPlayer)) + return; + + if (!CVRPlayerManager.Instance.GetPlayerPuppetMaster(_selectedPlayer, out PuppetMaster player)) + return; + + if (player._avatar == null) + return; + + float height = player.netIkController.GetRemoteHeight(); + ASTExtensionMod.Instance.SetAvatarHeight(height); + } + + #endregion Player Select Page + } +} \ No newline at end of file diff --git a/ASTExtension/Integrations/BTKUI/BtkUiAddon_Utils.cs b/ASTExtension/Integrations/BTKUI/BtkUiAddon_Utils.cs new file mode 100644 index 0000000..3dd1c30 --- /dev/null +++ b/ASTExtension/Integrations/BTKUI/BtkUiAddon_Utils.cs @@ -0,0 +1,18 @@ +using System.Reflection; + +namespace NAK.ASTExtension.Integrations +{ + public static partial class BtkUiAddon + { + #region Icon Utils + + private static Stream GetIconStream(string iconName) + { + Assembly assembly = Assembly.GetExecutingAssembly(); + string assemblyName = assembly.GetName().Name; + return assembly.GetManifestResourceStream($"{assemblyName}.Resources.{iconName}"); + } + + #endregion Icon Utils + } +} \ No newline at end of file diff --git a/ASTExtension/Main.cs b/ASTExtension/Main.cs index f395688..1f09f3b 100644 --- a/ASTExtension/Main.cs +++ b/ASTExtension/Main.cs @@ -7,6 +7,7 @@ using ABI_RC.Core.Util.AnimatorManager; using ABI_RC.Systems.GameEventSystem; using ABI_RC.Systems.InputManagement; using ABI.CCK.Components; +using ABI.CCK.Scripts; using HarmonyLib; using MelonLoader; using NAK.ASTExtension.Extensions; @@ -16,25 +17,20 @@ namespace NAK.ASTExtension; public class ASTExtensionMod : MelonMod { - private static MelonLogger.Instance Logger; + internal static ASTExtensionMod Instance; // lazy + internal static MelonLogger.Instance Logger; #region Melon Preferences + internal const string ModName = nameof(ASTExtension); + private static readonly MelonPreferences_Category Category = - MelonPreferences.CreateCategory(nameof(ASTExtension)); + MelonPreferences.CreateCategory(ModName); private static readonly MelonPreferences_Entry EntryUseScaleGesture = Category.CreateEntry("use_scale_gesture", true, "Use Scale Gesture", "Use the scale gesture to adjust your avatar's height."); - private static readonly MelonPreferences_Entry EntryUseCustomParameter = - Category.CreateEntry("use_custom_parameter", false, - "Use Custom Parameter", "Use a custom parameter to adjust your avatar's height."); - - private static readonly MelonPreferences_Entry EntryCustomParameterName = - Category.CreateEntry("custom_parameter_name", "AvatarScale", - "Custom Parameter Name", "The name of the custom parameter to use for height adjustment."); - private static readonly MelonPreferences_Entry EntryPersistentHeight = Category.CreateEntry("persistent_height", false, "Persistent Height", "Should the avatar height persist between avatar switches?"); @@ -51,79 +47,58 @@ public class ASTExtensionMod : MelonMod private static readonly MelonPreferences_Entry EntryHiddenAvatarHeight = Category.CreateEntry("hidden_avatar_height", -2f, is_hidden: true); - private void InitializeSettings() - { - EntryUseCustomParameter.OnEntryValueChangedUntyped.Subscribe(OnUseCustomParameterChanged); - EntryCustomParameterName.OnEntryValueChangedUntyped.Subscribe(OnCustomParameterNameChanged); - OnUseCustomParameterChanged(); - } - - private void OnUseCustomParameterChanged(object oldValue = null, object newValue = null) - { - SetupCustomParameter(); - } - - private void OnCustomParameterNameChanged(object oldValue = null, object newValue = null) - { - SetupCustomParameter(); - } - - private void SetupCustomParameter() - { - // use custom parameter - if (EntryUseCustomParameter.Value) - { - _parameterName = EntryCustomParameterName.Value; - CalibrateCustomParameter(); - return; - } - - // reset to default - _parameterName = "AvatarScale"; - _minHeight = GlobalMinHeight; - _maxHeight = GlobalMaxHeight; - } - #endregion Melon Preferences #region Melon Events public override void OnInitializeMelon() { + Instance = this; Logger = LoggerInstance; - InitializeSettings(); + //InitializeSettings(); CVRGameEventSystem.Avatar.OnLocalAvatarLoad.AddListener(OnLocalAvatarLoad); CVRGameEventSystem.Avatar.OnLocalAvatarClear.AddListener(OnLocalAvatarClear); MelonCoroutines.Start(WaitForGestureRecogniser()); // todo: once stable, use initialization game event + + InitializeIntegration("BTKUILib", Integrations.BtkUiAddon.Initialize); } + private static void InitializeIntegration(string modName, Action integrationAction) + { + if (RegisteredMelons.All(it => it.Info.Name != modName)) + return; + + Logger.Msg($"Initializing {modName} integration."); + integrationAction.Invoke(); + } + + #endregion Melon Events + + #region Game Events + private IEnumerator WaitForGestureRecogniser() { yield return new WaitUntil(() => CVRGestureRecognizer.Instance); InitializeScaleGesture(); } - #endregion Melon Events - - #region Game Events - private void OnLocalAvatarLoad(CVRAvatar _) { - _currentAvatarSupported = IsAvatarSupported(); - if (!_currentAvatarSupported) + if (!FindSupportedParameter(out string parameterName)) return; - - if (EntryUseCustomParameter.Value - && !string.IsNullOrEmpty(_parameterName)) - CalibrateCustomParameter(); + + if (!AttemptCalibrateParameter(parameterName, out float minHeight, out float maxHeight, out float modifier)) + return; + + SetupParameter(parameterName, minHeight, maxHeight, modifier); if (EntryPersistThroughRestart.Value && _lastHeight < 0) // has not been set { var lastHeight = EntryHiddenAvatarHeight.Value; - if (lastHeight > 0) SetAvatarHeight(lastHeight, true); + if (lastHeight > 0) SetAvatarHeight(lastHeight); return; } @@ -132,107 +107,178 @@ public class ASTExtensionMod : MelonMod SetAvatarHeight(_lastHeight); } - private void OnLocalAvatarClear(CVRAvatar _) + private void OnLocalAvatarClear(CVRAvatar avatar) { - _currentAvatarSupported = false; - if (!EntryPersistentHeight.Value) + { + ResetParameter(); return; + } - if (!IsAvatarSupported() + if (!_currentAvatarSupported && !EntryPersistFromUnsupported.Value) return; // update the last height - var height = PlayerSetup.Instance.GetCurrentAvatarHeight(); - _lastHeight = height; - EntryHiddenAvatarHeight.Value = height; + if (avatar != null) StoreLastHeight(PlayerSetup.Instance.GetCurrentAvatarHeight()); } #endregion Game Events - #region Avatar Scale Tool - - // todo: tool needs a dedicated parameter name - //private const string ASTParameterName = "ASTHeight"; - //private const string ASTMotionParameterName = "#MotionScale"; + #region Avatar Scale Tool Extension + + private static HashSet SUPPORTED_PARAMETERS = new() + { + "AvatarScale", // default + "Scale", // most common + "Scale/Scale", // kafe + "Scaler", // momo + "Height", // loliwurt + "LoliModifier" // avatar + }; //https://github.com/NotAKidoS/AvatarScaleTool/blob/eaa6d343f916b9bb834bb30989fc6987680492a2/AvatarScaleTool/Editor/Scripts/AvatarScaleTool.cs#L13-L14 - private const float GlobalMinHeight = 0.25f; - private const float GlobalMaxHeight = 2.5f; + private const float DEFAULT_MIN_HEIGHT = 0.25f; + private const float DEFAULT_MAX_HEIGHT = 2.5f; - private string _parameterName = "AvatarScale"; - - private float _lastHeight = -1f; - private float _minHeight = GlobalMinHeight; - private float _maxHeight = GlobalMaxHeight; private bool _currentAvatarSupported; + private string _parameterName = SUPPORTED_PARAMETERS.First(); + + private float _minHeight = DEFAULT_MIN_HEIGHT; + private float _maxHeight = DEFAULT_MAX_HEIGHT; + private float _modifier = 1f; + private float _lastHeight = -1f; + + private void SetupParameter(string parameterName, float minHeight, float maxHeight, float modifier) + { + _parameterName = parameterName; + _minHeight = minHeight; + _maxHeight = maxHeight; + _modifier = modifier; + _currentAvatarSupported = true; + } + + private void ResetParameter() + { + _parameterName = SUPPORTED_PARAMETERS.First(); + _minHeight = DEFAULT_MIN_HEIGHT; + _maxHeight = DEFAULT_MAX_HEIGHT; + _modifier = 1f; + _currentAvatarSupported = false; + } + + private void StoreLastHeight(float height) + { + _lastHeight = height; + EntryHiddenAvatarHeight.Value = height; + } private float GetValueFromHeight(float height) { - return Mathf.Clamp01((height - _minHeight) / (_maxHeight - _minHeight)); + return Mathf.Sign(_modifier) > 0 // negative means min & max heights were swapped (because i said so) + ? Mathf.Clamp01((height - _minHeight) / (_maxHeight - _minHeight)) * Mathf.Abs(_modifier) + : 1 - Mathf.Clamp01((height - _minHeight) / (_maxHeight - _minHeight)) * Mathf.Abs(_modifier); } - private float GetHeightFromValue(float value) + // private float GetHeightFromValue(float value) + // => Mathf.Lerp(_minHeight, _maxHeight, value * Mathf.Abs(_modifier)); + + private static bool FindSupportedParameter(out string parameterName) { - return Mathf.Lerp(_minHeight, _maxHeight, value); - } - - private void SetAvatarHeight(float height, bool immediate = false) - { - if (!IsAvatarSupported()) - return; - + parameterName = null; + AvatarAnimatorManager animatorManager = PlayerSetup.Instance.animatorManager; if (!animatorManager.IsInitialized) { Logger.Error("AnimatorManager is not initialized!"); - return; + return false; } - if (!animatorManager.HasParameter(_parameterName)) + var parameterSet = new HashSet(animatorManager.Parameters.Keys, StringComparer.OrdinalIgnoreCase); + foreach (var parameter in SUPPORTED_PARAMETERS) { - Logger.Error($"Parameter '{_parameterName}' does not exist!"); - return; + if (!parameterSet.Contains(parameter)) continue; + parameterName = parameterSet.First(p => p.Equals(parameter, StringComparison.OrdinalIgnoreCase)); + Logger.Msg($"Found supported parameter '{parameterName}'"); + return true; + } + + Logger.Error("No supported parameter found!"); + return false; + } + + private static bool AttemptCalibrateParameter(string parameterName, + out float minHeight, out float maxHeight, out float modifier) + { + minHeight = 0f; + maxHeight = 0f; + modifier = 1f; + + AvatarAnimatorManager animatorManager = PlayerSetup.Instance.animatorManager; + if (!animatorManager.IsInitialized) + { + Logger.Error("AnimatorManager is not initialized!"); + return false; + } + + if (string.IsNullOrEmpty(parameterName)) + { + Logger.Error("Parameter name is empty!"); + return false; + } + + if (!animatorManager.HasParameter(parameterName)) + { + Logger.Error($"Parameter '{parameterName}' does not exist!"); + return false; } Animator animator = animatorManager.Animator; - var value = GetValueFromHeight(height); - animator.SetFloat(_parameterName, value); - if (immediate) animator.Update(0f); // apply - - _lastHeight = height; // session - EntryHiddenAvatarHeight.Value = height; // persistent - - // update in menus - CVR_MenuManager.Instance.SendAdvancedAvatarUpdate(_parameterName, value); - } - - private bool IsAvatarSupported() - { - // check if avatar has the parameter - AvatarAnimatorManager animatorManager = PlayerSetup.Instance.animatorManager; - if (!animatorManager.IsInitialized) + animatorManager.GetParameter(parameterName, out float initialValue); + + // set min height to 0 + animator.SetFloat(parameterName, 0f); + animator.Update(0f); // apply + minHeight = PlayerSetup.Instance.GetCurrentAvatarHeight(); + + // set max height to 1++ + for (int i = 1; i <= 10; i++) { - Logger.Error("AnimatorManager is not initialized!"); + animator.SetFloat(parameterName, i); + animator.Update(0f); // apply + var height = PlayerSetup.Instance.GetCurrentAvatarHeight(); + if (height <= maxHeight) break; // stop if height is not increasing + modifier = i; + maxHeight = height; + } + + // reset the parameter to its initial value + animator.SetFloat(parameterName, initialValue); + animator.Update(0f); // apply + + // check if there was no change + if (Math.Abs(minHeight - maxHeight) < float.Epsilon) + { + Logger.Error("Calibration failed: min height is equal to max height!"); return false; } - - if (!animatorManager.HasParameter(_parameterName)) + + // swap if needed + if (minHeight > maxHeight) { - Logger.Error($"Parameter '{_parameterName}' does not exist!"); - return false; + (minHeight, maxHeight) = (maxHeight, minHeight); + modifier = -modifier; // invert } - + + Logger.Msg($"Calibrated custom parameter '{parameterName}' with min height {minHeight} and max height {maxHeight} using modifier {modifier}"); return true; } - - #endregion Avatar Scale Tool - - #region Custom Parameter Calibration - - private void CalibrateCustomParameter() + + internal void SetAvatarHeight(float height) { + if (!_currentAvatarSupported) + return; + AvatarAnimatorManager animatorManager = PlayerSetup.Instance.animatorManager; if (!animatorManager.IsInitialized) { @@ -246,28 +292,15 @@ public class ASTExtensionMod : MelonMod return; } - Animator animator = animatorManager.Animator; // we get from animator manager to ensure we have *profile* param - animatorManager.GetParameter(_parameterName, out float initialValue); - - // set min height to 0 - animator.SetFloat(_parameterName, 0f); - animator.Update(0f); // apply - var minHeight = PlayerSetup.Instance.GetCurrentAvatarHeight(); - - // set max height to 1 - animator.SetFloat(_parameterName, 1f); - animator.Update(0f); // apply - var maxHeight = PlayerSetup.Instance.GetCurrentAvatarHeight(); - - // reset the parameter to its initial value - animator.SetFloat(_parameterName, initialValue); - animator.Update(0f); // apply - - Logger.Msg( - $"Calibrated custom parameter '{_parameterName}' with min height {minHeight} and max height {maxHeight}"); + StoreLastHeight(height); + + var value = GetValueFromHeight(height); + animatorManager.SetParameter(_parameterName, value); + animatorManager.Animator.Update(0f); // apply + CVR_MenuManager.Instance.SendAdvancedAvatarUpdate(_parameterName, value); // update AAS menus } - #endregion Custom Parameter Calibration + #endregion Avatar Scale Tool Extension #region Scale Reconizer diff --git a/ASTExtension/README.md b/ASTExtension/README.md index 3957ef3..04179f0 100644 --- a/ASTExtension/README.md +++ b/ASTExtension/README.md @@ -2,7 +2,6 @@ Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool): - VR Gesture to scale -- Match IRL height - Persistent height - Copy height from others diff --git a/ASTExtension/Resources/ASM_Icon_AvatarHeightCopy.png b/ASTExtension/Resources/ASM_Icon_AvatarHeightCopy.png new file mode 100644 index 0000000..e665a6d Binary files /dev/null and b/ASTExtension/Resources/ASM_Icon_AvatarHeightCopy.png differ