From 3b830d31c4fba7523ff631d677ccf1293c03c508 Mon Sep 17 00:00:00 2001
From: NotAKidoS <37721153+NotAKidoS@users.noreply.github.com>
Date: Sat, 29 Jun 2024 22:01:52 -0500
Subject: [PATCH] ASTExtension: Initial release
---
ASTExtension/ASTExtension.csproj | 6 +
.../Extensions/PlayerSetupExtensions.cs | 17 +
ASTExtension/Main.cs | 365 ++++++++++++++++++
ASTExtension/Properties/AssemblyInfo.cs | 32 ++
ASTExtension/README.md | 20 +
ASTExtension/format.json | 24 ++
NAK_CVR_Mods.sln | 6 +
7 files changed, 470 insertions(+)
create mode 100644 ASTExtension/ASTExtension.csproj
create mode 100644 ASTExtension/Extensions/PlayerSetupExtensions.cs
create mode 100644 ASTExtension/Main.cs
create mode 100644 ASTExtension/Properties/AssemblyInfo.cs
create mode 100644 ASTExtension/README.md
create mode 100644 ASTExtension/format.json
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