From 5c7f7ab265e9749339118c4de8b51282ff555b5d Mon Sep 17 00:00:00 2001 From: NotAKidoS <37721153+NotAKidoS@users.noreply.github.com> Date: Sun, 30 Jun 2024 16:46:04 -0500 Subject: [PATCH] ASTExtension: Removed need for Avatar Scale Tool --- ASTExtension/ASTExtension.csproj | 8 + .../Extensions/PlayerSetupExtensions.cs | 6 + ASTExtension/Integrations/BTKUI/BtkUiAddon.cs | 56 ++++ .../Integrations/BTKUI/BtkUiAddon_Utils.cs | 18 ++ ASTExtension/Main.cs | 305 ++++++++++-------- ASTExtension/README.md | 1 - .../Resources/ASM_Icon_AvatarHeightCopy.png | Bin 0 -> 15721 bytes 7 files changed, 257 insertions(+), 137 deletions(-) create mode 100644 ASTExtension/Integrations/BTKUI/BtkUiAddon.cs create mode 100644 ASTExtension/Integrations/BTKUI/BtkUiAddon_Utils.cs create mode 100644 ASTExtension/Resources/ASM_Icon_AvatarHeightCopy.png 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 0000000000000000000000000000000000000000..e665a6d5a8f0dbcba5397b00b462fd9c4cbe86d4 GIT binary patch literal 15721 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 Y