diff --git a/ASTExtension/ASTExtension.csproj b/ASTExtension/ASTExtension.csproj new file mode 100644 index 0000000..78195e6 --- /dev/null +++ b/ASTExtension/ASTExtension.csproj @@ -0,0 +1,6 @@ + + + + ASTExtension + + diff --git a/ASTExtension/Extensions/PlayerSetupExtensions.cs b/ASTExtension/Extensions/PlayerSetupExtensions.cs new file mode 100644 index 0000000..dd8dd5c --- /dev/null +++ b/ASTExtension/Extensions/PlayerSetupExtensions.cs @@ -0,0 +1,17 @@ +using ABI_RC.Core.Player; +using UnityEngine; + +namespace NAK.ASTExtension.Extensions; + +public static class PlayerSetupExtensions +{ + // immediate measurement of the player's avatar height + public static float GetCurrentAvatarHeight(this PlayerSetup playerSetup) + { + Vector3 localScale = playerSetup._avatar.transform.localScale; + Vector3 initialScale = playerSetup.initialScale; + float initialHeight = playerSetup._initialAvatarHeight; + Vector3 scaleDifference = PlayerSetup.DivideVectors(localScale - initialScale, initialScale); + return initialHeight + initialHeight * scaleDifference.y; + } +} \ No newline at end of file diff --git a/ASTExtension/Main.cs b/ASTExtension/Main.cs new file mode 100644 index 0000000..756119e --- /dev/null +++ b/ASTExtension/Main.cs @@ -0,0 +1,365 @@ +using ABI_RC.Core.InteractionSystem; +using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; +using ABI_RC.Core.Util.AnimatorManager; +using ABI_RC.Systems.GameEventSystem; +using ABI_RC.Systems.InputManagement; +using ABI.CCK.Components; +using MelonLoader; +using NAK.ASTExtension.Extensions; +using UnityEngine; + +namespace NAK.ASTExtension; + +public class ASTExtensionMod : MelonMod +{ + private static MelonLogger.Instance Logger; + + #region Melon Preferences + + private static readonly MelonPreferences_Category Category = + MelonPreferences.CreateCategory(nameof(ASTExtension)); + + 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?"); + + private static readonly MelonPreferences_Entry EntryPersistThroughRestart = + Category.CreateEntry("persistent_height_through_restart", false, + "Persist Through Restart", "Should the avatar height persist between game restarts?"); + + private static readonly MelonPreferences_Entry EntryPersistFromUnsupported + = Category.CreateEntry("persist_from_unsupported", false, + "Persist From Unsupported", "Should the avatar height persist when the avatar is unsupported?"); + + // stores the last avatar height as a melon pref + 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() + { + Logger = LoggerInstance; + + InitializeSettings(); + InitializeScaleGesture(); + + CVRGameEventSystem.Avatar.OnLocalAvatarLoad.AddListener(OnLocalAvatarLoad); + CVRGameEventSystem.Avatar.OnLocalAvatarClear.AddListener(OnLocalAvatarClear); + } + + #endregion Melon Events + + #region Game Events + + private void OnLocalAvatarLoad(CVRAvatar _) + { + _currentAvatarSupported = IsAvatarSupported(); + if (!_currentAvatarSupported) + return; + + if (EntryUseCustomParameter.Value + && !string.IsNullOrEmpty(_parameterName)) + CalibrateCustomParameter(); + + if (EntryPersistThroughRestart.Value + && _lastHeight < 0) // has not been set + { + var lastHeight = EntryHiddenAvatarHeight.Value; + if (lastHeight > 0) SetAvatarHeight(lastHeight, true); + return; + } + + if (EntryPersistentHeight.Value + && _lastHeight > 0) // has been set + SetAvatarHeight(_lastHeight); + } + + private void OnLocalAvatarClear(CVRAvatar _) + { + _currentAvatarSupported = false; + + if (!EntryPersistentHeight.Value) + return; + + if (!IsAvatarSupported() + && !EntryPersistFromUnsupported.Value) + return; + + // update the last height + var height = PlayerSetup.Instance.GetCurrentAvatarHeight(); + _lastHeight = height; + EntryHiddenAvatarHeight.Value = height; + } + + #endregion Game Events + + #region Avatar Scale Tool + + // todo: tool needs a dedicated parameter name + //private const string ASTParameterName = "ASTHeight"; + //private const string ASTMotionParameterName = "#MotionScale"; + + //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 string _parameterName = "AvatarScale"; + + private float _lastHeight = -1f; + private float _minHeight = GlobalMinHeight; + private float _maxHeight = GlobalMaxHeight; + private bool _currentAvatarSupported; + + private float GetValueFromHeight(float height) + { + return Mathf.Clamp01((height - _minHeight) / (_maxHeight - _minHeight)); + } + + private float GetHeightFromValue(float value) + { + return Mathf.Lerp(_minHeight, _maxHeight, value); + } + + private void SetAvatarHeight(float height, bool immediate = false) + { + if (!IsAvatarSupported()) + return; + + AvatarAnimatorManager animatorManager = PlayerSetup.Instance.animatorManager; + if (!animatorManager.IsInitialized) + { + Logger.Error("AnimatorManager is not initialized!"); + return; + } + + if (!animatorManager.HasParameter(_parameterName)) + { + Logger.Error($"Parameter '{_parameterName}' does not exist!"); + return; + } + + 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) + { + Logger.Error("AnimatorManager is not initialized!"); + return false; + } + + if (!animatorManager.HasParameter(_parameterName)) + { + Logger.Error($"Parameter '{_parameterName}' does not exist!"); + return false; + } + + return true; + } + + #endregion Avatar Scale Tool + + #region Custom Parameter Calibration + + private void CalibrateCustomParameter() + { + AvatarAnimatorManager animatorManager = PlayerSetup.Instance.animatorManager; + if (!animatorManager.IsInitialized) + { + Logger.Error("AnimatorManager is not initialized!"); + return; + } + + if (!animatorManager.HasParameter(_parameterName)) + { + Logger.Error($"Parameter '{_parameterName}' does not exist!"); + 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}"); + } + + #endregion Custom Parameter Calibration + + #region Scale Reconizer + + // Require triggers to be down while doing fist - Exteratta + private readonly bool RequireTriggers = true; + + // Initial values when scale gesture is started + private float _initialModifier; + private float _initialTargetHeight; + + private void InitializeScaleGesture() + { + // This requires arms far outward- pull inward with fist and triggers. + // Release triggers while still holding fist to readjust. + + CVRGesture gesture = new() + { + name = "astExtensionIn", + 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, + needsToBeInView = true + }); + gesture.onStart.AddListener(OnScaleStart); + gesture.onStay.AddListener(OnScaleStay); + CVRGestureRecognizer.Instance.gestures.Add(gesture); + + gesture = new CVRGesture + { + name = "astExtensionOut", + 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, + needsToBeInView = true + }); + gesture.onStart.AddListener(OnScaleStart); + gesture.onStay.AddListener(OnScaleStay); + CVRGestureRecognizer.Instance.gestures.Add(gesture); + } + + private void OnScaleStart(float modifier, Transform transform1, Transform transform2) + { + if (!_currentAvatarSupported) + return; + + if (!EntryUseScaleGesture.Value) + return; + + // Store initial modifier so we can get difference later + _initialModifier = Mathf.Max(modifier, 0.01f); // no zero + _initialTargetHeight = PlayerSetup.Instance.GetCurrentAvatarHeight(); + } + + private void OnScaleStay(float modifier, Transform transform1, Transform transform2) + { + if (!_currentAvatarSupported) + return; + + if (!EntryUseScaleGesture.Value) + return; + + modifier = Mathf.Max(modifier, 0.01f); // no zero + + // Allow user to release triggers to reset "world grip" + if (RequireTriggers && !AreBothTriggersDown()) + { + _initialModifier = modifier; + _initialTargetHeight = PlayerSetup.Instance.GetCurrentAvatarHeight(); + return; + } + + // Invert so the gesture is more of a world squish instead of happy hug + var modifierRatio = 1f / (modifier / _initialModifier); + + // Determine the adjustment factor for the height, this will be >1 if scaling up, <1 if scaling down. + var heightAdjustmentFactor = modifierRatio > 1 ? 1 + (modifierRatio - 1) : 1 - (1 - modifierRatio); + + // Apply the adjustment to the target height + var targetHeight = _initialTargetHeight * heightAdjustmentFactor; + targetHeight = Mathf.Clamp(targetHeight, _minHeight, _maxHeight); + SetAvatarHeight(targetHeight); + } + + 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; + } + + #endregion Scale Reconizer +} \ No newline at end of file diff --git a/ASTExtension/Properties/AssemblyInfo.cs b/ASTExtension/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..747a7d8 --- /dev/null +++ b/ASTExtension/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using MelonLoader; +using NAK.ASTExtension.Properties; +using System.Reflection; + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.ASTExtension))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.ASTExtension))] + +[assembly: MelonInfo( + typeof(NAK.ASTExtension.ASTExtensionMod), + nameof(NAK.ASTExtension), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ASTExtension" +)] + +[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] +[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] +[assembly: MelonColor(255, 246, 25, 99)] // red-pink +[assembly: MelonAuthorColor(255, 158, 21, 32)] // red +[assembly: HarmonyDontPatchAll] + +namespace NAK.ASTExtension.Properties; +internal static class AssemblyInfoParams +{ + public const string Version = "1.0.0"; + public const string Author = "NotAKidoS"; +} \ No newline at end of file diff --git a/ASTExtension/README.md b/ASTExtension/README.md new file mode 100644 index 0000000..3957ef3 --- /dev/null +++ b/ASTExtension/README.md @@ -0,0 +1,20 @@ +# ASTExtension + +Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool): +- VR Gesture to scale +- Match IRL height +- Persistent height +- Copy height from others + +Requires setup in Unity. This is **not** Universal Scaling. + +--- + +Here is the block of text where I tell you this mod is not affiliated with or endorsed by ABI. +https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games + +> This mod is an independent creation not affiliated with, supported by, or approved by Alpha Blend Interactive. + +> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use. + +> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive. diff --git a/ASTExtension/format.json b/ASTExtension/format.json new file mode 100644 index 0000000..466398d --- /dev/null +++ b/ASTExtension/format.json @@ -0,0 +1,24 @@ +{ + "_id": -1, + "name": "ASTExtension", + "modversion": "1.0.0", + "gameversion": "2024r175", + "loaderversion": "0.6.1", + "modtype": "Mod", + "author": "NotAKidoS", + "description": "Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool):\n- VR Gesture to scale\n- Match IRL height\n- Persistent height\n- Copy height from others\n\nRequires setup in Unity. This is **not** Universal Scaling.", + "searchtags": [ + "tool", + "scaling", + "height", + "extension", + "avatar" + ], + "requirements": [ + "None" + ], + "downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r33/ASTExtension.dll", + "sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ASTExtension/", + "changelog": "- Initial release", + "embedcolor": "#f61963" +} \ No newline at end of file diff --git a/NAK_CVR_Mods.sln b/NAK_CVR_Mods.sln index a01b4b8..3724659 100644 --- a/NAK_CVR_Mods.sln +++ b/NAK_CVR_Mods.sln @@ -77,6 +77,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteractionTest", "Interact EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeepVelocityOnExitFlight", "KeepVelocityOnExitFlight\KeepVelocityOnExitFlight.csproj", "{0BB3D187-BBBA-4C58-B246-102342BE5E8C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ASTExtension", "ASTExtension\ASTExtension.csproj", "{6580AA87-6A95-438E-A5D3-70E583CCD77B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -231,6 +233,10 @@ Global {0BB3D187-BBBA-4C58-B246-102342BE5E8C}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BB3D187-BBBA-4C58-B246-102342BE5E8C}.Release|Any CPU.ActiveCfg = Release|Any CPU {0BB3D187-BBBA-4C58-B246-102342BE5E8C}.Release|Any CPU.Build.0 = Release|Any CPU + {6580AA87-6A95-438E-A5D3-70E583CCD77B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6580AA87-6A95-438E-A5D3-70E583CCD77B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6580AA87-6A95-438E-A5D3-70E583CCD77B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6580AA87-6A95-438E-A5D3-70E583CCD77B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE