From cb26ab1e6cc2745d0659c537cca29983fc0fd987 Mon Sep 17 00:00:00 2001 From: SDraw Date: Sat, 11 Feb 2023 13:49:50 +0300 Subject: [PATCH] Automatic locomotion mass center Animated bend normal while jumping/flying Arm weights fix Mod remake --- README.md | 8 +- ml_amt/MotionTweaker.cs | 75 +++++--- ml_amt/README.md | 7 +- ml_amt/Settings.cs | 19 +- ml_amt/resources/menu.js | 7 + ml_lme/LeapTracked.cs | 20 ++- ml_lme/Properties/AssemblyInfo.cs | 6 +- ml_lme/README.md | 10 +- ml_pam/ArmMover.cs | 279 ++++++++++++++++++++++++++---- ml_pam/Main.cs | 61 +++++-- ml_pam/Properties/AssemblyInfo.cs | 7 +- ml_pam/README.md | 7 +- ml_pam/Scripts.cs | 26 +++ ml_pam/Settings.cs | 113 ++++++++++++ ml_pam/ml_pam.csproj | 15 ++ ml_pam/resources/menu.js | 209 ++++++++++++++++++++++ 16 files changed, 773 insertions(+), 96 deletions(-) create mode 100644 ml_pam/Scripts.cs create mode 100644 ml_pam/Settings.cs create mode 100644 ml_pam/resources/menu.js diff --git a/README.md b/README.md index 87c9637..9e1ab38 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ Merged set of MelonLoader mods for ChilloutVR. | Full name | Short name | Latest version | Available in [CVRMA](https://github.com/knah/CVRMelonAssistant) | Current Status | Notes | |-----------|------------|----------------|-----------------------------------------------------------------|----------------|-------| | Avatar Change Info | ml_aci | 1.0.3 | Retired | Retired | Superseded by `Extended Game Notifications` -| Avatar Motion Tweaker | ml_amt | 1.2.2 | Yes | Working | +| Avatar Motion Tweaker | ml_amt | 1.2.3 | Yes, update review | Working | | Desktop Head Tracking | ml_dht | 1.1.1 | Yes | Working | | Desktop Reticle Switch | ml_drs | 1.0.0 | Yes | Working | -| Extended Game Notifications | ml_egn | 1.0.1 | Yes, update review | Working +| Extended Game Notifications | ml_egn | 1.0.1 | Yes | Working | Four Point Tracking | ml_fpt | 1.0.9 | Retired | Deprecated | In-game feature since 2022r170 update -| Leap Motion Extension | ml_lme | 1.3.0 | Yes, update review | Working | -| Pickup Arm Movement | ml_pam | 1.0.0 | Retired | Retired | No desire for development +| Leap Motion Extension | ml_lme | 1.3.1 | Yes, update review | Working | +| Pickup Arm Movement | ml_pam | 1.0.1 | Retired, update review | Working | | Server Connection Info | ml_sci | 1.0.2 | Retired | Retired | Superseded by `Extended Game Notifications` diff --git a/ml_amt/MotionTweaker.cs b/ml_amt/MotionTweaker.cs index c21e6cf..6d45664 100644 --- a/ml_amt/MotionTweaker.cs +++ b/ml_amt/MotionTweaker.cs @@ -11,8 +11,10 @@ namespace ml_amt [DisallowMultipleComponent] class MotionTweaker : MonoBehaviour { + static readonly Vector4 ms_pointVector = new Vector4(0f, 0f, 0f, 1f); static readonly FieldInfo ms_grounded = typeof(MovementSystem).GetField("_isGrounded", BindingFlags.NonPublic | BindingFlags.Instance); static readonly FieldInfo ms_groundedRaw = typeof(MovementSystem).GetField("_isGroundedRaw", BindingFlags.NonPublic | BindingFlags.Instance); + static readonly FieldInfo ms_hasToes = typeof(IKSolverVR).GetField("hasToes", BindingFlags.NonPublic | BindingFlags.Instance); static readonly int ms_emoteHash = Animator.StringToHash("Emote"); enum PoseState @@ -22,17 +24,18 @@ namespace ml_amt Proning } - static readonly Vector4 ms_pointVector = new Vector4(0f, 0f, 0f, 1f); - VRIK m_vrIk = null; int m_locomotionLayer = 0; float m_ikWeight = 1f; // Original weight float m_locomotionWeight = 1f; // Original weight bool m_plantFeet = false; // Original plant feet float m_avatarScale = 1f; // Instantiated scale + Vector3 m_locomotionOffset = Vector3.zero; // Original locomotion offset + bool m_bendNormalLeft = false; + bool m_bendNormalRight = false; Transform m_avatarHips = null; float m_viewPointHeight = 1f; - bool m_isInVR = false; + bool m_inVR = false; bool m_avatarReady = false; bool m_compatibleAvatar = false; @@ -55,15 +58,14 @@ namespace ml_amt bool m_ikOverrideFly = true; bool m_ikOverrideJump = true; - bool m_customLocomotionOffset = false; - Vector3 m_locomotionOffset = Vector3.zero; - bool m_detectEmotes = true; bool m_emoteActive = false; bool m_followHips = true; Vector3 m_hipsToPlayer = Vector3.zero; + Vector3 m_massCenter = Vector3.zero; + readonly List m_parameters = null; internal MotionTweaker() @@ -73,7 +75,7 @@ namespace ml_amt void Start() { - m_isInVR = Utils.IsInVR(); + m_inVR = Utils.IsInVR(); Settings.IKOverrideCrouchChange += this.SetIKOverrideCrouch; Settings.CrouchLimitChange += this.SetCrouchLimit; @@ -85,6 +87,7 @@ namespace ml_amt Settings.IKOverrideJumpChange += this.SetIKOverrideJump; Settings.DetectEmotesChange += this.SetDetectEmotes; Settings.FollowHipsChange += this.SetFollowHips; + Settings.MassCenterChange += this.SetMassCenter; } void OnDestroy() @@ -99,6 +102,7 @@ namespace ml_amt Settings.IKOverrideJumpChange -= this.SetIKOverrideJump; Settings.DetectEmotesChange -= this.SetDetectEmotes; Settings.FollowHipsChange -= this.SetFollowHips; + Settings.MassCenterChange -= this.SetMassCenter; } void Update() @@ -110,7 +114,7 @@ namespace ml_amt m_moving = !Mathf.Approximately(MovementSystem.Instance.movementVector.magnitude, 0f); // Update upright - Matrix4x4 l_hmdMatrix = PlayerSetup.Instance.transform.GetMatrix().inverse * (m_isInVR ? PlayerSetup.Instance.vrHeadTracker.transform.GetMatrix() : PlayerSetup.Instance.desktopCameraRig.transform.GetMatrix()); + Matrix4x4 l_hmdMatrix = PlayerSetup.Instance.transform.GetMatrix().inverse * (m_inVR ? PlayerSetup.Instance.vrHeadTracker.transform.GetMatrix() : PlayerSetup.Instance.desktopCameraRig.transform.GetMatrix()); float l_currentHeight = Mathf.Clamp((l_hmdMatrix * ms_pointVector).y, 0f, float.MaxValue); float l_avatarScale = (m_avatarScale > 0f) ? (PlayerSetup.Instance._avatar.transform.localScale.y / m_avatarScale) : 0f; float l_avatarViewHeight = Mathf.Clamp(m_viewPointHeight * l_avatarScale, 0f, float.MaxValue); @@ -123,7 +127,7 @@ namespace ml_amt m_hipsToPlayer.Set(l_hipsToPoint.x, 0f, l_hipsToPoint.z); } - if(m_isInVR && (m_vrIk != null) && m_vrIk.enabled) + if(m_inVR && (m_vrIk != null) && m_vrIk.enabled) { if(m_adjustedMovement) { @@ -170,20 +174,20 @@ namespace ml_amt m_poseState = PoseState.Standing; m_customCrouchLimit = false; m_customProneLimit = false; - m_customLocomotionOffset = false; - m_locomotionOffset = Vector3.zero; m_avatarScale = 1f; + m_locomotionOffset = Vector3.zero; m_emoteActive = false; m_moving = false; m_hipsToPlayer = Vector3.zero; m_avatarHips = null; m_viewPointHeight = 1f; + m_massCenter = Vector3.zero; m_parameters.Clear(); } internal void OnSetupAvatar() { - m_isInVR = Utils.IsInVR(); + m_inVR = Utils.IsInVR(); m_vrIk = PlayerSetup.Instance._avatar.GetComponent(); m_locomotionLayer = PlayerSetup.Instance._animator.GetLayerIndex("Locomotion/Emotes"); m_avatarHips = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.Hips); @@ -222,15 +226,31 @@ namespace ml_amt m_customProneLimit = (l_customTransform != null); m_proneLimit = m_customProneLimit ? Mathf.Clamp01(l_customTransform.localPosition.y) : Settings.ProneLimit; - l_customTransform = PlayerSetup.Instance._avatar.transform.Find("LocomotionOffset"); - m_customLocomotionOffset = (l_customTransform != null); - m_locomotionOffset = m_customLocomotionOffset ? l_customTransform.localPosition : Vector3.zero; - // Apply VRIK tweaks if(m_vrIk != null) { - if(m_customLocomotionOffset) - m_vrIk.solver.locomotion.offset = m_locomotionOffset; + m_locomotionOffset = m_vrIk.solver.locomotion.offset; + m_massCenter = m_locomotionOffset; + + if((bool)ms_hasToes.GetValue(m_vrIk.solver)) + { + Transform l_foot = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.LeftFoot); + if(l_foot == null) + l_foot = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.RightFoot); + + Transform l_toe = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.LeftToes); + if(l_toe == null) + l_toe = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.RightToes); + + if((l_foot != null) && (l_toe != null)) + { + Vector3 l_footPos = (PlayerSetup.Instance._avatar.transform.GetMatrix().inverse * l_foot.GetMatrix()) * ms_pointVector; + Vector3 l_toePos = (PlayerSetup.Instance._avatar.transform.GetMatrix().inverse * l_toe.GetMatrix()) * ms_pointVector; + m_massCenter = new Vector3(0f, 0f, l_toePos.z - l_footPos.z); + } + } + + m_vrIk.solver.locomotion.offset = (Settings.MassCenter ? m_massCenter : m_locomotionOffset); m_vrIk.onPreSolverUpdate.AddListener(this.OnIKPreUpdate); m_vrIk.onPostSolverUpdate.AddListener(this.OnIKPostUpdate); @@ -260,6 +280,8 @@ namespace ml_amt m_ikWeight = m_vrIk.solver.IKPositionWeight; m_locomotionWeight = m_vrIk.solver.locomotion.weight; m_plantFeet = m_vrIk.solver.plantFeet; + m_bendNormalLeft = m_vrIk.solver.leftLeg.useAnimatedBendNormal; + m_bendNormalRight = m_vrIk.solver.rightLeg.useAnimatedBendNormal; if(m_detectEmotes && m_emoteActive) m_vrIk.solver.IKPositionWeight = 0f; @@ -272,18 +294,22 @@ namespace ml_amt if(m_ikOverrideFly && MovementSystem.Instance.flying) { m_vrIk.solver.locomotion.weight = 0f; + m_vrIk.solver.leftLeg.useAnimatedBendNormal = true; + m_vrIk.solver.rightLeg.useAnimatedBendNormal = true; l_legsOverride = true; } if(m_ikOverrideJump && !m_grounded && !MovementSystem.Instance.flying) { m_vrIk.solver.locomotion.weight = 0f; + m_vrIk.solver.leftLeg.useAnimatedBendNormal = true; + m_vrIk.solver.rightLeg.useAnimatedBendNormal = true; l_legsOverride = true; } bool l_solverActive = !Mathf.Approximately(m_vrIk.solver.IKPositionWeight, 0f); - if(l_legsOverride && l_solverActive && m_followHips && (!m_moving || (m_poseState == PoseState.Proning)) && m_isInVR && !BodySystem.isCalibratedAsFullBody) + if(l_legsOverride && l_solverActive && m_followHips && (!m_moving || (m_poseState == PoseState.Proning)) && m_inVR && !BodySystem.isCalibratedAsFullBody) { m_vrIk.solver.plantFeet = false; ABI_RC.Systems.IK.IKSystem.VrikRootController.enabled = false; @@ -296,6 +322,8 @@ namespace ml_amt m_vrIk.solver.IKPositionWeight = m_ikWeight; m_vrIk.solver.locomotion.weight = m_locomotionWeight; m_vrIk.solver.plantFeet = m_plantFeet; + m_vrIk.solver.leftLeg.useAnimatedBendNormal = m_bendNormalLeft; + m_vrIk.solver.rightLeg.useAnimatedBendNormal = m_bendNormalRight; } public void SetIKOverrideCrouch(bool p_state) @@ -320,7 +348,7 @@ namespace ml_amt { m_poseTransitions = p_state; - if(!m_poseTransitions && m_avatarReady && m_isInVR) + if(!m_poseTransitions && m_avatarReady && m_inVR) { PlayerSetup.Instance.animatorManager.SetAnimatorParameterBool("Crouching", false); PlayerSetup.Instance.animatorManager.SetAnimatorParameterBool("Prone", false); @@ -330,7 +358,7 @@ namespace ml_amt { m_adjustedMovement = p_state; - if(!m_adjustedMovement && m_avatarReady && m_isInVR) + if(!m_adjustedMovement && m_avatarReady && m_inVR) { MovementSystem.Instance.ChangeCrouch(false); MovementSystem.Instance.ChangeProne(false); @@ -352,6 +380,11 @@ namespace ml_amt { m_followHips = p_state; } + public void SetMassCenter(bool p_state) + { + if(m_vrIk != null) + m_vrIk.solver.locomotion.offset = (Settings.MassCenter ? m_massCenter : m_locomotionOffset); + } public float GetUpright() => m_upright; public bool GetGroundedRaw() => m_groundedRaw; diff --git a/ml_amt/README.md b/ml_amt/README.md index 0489c8c..6e5482a 100644 --- a/ml_amt/README.md +++ b/ml_amt/README.md @@ -18,7 +18,7 @@ Available mod's settings in `Settings - IK - Avatar Motion Tweaker`: * Note: Can be overrided by avatar. For this avatar has to have child gameobject with name `ProneLimit`, its Y-axis location will be used as limit, should be in range [0.0, 1.0]. * **IK override while flying:** disables legs locomotion/autostep in fly mode; default value - `true`. * **IK override while jumping:** disables legs locomotion/autostep in jump; default value - `true`. -* **Follow hips on IK override:** adjust avatar position to overcome animation snapping on IK override; default value - `true`. +* **Follow hips on IK override:** adjusts avatar position to overcome animation snapping on IK override; default value - `true`. * Note: Works best with animations that have root transform position (XZ) based on center of mass. * Note: Made for four point tracking (head, hands, hips) in mind. * **Pose transitions:** allows regular avatars animator to transit in crouch/prone states; default value - `true`. @@ -26,6 +26,8 @@ Available mod's settings in `Settings - IK - Avatar Motion Tweaker`: * **Adjusted pose movement speed:** scales movement speed upon crouching/proning; default value - `true`. * **Detect animations emote tag:** disables avatar's IK entirely if current animator state has `Emote` tag; default value - `true`. * Note: Created as example for [propoused game feature](https://feedback.abinteractive.net/p/disabling-vr-ik-for-emotes-via-animator-state-tag-7b80d963-053a-41c0-86ac-e3d53c61c1e2). +* **Adjusted locomotion mass center:** automatically changes IK locomotion center if avatar has toe bones; default value - `true`. + * Note: Compatible with [DesktopVRIK](https://github.com/NotAKidOnSteam/DesktopVRIK) and [FuckToes](https://github.com/NotAKidOnSteam/FuckToes). * **Alternative avatar collider scale:** applies slightly different approach to avatar collider size change; default value - `true` Available additional parameters for AAS animator: @@ -38,8 +40,5 @@ Available additional parameters for AAS animator: * **`Moving`:** defines movement state of player * Note: Can be set as local-only (not synced) if starts with `#` character. -Additional avatars tweaks: -* If avatar has child object with name `LocomotionOffset` its local position will be used for offsetting VRIK locomotion mass center. - Additional mod's behaviour: * Overrides FBT behaviour in 4PT mode (head, hands, hips). Be sure to disable legs and knees tracking in `Settings - IK tab`. diff --git a/ml_amt/Settings.cs b/ml_amt/Settings.cs index ed548c0..12d0d83 100644 --- a/ml_amt/Settings.cs +++ b/ml_amt/Settings.cs @@ -19,7 +19,8 @@ namespace ml_amt IKOverrideJump, DetectEmotes, FollowHips, - CollisionScale + CollisionScale, + MassCenter }; static bool ms_ikOverrideCrouch = true; @@ -33,6 +34,7 @@ namespace ml_amt static bool ms_detectEmotes = true; static bool ms_followHips = true; static bool ms_collisionScale = true; + static bool ms_massCenter = true; static MelonLoader.MelonPreferences_Category ms_category = null; static List ms_entries = null; @@ -48,6 +50,7 @@ namespace ml_amt static public event Action DetectEmotesChange; static public event Action FollowHipsChange; static public event Action CollisionScaleChange; + static public event Action MassCenterChange; internal static void Init() { @@ -65,7 +68,8 @@ namespace ml_amt ms_category.CreateEntry(ModSetting.IKOverrideJump.ToString(), true), ms_category.CreateEntry(ModSetting.DetectEmotes.ToString(), true), ms_category.CreateEntry(ModSetting.FollowHips.ToString(), true), - ms_category.CreateEntry(ModSetting.CollisionScale.ToString(), true) + ms_category.CreateEntry(ModSetting.CollisionScale.ToString(), true), + ms_category.CreateEntry(ModSetting.MassCenter.ToString(), true) }; Load(); @@ -108,6 +112,7 @@ namespace ml_amt ms_detectEmotes = (bool)ms_entries[(int)ModSetting.DetectEmotes].BoxedValue; ms_followHips = (bool)ms_entries[(int)ModSetting.FollowHips].BoxedValue; ms_collisionScale = (bool)ms_entries[(int)ModSetting.CollisionScale].BoxedValue; + ms_massCenter = (bool)ms_entries[(int)ModSetting.MassCenter].BoxedValue; } static void OnSliderUpdate(string p_name, string p_value) @@ -203,6 +208,12 @@ namespace ml_amt CollisionScaleChange?.Invoke(ms_collisionScale); } break; + + case ModSetting.MassCenter: + { + ms_massCenter = bool.Parse(p_value); + MassCenterChange?.Invoke(ms_massCenter); + } break; } ms_entries[(int)l_setting].BoxedValue = bool.Parse(p_value); @@ -253,5 +264,9 @@ namespace ml_amt { get => ms_collisionScale; } + public static bool MassCenter + { + get => ms_massCenter; + } } } diff --git a/ml_amt/resources/menu.js b/ml_amt/resources/menu.js index 1b6ebaa..72c85c1 100644 --- a/ml_amt/resources/menu.js +++ b/ml_amt/resources/menu.js @@ -249,6 +249,13 @@ function inp_toggle_mod_amt(_obj, _callbackName) {
+ +
+
Adjusted locomotion mass center:
+
+
+
+
Alternative avatar collider scale:
diff --git a/ml_lme/LeapTracked.cs b/ml_lme/LeapTracked.cs index 443e51f..4725404 100644 --- a/ml_lme/LeapTracked.cs +++ b/ml_lme/LeapTracked.cs @@ -16,7 +16,7 @@ namespace ml_lme static readonly Quaternion ms_offsetRightDesktop = Quaternion.Euler(0f, 270f, 0f); VRIK m_vrIK = null; - Vector2 m_armsWeights = Vector2.zero; + Vector4 m_armsWeights = Vector2.zero; bool m_inVR = false; Transform m_hips = null; Transform m_origLeftHand = null; @@ -350,14 +350,19 @@ namespace ml_lme void OnIKPreUpdate() { - m_armsWeights.Set(m_vrIK.solver.leftArm.positionWeight, m_vrIK.solver.rightArm.positionWeight); + m_armsWeights.Set( + m_vrIK.solver.leftArm.positionWeight, + m_vrIK.solver.leftArm.rotationWeight, + m_vrIK.solver.rightArm.positionWeight, + m_vrIK.solver.rightArm.rotationWeight + ); - if(m_leftTargetActive && Mathf.Approximately(m_armsWeights.x, 0f)) + if(m_leftTargetActive && (Mathf.Approximately(m_armsWeights.x, 0f) || Mathf.Approximately(m_armsWeights.y, 0f))) { m_vrIK.solver.leftArm.positionWeight = 1f; m_vrIK.solver.leftArm.rotationWeight = 1f; } - if(m_rightTargetActive && Mathf.Approximately(m_armsWeights.y, 0f)) + if(m_rightTargetActive && (Mathf.Approximately(m_armsWeights.z, 0f) || Mathf.Approximately(m_armsWeights.w, 0f))) { m_vrIK.solver.rightArm.positionWeight = 1f; m_vrIK.solver.rightArm.rotationWeight = 1f; @@ -366,10 +371,9 @@ namespace ml_lme void OnIKPostUpdate() { m_vrIK.solver.leftArm.positionWeight = m_armsWeights.x; - m_vrIK.solver.leftArm.rotationWeight = m_armsWeights.x; - - m_vrIK.solver.rightArm.positionWeight = m_armsWeights.y; - m_vrIK.solver.rightArm.rotationWeight = m_armsWeights.y; + m_vrIK.solver.leftArm.rotationWeight = m_armsWeights.y; + m_vrIK.solver.rightArm.positionWeight = m_armsWeights.z; + m_vrIK.solver.rightArm.rotationWeight = m_armsWeights.w; } void RestoreVRIK() diff --git a/ml_lme/Properties/AssemblyInfo.cs b/ml_lme/Properties/AssemblyInfo.cs index 095d7d8..c404034 100644 --- a/ml_lme/Properties/AssemblyInfo.cs +++ b/ml_lme/Properties/AssemblyInfo.cs @@ -1,10 +1,10 @@ using System.Reflection; [assembly: AssemblyTitle("LeapMotionExtension")] -[assembly: AssemblyVersion("1.3.0")] -[assembly: AssemblyFileVersion("1.3.0")] +[assembly: AssemblyVersion("1.3.1")] +[assembly: AssemblyFileVersion("1.3.1")] -[assembly: MelonLoader.MelonInfo(typeof(ml_lme.LeapMotionExtension), "LeapMotionExtension", "1.3.0", "SDraw", "https://github.com/SDraw/ml_mods_cvr")] +[assembly: MelonLoader.MelonInfo(typeof(ml_lme.LeapMotionExtension), "LeapMotionExtension", "1.3.1", "SDraw", "https://github.com/SDraw/ml_mods_cvr")] [assembly: MelonLoader.MelonGame(null, "ChilloutVR")] [assembly: MelonLoader.MelonPlatform(MelonLoader.MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] [assembly: MelonLoader.MelonPlatformDomain(MelonLoader.MelonPlatformDomainAttribute.CompatibleDomains.MONO)] diff --git a/ml_lme/README.md b/ml_lme/README.md index 00f797c..663a96e 100644 --- a/ml_lme/README.md +++ b/ml_lme/README.md @@ -12,15 +12,15 @@ This mod allows you to use your Leap Motion controller for hands and fingers tra # Usage ## Settings Available mod's settings in `Settings - Implementation - Leap Motion Tracking`: -* **Enable tracking:** enable hands tracking from Leap Motion data, disabled by default. -* **Tracking mode:** set Leap Motion tracking mode, available values: `Screentop`, `Desktop` (by default), `HMD`. +* **Enable tracking:** enables/disables hands tracking from Leap Motion data, disabled by default. +* **Tracking mode:** sets Leap Motion tracking mode, available values: `Screentop`, `Desktop` (by default), `HMD`. * **Desktop offset X/Y/Z:** offset position for body attachment, (0, -45, 30) by default. -* **Attach to head:** attach hands transformation to head instead of body, disabled by default. +* **Attach to head:** attaches hands transformation to head instead of body, disabled by default. * **Head offset X/Y/Z:** offset position for head attachment (`Attach to head` is **`true`**), (0, -30, 15) by default. * **Offset angle X/Y/X:** rotation around specific axis, useful for neck mounts, 0 by default. * **Track elbows:** elbows tracking, works best in `Screentop` and `HMD` tracking modes, `true` by default. -* **Fingers tracking only:** apply only fingers tracking, disabled by default. -* **Model visibility:** show Leap Motion controller model, useful for tracking visualizing, disabled by default. +* **Fingers tracking only:** applies only fingers tracking, disabled by default. +* **Model visibility:** shows Leap Motion controller model, useful for tracking visualizing, disabled by default. * **Interaction input:** enables in-game interactions (props, menu and etc.); `true` by default. * **Interact gesture threadhold:** activation limit for interaction based on hand gesture; 80 by default. * **Grip gesture threadhold:** activation limit for grip based on hand gesture; 40 by default. diff --git a/ml_pam/ArmMover.cs b/ml_pam/ArmMover.cs index d2fd112..01db46d 100644 --- a/ml_pam/ArmMover.cs +++ b/ml_pam/ArmMover.cs @@ -1,5 +1,8 @@ using ABI.CCK.Components; +using ABI_RC.Core.InteractionSystem; using ABI_RC.Core.Player; +using RootMotion.FinalIK; +using System.Reflection; using UnityEngine; namespace ml_pam @@ -7,57 +10,275 @@ namespace ml_pam [DisallowMultipleComponent] class ArmMover : MonoBehaviour { + const float c_offsetLimit = 0.5f; + + static readonly float[] ms_tposeMuscles = typeof(ABI_RC.Systems.IK.SubSystems.BodySystem).GetField("TPoseMuscles", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null) as float[]; static readonly Vector4 ms_pointVector = new Vector4(0f, 0f, 0f, 1f); - static readonly Quaternion ms_rotationOffset = Quaternion.Euler(0f, 0f, -90f); + static readonly Quaternion ms_offsetRight = Quaternion.Euler(0f, 0f, 90f); + static readonly Quaternion ms_offsetRightDesktop = Quaternion.Euler(0f, 270f, 0f); + static readonly Quaternion ms_palmToLeft = Quaternion.Euler(0f, 0f, -90f); - Animator m_animator = null; + bool m_inVR = false; + VRIK m_vrIK = null; + Vector2 m_armWeight = Vector2.zero; + Transform m_origRightHand = null; + float m_playspaceScale = 1f; - int m_mainLayer = -1; - CVRPickupObject m_target = null; + bool m_enabled = true; + ArmIK m_armIK = null; + Transform m_target = null; + Transform m_rotationTarget = null; + CVRPickupObject m_pickup = null; Matrix4x4 m_offset = Matrix4x4.identity; + bool m_targetActive = false; void Start() { - m_animator = PlayerSetup.Instance._animator; - m_mainLayer = m_animator.GetLayerIndex("Locomotion/Emotes"); + m_inVR = Utils.IsInVR(); + + m_target = new GameObject("ArmPickupTarget").transform; + m_target.parent = PlayerSetup.Instance.GetActiveCamera().transform; + m_target.localPosition = Vector3.zero; + m_target.localRotation = Quaternion.identity; + + m_rotationTarget = new GameObject("RotationTarget").transform; + m_rotationTarget.parent = m_target; + m_rotationTarget.localPosition = new Vector3(c_offsetLimit * Settings.GrabOffset, 0f, 0f); + m_rotationTarget.localRotation = Quaternion.identity; + + m_enabled = Settings.Enabled; + + Settings.EnabledChange += this.SetEnabled; + Settings.GrabOffsetChange += this.SetGrabOffset; } - void OnAnimatorIK(int p_layerIndex) + void OnDestroy() { - if((p_layerIndex == m_mainLayer) && (m_target != null)) // Only main Locomotion/Emotes layer + Settings.EnabledChange -= this.SetEnabled; + Settings.GrabOffsetChange -= this.SetGrabOffset; + } + + void Update() + { + if(m_enabled && (m_pickup != null)) { - Transform l_camera = PlayerSetup.Instance.GetActiveCamera().transform; + Matrix4x4 l_result = m_pickup.transform.GetMatrix() * m_offset; + m_target.position = l_result * ms_pointVector; + } + } - m_animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1f); - m_animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f); + void OnIKPreUpdate() + { + m_armWeight.Set(m_vrIK.solver.rightArm.positionWeight, m_vrIK.solver.rightArm.rotationWeight); - switch(m_target.gripType) + if(m_targetActive && (Mathf.Approximately(m_armWeight.x, 0f) || Mathf.Approximately(m_armWeight.y, 0f))) + { + m_vrIK.solver.rightArm.positionWeight = 1f; + m_vrIK.solver.rightArm.rotationWeight = 1f; + } + } + void OnIKPostUpdate() + { + m_vrIK.solver.rightArm.positionWeight = m_armWeight.x; + m_vrIK.solver.rightArm.rotationWeight = m_armWeight.y; + } + + // Settings + void SetEnabled(bool p_state) + { + m_enabled = p_state; + + RefreshArmIK(); + if(m_enabled) + RestorePickup(); + else + RestoreVRIK(); + } + + void SetGrabOffset(float p_value) + { + if(m_rotationTarget != null) + m_rotationTarget.localPosition = new Vector3(c_offsetLimit * m_playspaceScale * p_value, 0f, 0f); + } + + // Game events + internal void OnAvatarClear() + { + m_vrIK = null; + m_origRightHand = null; + m_armIK = null; + m_targetActive = false; + } + + internal void OnAvatarSetup() + { + // Recheck if user could switch to VR + if(m_inVR != Utils.IsInVR()) + { + m_target.parent = PlayerSetup.Instance.GetActiveCamera().transform; + m_target.localPosition = Vector3.zero; + m_target.localRotation = Quaternion.identity; + } + + m_inVR = Utils.IsInVR(); + m_vrIK = PlayerSetup.Instance._animator.GetComponent(); + + if(PlayerSetup.Instance._animator.isHuman) + { + HumanPose l_currentPose = new HumanPose(); + HumanPoseHandler l_poseHandler = null; + + if(!m_inVR) { - case CVRPickupObject.GripType.Origin: - { - if(m_target.gripOrigin != null) - { - m_animator.SetIKPosition(AvatarIKGoal.RightHand, m_target.gripOrigin.position); - m_animator.SetIKRotation(AvatarIKGoal.RightHand, l_camera.rotation * ms_rotationOffset); - } - } - break; + l_poseHandler = new HumanPoseHandler(PlayerSetup.Instance._animator.avatar, PlayerSetup.Instance._avatar.transform); + l_poseHandler.GetHumanPose(ref l_currentPose); - case CVRPickupObject.GripType.Free: + HumanPose l_tPose = new HumanPose { - Matrix4x4 l_result = m_target.transform.GetMatrix() * m_offset; - m_animator.SetIKPosition(AvatarIKGoal.RightHand, l_result * ms_pointVector); - m_animator.SetIKRotation(AvatarIKGoal.RightHand, l_camera.rotation * ms_rotationOffset); + bodyPosition = l_currentPose.bodyPosition, + bodyRotation = l_currentPose.bodyRotation, + muscles = new float[l_currentPose.muscles.Length] + }; + for(int i = 0; i < l_tPose.muscles.Length; i++) + l_tPose.muscles[i] = ms_tposeMuscles[i]; + + l_poseHandler.SetHumanPose(ref l_tPose); + } + + Transform l_hand = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.RightHand); + if(l_hand != null) + m_rotationTarget.localRotation = (ms_palmToLeft * (m_inVR ? ms_offsetRight : ms_offsetRightDesktop)) * (PlayerSetup.Instance._avatar.transform.GetMatrix().inverse * l_hand.GetMatrix()).rotation; + + if(m_vrIK == null) + { + Transform l_chest = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.UpperChest); + if(l_chest == null) + l_chest = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.Chest); + if(l_chest == null) + l_chest = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.Spine); + + m_armIK = PlayerSetup.Instance._avatar.AddComponent(); + m_armIK.solver.isLeft = false; + m_armIK.solver.SetChain( + l_chest, + PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.RightShoulder), + PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.RightUpperArm), + PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.RightLowerArm), + l_hand, + PlayerSetup.Instance._animator.transform + ); + m_armIK.solver.arm.target = m_rotationTarget; + m_armIK.solver.arm.positionWeight = 1f; + m_armIK.solver.arm.rotationWeight = 1f; + m_armIK.solver.IKPositionWeight = 0f; + m_armIK.solver.IKRotationWeight = 0f; + m_armIK.enabled = m_enabled; + } + else + { + m_origRightHand = m_vrIK.solver.rightArm.target; + m_vrIK.solver.OnPreUpdate += this.OnIKPreUpdate; + m_vrIK.solver.OnPostUpdate += this.OnIKPostUpdate; + } + + l_poseHandler?.SetHumanPose(ref l_currentPose); + l_poseHandler?.Dispose(); + } + + if(m_enabled) + RestorePickup(); + } + + internal void OnPickupGrab(CVRPickupObject p_pickup, ControllerRay p_ray, Vector3 p_hit) + { + if(p_ray == ViewManager.Instance.desktopControllerRay) + { + m_pickup = p_pickup; + + // Set offsets + if(m_pickup.gripType == CVRPickupObject.GripType.Origin) + { + if(m_pickup.ikReference != null) + m_offset = (m_pickup.transform.GetMatrix().inverse * m_pickup.ikReference.GetMatrix()); + else + { + if(m_pickup.gripOrigin != null) + m_offset = m_pickup.transform.GetMatrix().inverse * m_pickup.gripOrigin.GetMatrix(); + } + } + else + m_offset = m_pickup.transform.GetMatrix().inverse * Matrix4x4.Translate(p_hit); + + if(m_enabled) + { + if((m_vrIK != null) && !m_targetActive) + { + m_vrIK.solver.rightArm.target = m_rotationTarget; + m_targetActive = true; + } + + if(m_armIK != null) + { + m_armIK.solver.IKPositionWeight = 1f; + m_armIK.solver.IKRotationWeight = 1f; } - break; } } } - public void SetTarget(CVRPickupObject p_target, Vector3 p_hit) + internal void OnPickupDrop(CVRPickupObject p_pickup) { - m_target = p_target; - m_offset = (m_target != null) ? (p_target.transform.GetMatrix().inverse * Matrix4x4.Translate(p_hit)): Matrix4x4.identity; + if(m_pickup == p_pickup) + { + m_pickup = null; + + if(m_enabled) + { + RestoreVRIK(); + + if(m_armIK != null) + { + m_armIK.solver.IKPositionWeight = 0f; + m_armIK.solver.IKRotationWeight = 0f; + } + } + } + } + + internal void OnPlayspaceScale(float p_relation) + { + m_playspaceScale = p_relation; + SetGrabOffset(Settings.GrabOffset); + } + + // Arbitrary + void RestorePickup() + { + if((m_vrIK != null) && (m_pickup != null)) + { + m_vrIK.solver.rightArm.target = m_rotationTarget; + m_targetActive = true; + } + if((m_armIK != null) && (m_pickup != null)) + { + m_armIK.solver.IKPositionWeight = 1f; + m_armIK.solver.IKRotationWeight = 1f; + } + } + + void RestoreVRIK() + { + if((m_vrIK != null) && m_targetActive) + { + m_vrIK.solver.rightArm.target = m_origRightHand; + m_targetActive = false; + } + } + + void RefreshArmIK() + { + if(m_armIK != null) + m_armIK.enabled = m_enabled; } } } diff --git a/ml_pam/Main.cs b/ml_pam/Main.cs index 3f808c9..728e91b 100644 --- a/ml_pam/Main.cs +++ b/ml_pam/Main.cs @@ -1,4 +1,5 @@ using ABI.CCK.Components; +using ABI_RC.Core.InteractionSystem; using ABI_RC.Core.Player; using System; using System.Reflection; @@ -10,13 +11,15 @@ namespace ml_pam { static PickupArmMovement ms_instance = null; - ArmMover m_localPuller = null; + ArmMover m_localMover = null; public override void OnInitializeMelon() { if(ms_instance == null) ms_instance = this; + Settings.Init(); + HarmonyInstance.Patch( typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.ClearAvatar)), null, @@ -37,6 +40,21 @@ namespace ml_pam null, new HarmonyLib.HarmonyMethod(typeof(PickupArmMovement).GetMethod(nameof(OnCVRPickupObjectDrop_Postfix), BindingFlags.Static | BindingFlags.NonPublic)) ); + HarmonyInstance.Patch( + typeof(PlayerSetup).GetMethod("SetPlaySpaceScale", BindingFlags.NonPublic | BindingFlags.Instance), + null, + new HarmonyLib.HarmonyMethod(typeof(PickupArmMovement).GetMethod(nameof(OnPlayspaceScale_Postfix), BindingFlags.Static | BindingFlags.NonPublic)) + ); + + MelonLoader.MelonCoroutines.Start(WaitForLocalPlayer()); + } + + System.Collections.IEnumerator WaitForLocalPlayer() + { + while(PlayerSetup.Instance == null) + yield return null; + + m_localMover = PlayerSetup.Instance.gameObject.AddComponent(); } public override void OnDeinitializeMelon() @@ -50,7 +68,8 @@ namespace ml_pam { try { - m_localPuller = null; + if(m_localMover != null) + m_localMover.OnAvatarClear(); } catch(Exception e) { @@ -63,8 +82,8 @@ namespace ml_pam { try { - if(!Utils.IsInVR()) - m_localPuller = PlayerSetup.Instance._avatar.AddComponent(); + if(m_localMover != null) + m_localMover.OnAvatarSetup(); } catch(Exception e) { @@ -72,15 +91,13 @@ namespace ml_pam } } - static void OnCVRPickupObjectGrab_Postfix(ref CVRPickupObject __instance, Vector3 __2) => ms_instance?.OnCVRPickupObjectGrab(__instance, __2); - void OnCVRPickupObjectGrab(CVRPickupObject p_pickup, Vector3 p_hit) + static void OnCVRPickupObjectGrab_Postfix(ref CVRPickupObject __instance, ControllerRay __1, Vector3 __2) => ms_instance?.OnCVRPickupObjectGrab(__instance, __1, __2); + void OnCVRPickupObjectGrab(CVRPickupObject p_pickup, ControllerRay p_ray, Vector3 p_hit) { try { - if(p_pickup.IsGrabbedByMe() && (m_localPuller != null)) - { - m_localPuller.SetTarget(p_pickup, p_hit); - } + if(p_pickup.IsGrabbedByMe() && (m_localMover != null)) + m_localMover.OnPickupGrab(p_pickup, p_ray, p_hit); } catch(Exception e) { @@ -88,15 +105,27 @@ namespace ml_pam } } - static void OnCVRPickupObjectDrop_Postfix() => ms_instance?.OnCVRPickupObjectDrop(); - void OnCVRPickupObjectDrop() + static void OnCVRPickupObjectDrop_Postfix(ref CVRPickupObject __instance) => ms_instance?.OnCVRPickupObjectDrop(__instance); + void OnCVRPickupObjectDrop(CVRPickupObject p_pickup) { try { - if(m_localPuller != null) - { - m_localPuller.SetTarget(null, Vector3.zero); - } + if(m_localMover != null) + m_localMover.OnPickupDrop(p_pickup); + } + catch(Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + } + + static void OnPlayspaceScale_Postfix(float ____avatarScaleRelation) => ms_instance?.OnPlayspaceScale(____avatarScaleRelation); + void OnPlayspaceScale(float p_relation) + { + try + { + if(m_localMover != null) + m_localMover.OnPlayspaceScale(p_relation); } catch(Exception e) { diff --git a/ml_pam/Properties/AssemblyInfo.cs b/ml_pam/Properties/AssemblyInfo.cs index a0e0b87..9a229fb 100644 --- a/ml_pam/Properties/AssemblyInfo.cs +++ b/ml_pam/Properties/AssemblyInfo.cs @@ -1,10 +1,11 @@ using System.Reflection; [assembly: AssemblyTitle("PickupArmMovement")] -[assembly: AssemblyVersion("1.0.0")] -[assembly: AssemblyFileVersion("1.0.0")] +[assembly: AssemblyVersion("1.0.1")] +[assembly: AssemblyFileVersion("1.0.1")] -[assembly: MelonLoader.MelonInfo(typeof(ml_pam.PickupArmMovement), "PickupArmMovement", "1.0.0", "SDraw", "https://github.com/SDraw/ml_mods_cvr")] +[assembly: MelonLoader.MelonInfo(typeof(ml_pam.PickupArmMovement), "PickupArmMovement", "1.0.1", "SDraw", "https://github.com/SDraw/ml_mods_cvr")] [assembly: MelonLoader.MelonGame(null, "ChilloutVR")] +[assembly: MelonLoader.MelonPriority(1)] [assembly: MelonLoader.MelonPlatform(MelonLoader.MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] [assembly: MelonLoader.MelonPlatformDomain(MelonLoader.MelonPlatformDomainAttribute.CompatibleDomains.MONO)] \ No newline at end of file diff --git a/ml_pam/README.md b/ml_pam/README.md index dceb34e..e2fe1c9 100644 --- a/ml_pam/README.md +++ b/ml_pam/README.md @@ -1,7 +1,12 @@ # Pickup Arm Movement -This mod adds arm tracking upon holding pickup. +This mod adds arm tracking upon holding pickup in desktop mode. # Installation * Install [latest MelonLoader](https://github.com/LavaGang/MelonLoader) * Get [latest release DLL](../../../releases/latest): * Put `ml_pam.dll` in `Mods` folder of game + +# Usage +Available mod's settings in `Settings - Interactions`: +* **Enable hand movement:** enables/disables arm tracking; default value - `true`. +* **Grab offset:** offset from pickup grab point; defalut value - `25`. diff --git a/ml_pam/Scripts.cs b/ml_pam/Scripts.cs new file mode 100644 index 0000000..5509d9e --- /dev/null +++ b/ml_pam/Scripts.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using System.Reflection; + +namespace ml_pam +{ + static class Scripts + { + public static string GetEmbeddedScript(string p_name) + { + string l_result = ""; + Assembly l_assembly = Assembly.GetExecutingAssembly(); + string l_assemblyName = l_assembly.GetName().Name; + + try + { + Stream l_libraryStream = l_assembly.GetManifestResourceStream(l_assemblyName + ".resources." + p_name); + StreamReader l_streadReader = new StreamReader(l_libraryStream); + l_result = l_streadReader.ReadToEnd(); + } + catch(Exception) { } + + return l_result; + } + } +} diff --git a/ml_pam/Settings.cs b/ml_pam/Settings.cs new file mode 100644 index 0000000..c3fc659 --- /dev/null +++ b/ml_pam/Settings.cs @@ -0,0 +1,113 @@ +using ABI_RC.Core.InteractionSystem; +using cohtml; +using System; +using System.Collections.Generic; + +namespace ml_pam +{ + static class Settings + { + public enum ModSetting + { + Enabled = 0, + GrabOffset + } + + static bool ms_enabled = true; + static float ms_grabOffset = 0.25f; + + static MelonLoader.MelonPreferences_Category ms_category = null; + static List ms_entries = null; + + static public event Action EnabledChange; + static public event Action GrabOffsetChange; + + internal static void Init() + { + ms_category = MelonLoader.MelonPreferences.CreateCategory("PAM"); + + ms_entries = new List() + { + ms_category.CreateEntry(ModSetting.Enabled.ToString(), ms_enabled), + ms_category.CreateEntry(ModSetting.GrabOffset.ToString(), 25), + }; + + Load(); + + MelonLoader.MelonCoroutines.Start(WaitMainMenuUi()); + } + + static System.Collections.IEnumerator WaitMainMenuUi() + { + while(ViewManager.Instance == null) + yield return null; + while(ViewManager.Instance.gameMenuView == null) + yield return null; + while(ViewManager.Instance.gameMenuView.Listener == null) + yield return null; + + ViewManager.Instance.gameMenuView.Listener.ReadyForBindings += () => + { + ViewManager.Instance.gameMenuView.View.BindCall("MelonMod_PAM_Call_InpToggle", new Action(OnToggleUpdate)); + ViewManager.Instance.gameMenuView.View.BindCall("MelonMod_PAM_Call_InpSlider", new Action(OnSliderUpdate)); + }; + ViewManager.Instance.gameMenuView.Listener.FinishLoad += (_) => + { + ViewManager.Instance.gameMenuView.View.ExecuteScript(Scripts.GetEmbeddedScript("menu.js")); + foreach(var l_entry in ms_entries) + ViewManager.Instance.gameMenuView.View.TriggerEvent("updateModSettingPAM", l_entry.DisplayName, l_entry.GetValueAsString()); + }; + } + + static void Load() + { + ms_enabled = (bool)ms_entries[(int)ModSetting.Enabled].BoxedValue; + ms_grabOffset = (int)ms_entries[(int)ModSetting.GrabOffset].BoxedValue * 0.01f; + } + + static void OnToggleUpdate(string p_name, string p_value) + { + if(Enum.TryParse(p_name, out ModSetting l_setting)) + { + switch(l_setting) + { + case ModSetting.Enabled: + { + ms_enabled = bool.Parse(p_value); + EnabledChange?.Invoke(ms_enabled); + } + break; + } + + ms_entries[(int)l_setting].BoxedValue = bool.Parse(p_value); + } + } + + static void OnSliderUpdate(string p_name, string p_value) + { + if(Enum.TryParse(p_name, out ModSetting l_setting)) + { + switch(l_setting) + { + case ModSetting.GrabOffset: + { + ms_grabOffset = int.Parse(p_value) * 0.01f; + GrabOffsetChange?.Invoke(ms_grabOffset); + } + break; + } + + ms_entries[(int)l_setting].BoxedValue = int.Parse(p_value); + } + } + + public static bool Enabled + { + get => ms_enabled; + } + public static float GrabOffset + { + get => ms_grabOffset; + } + } +} diff --git a/ml_pam/ml_pam.csproj b/ml_pam/ml_pam.csproj index 23e1b5b..7250d1e 100644 --- a/ml_pam/ml_pam.csproj +++ b/ml_pam/ml_pam.csproj @@ -42,6 +42,16 @@ D:\Games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp.dll False + + False + False + + + False + + + False + False D:\Games\Steam\steamapps\common\ChilloutVR\MelonLoader\MelonLoader.dll @@ -66,8 +76,13 @@ + + + + + copy /y "$(TargetPath)" "D:\Games\Steam\steamapps\common\ChilloutVR\Mods\" diff --git a/ml_pam/resources/menu.js b/ml_pam/resources/menu.js new file mode 100644 index 0000000..c4b41b2 --- /dev/null +++ b/ml_pam/resources/menu.js @@ -0,0 +1,209 @@ +// Add settings +var g_modSettingsPAM = []; + +engine.on('updateModSettingPAM', function (_name, _value) { + for (var i = 0; i < g_modSettingsPAM.length; i++) { + if (g_modSettingsPAM[i].name == _name) { + g_modSettingsPAM[i].updateValue(_value); + break; + } + } +}); + +// Modified from original `inp` types, because I have no js knowledge to hook stuff +function inp_toggle_mod_pam(_obj, _callbackName) { + this.obj = _obj; + this.callbackName = _callbackName; + this.value = _obj.getAttribute('data-current'); + this.name = _obj.id; + this.type = _obj.getAttribute('data-type'); + + var self = this; + + this.mouseDown = function (_e) { + self.value = self.value == "True" ? "False" : "True"; + self.updateState(); + } + + this.updateState = function () { + self.obj.classList.remove("checked"); + if (self.value == "True") { + self.obj.classList.add("checked"); + } + + engine.call(self.callbackName, self.name, self.value); + } + + _obj.addEventListener('mousedown', this.mouseDown); + + this.getValue = function () { + return self.value; + } + + this.updateValue = function (value) { + self.value = value; + + self.obj.classList.remove("checked"); + if (self.value == "True") { + self.obj.classList.add("checked"); + } + } + + this.updateValue(this.value); + + return { + name: this.name, + value: this.getValue, + updateValue: this.updateValue + } +} + +function inp_slider_mod_pam(_obj, _callbackName) { + this.obj = _obj; + this.callbackName = _callbackName; + this.minValue = parseFloat(_obj.getAttribute('data-min')); + this.maxValue = parseFloat(_obj.getAttribute('data-max')); + this.percent = 0; + this.value = parseFloat(_obj.getAttribute('data-current')); + this.dragActive = false; + this.name = _obj.id; + this.type = _obj.getAttribute('data-type'); + this.stepSize = _obj.getAttribute('data-stepSize') || 0; + this.format = _obj.getAttribute('data-format') || '{value}'; + + var self = this; + + if (this.stepSize != 0) + this.value = Math.round(this.value / this.stepSize) * this.stepSize; + else + this.value = Math.round(this.value); + + this.valueLabelBackground = document.createElement('div'); + this.valueLabelBackground.className = 'valueLabel background'; + this.valueLabelBackground.innerHTML = this.format.replace('{value}', this.value); + this.obj.appendChild(this.valueLabelBackground); + + this.valueBar = document.createElement('div'); + this.valueBar.className = 'valueBar'; + this.valueBar.setAttribute('style', 'width: ' + (((this.value - this.minValue) / (this.maxValue - this.minValue)) * 100) + '%;'); + this.obj.appendChild(this.valueBar); + + this.valueLabelForeground = document.createElement('div'); + this.valueLabelForeground.className = 'valueLabel foreground'; + this.valueLabelForeground.innerHTML = this.format.replace('{value}', this.value); + this.valueLabelForeground.setAttribute('style', 'width: ' + (1.0 / ((this.value - this.minValue) / (this.maxValue - this.minValue)) * 100) + '%;'); + this.valueBar.appendChild(this.valueLabelForeground); + + this.mouseDown = function (_e) { + self.dragActive = true; + self.mouseMove(_e, false); + } + + this.mouseMove = function (_e, _write) { + if (self.dragActive) { + var rect = _obj.getBoundingClientRect(); + var start = rect.left; + var end = rect.right; + self.percent = Math.min(Math.max((_e.clientX - start) / rect.width, 0), 1); + var value = self.percent; + value *= (self.maxValue - self.minValue); + value += self.minValue; + if (self.stepSize != 0) { + value = Math.round(value / self.stepSize); + self.value = value * self.stepSize; + self.percent = (self.value - self.minValue) / (self.maxValue - self.minValue); + } + else + self.value = Math.round(value); + + self.valueBar.setAttribute('style', 'width: ' + (self.percent * 100) + '%;'); + self.valueLabelForeground.setAttribute('style', 'width: ' + (1.0 / self.percent * 100) + '%;'); + self.valueLabelBackground.innerHTML = self.valueLabelForeground.innerHTML = self.format.replace('{value}', self.value); + + engine.call(self.callbackName, self.name, "" + self.value); + self.displayImperial(); + } + } + + this.mouseUp = function (_e) { + self.mouseMove(_e, true); + self.dragActive = false; + } + + _obj.addEventListener('mousedown', this.mouseDown); + document.addEventListener('mousemove', this.mouseMove); + document.addEventListener('mouseup', this.mouseUp); + + this.getValue = function () { + return self.value; + } + + this.updateValue = function (value) { + if (self.stepSize != 0) + self.value = Math.round(value * self.stepSize) / self.stepSize; + else + self.value = Math.round(value); + self.percent = (self.value - self.minValue) / (self.maxValue - self.minValue); + self.valueBar.setAttribute('style', 'width: ' + (self.percent * 100) + '%;'); + self.valueLabelForeground.setAttribute('style', 'width: ' + (1.0 / self.percent * 100) + '%;'); + self.valueLabelBackground.innerHTML = self.valueLabelForeground.innerHTML = self.format.replace('{value}', self.value); + self.displayImperial(); + } + + this.displayImperial = function () { + var displays = document.querySelectorAll('.imperialDisplay'); + for (var i = 0; i < displays.length; i++) { + var binding = displays[i].getAttribute('data-binding'); + if (binding == self.name) { + var realFeet = ((self.value * 0.393700) / 12); + var feet = Math.floor(realFeet); + var inches = Math.floor((realFeet - feet) * 12); + displays[i].innerHTML = feet + "'" + inches + ''''; + } + } + } + + return { + name: this.name, + value: this.getValue, + updateValue: this.updateValue + } +} + +// Add own menu +{ + let l_block = document.createElement('div'); + l_block.innerHTML = ` +
+
Pickup Arm Mover
+
+
+ +
+
Enable hand movement:
+
+
+
+
+ +
+
Grab offset:
+
+
+
+
+ `; + document.getElementById('settings-interaction').appendChild(l_block); + + // Update toggles in new menu block + let l_toggles = l_block.querySelectorAll('.inp_toggle'); + for (var i = 0; i < l_toggles.length; i++) { + g_modSettingsPAM[g_modSettingsPAM.length] = new inp_toggle_mod_pam(l_toggles[i], 'MelonMod_PAM_Call_InpToggle'); + } + + // Update sliders in new menu block + let l_sliders = l_block.querySelectorAll('.inp_slider'); + for (var i = 0; i < l_sliders.length; i++) { + g_modSettingsPAM[g_modSettingsPAM.length] = new inp_slider_mod_pam(l_sliders[i], 'MelonMod_PAM_Call_InpSlider'); + } +}