Automatic locomotion mass center

Animated bend normal while jumping/flying
Arm weights fix
Mod remake
This commit is contained in:
SDraw 2023-02-11 13:49:50 +03:00
parent f2b63303eb
commit cb26ab1e6c
No known key found for this signature in database
GPG key ID: BB95B4DAB2BB8BB5
16 changed files with 773 additions and 96 deletions

View file

@ -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 | | 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 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 Head Tracking | ml_dht | 1.1.1 | Yes | Working |
| Desktop Reticle Switch | ml_drs | 1.0.0 | 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 | 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 | | Leap Motion Extension | ml_lme | 1.3.1 | Yes, update review | Working |
| Pickup Arm Movement | ml_pam | 1.0.0 | Retired | Retired | No desire for development | 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` | Server Connection Info | ml_sci | 1.0.2 | Retired | Retired | Superseded by `Extended Game Notifications`

View file

@ -11,8 +11,10 @@ namespace ml_amt
[DisallowMultipleComponent] [DisallowMultipleComponent]
class MotionTweaker : MonoBehaviour 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_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_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"); static readonly int ms_emoteHash = Animator.StringToHash("Emote");
enum PoseState enum PoseState
@ -22,17 +24,18 @@ namespace ml_amt
Proning Proning
} }
static readonly Vector4 ms_pointVector = new Vector4(0f, 0f, 0f, 1f);
VRIK m_vrIk = null; VRIK m_vrIk = null;
int m_locomotionLayer = 0; int m_locomotionLayer = 0;
float m_ikWeight = 1f; // Original weight float m_ikWeight = 1f; // Original weight
float m_locomotionWeight = 1f; // Original weight float m_locomotionWeight = 1f; // Original weight
bool m_plantFeet = false; // Original plant feet bool m_plantFeet = false; // Original plant feet
float m_avatarScale = 1f; // Instantiated scale 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; Transform m_avatarHips = null;
float m_viewPointHeight = 1f; float m_viewPointHeight = 1f;
bool m_isInVR = false; bool m_inVR = false;
bool m_avatarReady = false; bool m_avatarReady = false;
bool m_compatibleAvatar = false; bool m_compatibleAvatar = false;
@ -55,15 +58,14 @@ namespace ml_amt
bool m_ikOverrideFly = true; bool m_ikOverrideFly = true;
bool m_ikOverrideJump = true; bool m_ikOverrideJump = true;
bool m_customLocomotionOffset = false;
Vector3 m_locomotionOffset = Vector3.zero;
bool m_detectEmotes = true; bool m_detectEmotes = true;
bool m_emoteActive = false; bool m_emoteActive = false;
bool m_followHips = true; bool m_followHips = true;
Vector3 m_hipsToPlayer = Vector3.zero; Vector3 m_hipsToPlayer = Vector3.zero;
Vector3 m_massCenter = Vector3.zero;
readonly List<AvatarParameter> m_parameters = null; readonly List<AvatarParameter> m_parameters = null;
internal MotionTweaker() internal MotionTweaker()
@ -73,7 +75,7 @@ namespace ml_amt
void Start() void Start()
{ {
m_isInVR = Utils.IsInVR(); m_inVR = Utils.IsInVR();
Settings.IKOverrideCrouchChange += this.SetIKOverrideCrouch; Settings.IKOverrideCrouchChange += this.SetIKOverrideCrouch;
Settings.CrouchLimitChange += this.SetCrouchLimit; Settings.CrouchLimitChange += this.SetCrouchLimit;
@ -85,6 +87,7 @@ namespace ml_amt
Settings.IKOverrideJumpChange += this.SetIKOverrideJump; Settings.IKOverrideJumpChange += this.SetIKOverrideJump;
Settings.DetectEmotesChange += this.SetDetectEmotes; Settings.DetectEmotesChange += this.SetDetectEmotes;
Settings.FollowHipsChange += this.SetFollowHips; Settings.FollowHipsChange += this.SetFollowHips;
Settings.MassCenterChange += this.SetMassCenter;
} }
void OnDestroy() void OnDestroy()
@ -99,6 +102,7 @@ namespace ml_amt
Settings.IKOverrideJumpChange -= this.SetIKOverrideJump; Settings.IKOverrideJumpChange -= this.SetIKOverrideJump;
Settings.DetectEmotesChange -= this.SetDetectEmotes; Settings.DetectEmotesChange -= this.SetDetectEmotes;
Settings.FollowHipsChange -= this.SetFollowHips; Settings.FollowHipsChange -= this.SetFollowHips;
Settings.MassCenterChange -= this.SetMassCenter;
} }
void Update() void Update()
@ -110,7 +114,7 @@ namespace ml_amt
m_moving = !Mathf.Approximately(MovementSystem.Instance.movementVector.magnitude, 0f); m_moving = !Mathf.Approximately(MovementSystem.Instance.movementVector.magnitude, 0f);
// Update upright // 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_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_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); 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); 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) if(m_adjustedMovement)
{ {
@ -170,20 +174,20 @@ namespace ml_amt
m_poseState = PoseState.Standing; m_poseState = PoseState.Standing;
m_customCrouchLimit = false; m_customCrouchLimit = false;
m_customProneLimit = false; m_customProneLimit = false;
m_customLocomotionOffset = false;
m_locomotionOffset = Vector3.zero;
m_avatarScale = 1f; m_avatarScale = 1f;
m_locomotionOffset = Vector3.zero;
m_emoteActive = false; m_emoteActive = false;
m_moving = false; m_moving = false;
m_hipsToPlayer = Vector3.zero; m_hipsToPlayer = Vector3.zero;
m_avatarHips = null; m_avatarHips = null;
m_viewPointHeight = 1f; m_viewPointHeight = 1f;
m_massCenter = Vector3.zero;
m_parameters.Clear(); m_parameters.Clear();
} }
internal void OnSetupAvatar() internal void OnSetupAvatar()
{ {
m_isInVR = Utils.IsInVR(); m_inVR = Utils.IsInVR();
m_vrIk = PlayerSetup.Instance._avatar.GetComponent<VRIK>(); m_vrIk = PlayerSetup.Instance._avatar.GetComponent<VRIK>();
m_locomotionLayer = PlayerSetup.Instance._animator.GetLayerIndex("Locomotion/Emotes"); m_locomotionLayer = PlayerSetup.Instance._animator.GetLayerIndex("Locomotion/Emotes");
m_avatarHips = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.Hips); m_avatarHips = PlayerSetup.Instance._animator.GetBoneTransform(HumanBodyBones.Hips);
@ -222,15 +226,31 @@ namespace ml_amt
m_customProneLimit = (l_customTransform != null); m_customProneLimit = (l_customTransform != null);
m_proneLimit = m_customProneLimit ? Mathf.Clamp01(l_customTransform.localPosition.y) : Settings.ProneLimit; 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 // Apply VRIK tweaks
if(m_vrIk != null) if(m_vrIk != null)
{ {
if(m_customLocomotionOffset) m_locomotionOffset = m_vrIk.solver.locomotion.offset;
m_vrIk.solver.locomotion.offset = m_locomotionOffset; 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.onPreSolverUpdate.AddListener(this.OnIKPreUpdate);
m_vrIk.onPostSolverUpdate.AddListener(this.OnIKPostUpdate); m_vrIk.onPostSolverUpdate.AddListener(this.OnIKPostUpdate);
@ -260,6 +280,8 @@ namespace ml_amt
m_ikWeight = m_vrIk.solver.IKPositionWeight; m_ikWeight = m_vrIk.solver.IKPositionWeight;
m_locomotionWeight = m_vrIk.solver.locomotion.weight; m_locomotionWeight = m_vrIk.solver.locomotion.weight;
m_plantFeet = m_vrIk.solver.plantFeet; 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) if(m_detectEmotes && m_emoteActive)
m_vrIk.solver.IKPositionWeight = 0f; m_vrIk.solver.IKPositionWeight = 0f;
@ -272,18 +294,22 @@ namespace ml_amt
if(m_ikOverrideFly && MovementSystem.Instance.flying) if(m_ikOverrideFly && MovementSystem.Instance.flying)
{ {
m_vrIk.solver.locomotion.weight = 0f; m_vrIk.solver.locomotion.weight = 0f;
m_vrIk.solver.leftLeg.useAnimatedBendNormal = true;
m_vrIk.solver.rightLeg.useAnimatedBendNormal = true;
l_legsOverride = true; l_legsOverride = true;
} }
if(m_ikOverrideJump && !m_grounded && !MovementSystem.Instance.flying) if(m_ikOverrideJump && !m_grounded && !MovementSystem.Instance.flying)
{ {
m_vrIk.solver.locomotion.weight = 0f; m_vrIk.solver.locomotion.weight = 0f;
m_vrIk.solver.leftLeg.useAnimatedBendNormal = true;
m_vrIk.solver.rightLeg.useAnimatedBendNormal = true;
l_legsOverride = true; l_legsOverride = true;
} }
bool l_solverActive = !Mathf.Approximately(m_vrIk.solver.IKPositionWeight, 0f); 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; m_vrIk.solver.plantFeet = false;
ABI_RC.Systems.IK.IKSystem.VrikRootController.enabled = 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.IKPositionWeight = m_ikWeight;
m_vrIk.solver.locomotion.weight = m_locomotionWeight; m_vrIk.solver.locomotion.weight = m_locomotionWeight;
m_vrIk.solver.plantFeet = m_plantFeet; 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) public void SetIKOverrideCrouch(bool p_state)
@ -320,7 +348,7 @@ namespace ml_amt
{ {
m_poseTransitions = p_state; 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("Crouching", false);
PlayerSetup.Instance.animatorManager.SetAnimatorParameterBool("Prone", false); PlayerSetup.Instance.animatorManager.SetAnimatorParameterBool("Prone", false);
@ -330,7 +358,7 @@ namespace ml_amt
{ {
m_adjustedMovement = p_state; m_adjustedMovement = p_state;
if(!m_adjustedMovement && m_avatarReady && m_isInVR) if(!m_adjustedMovement && m_avatarReady && m_inVR)
{ {
MovementSystem.Instance.ChangeCrouch(false); MovementSystem.Instance.ChangeCrouch(false);
MovementSystem.Instance.ChangeProne(false); MovementSystem.Instance.ChangeProne(false);
@ -352,6 +380,11 @@ namespace ml_amt
{ {
m_followHips = p_state; 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 float GetUpright() => m_upright;
public bool GetGroundedRaw() => m_groundedRaw; public bool GetGroundedRaw() => m_groundedRaw;

View file

@ -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]. * 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 flying:** disables legs locomotion/autostep in fly mode; default value - `true`.
* **IK override while jumping:** disables legs locomotion/autostep in jump; 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: 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. * 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`. * **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`. * **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`. * **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). * 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` * **Alternative avatar collider scale:** applies slightly different approach to avatar collider size change; default value - `true`
Available additional parameters for AAS animator: Available additional parameters for AAS animator:
@ -38,8 +40,5 @@ Available additional parameters for AAS animator:
* **`Moving`:** defines movement state of player * **`Moving`:** defines movement state of player
* Note: Can be set as local-only (not synced) if starts with `#` character. * 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: 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`. * Overrides FBT behaviour in 4PT mode (head, hands, hips). Be sure to disable legs and knees tracking in `Settings - IK tab`.

View file

@ -19,7 +19,8 @@ namespace ml_amt
IKOverrideJump, IKOverrideJump,
DetectEmotes, DetectEmotes,
FollowHips, FollowHips,
CollisionScale CollisionScale,
MassCenter
}; };
static bool ms_ikOverrideCrouch = true; static bool ms_ikOverrideCrouch = true;
@ -33,6 +34,7 @@ namespace ml_amt
static bool ms_detectEmotes = true; static bool ms_detectEmotes = true;
static bool ms_followHips = true; static bool ms_followHips = true;
static bool ms_collisionScale = true; static bool ms_collisionScale = true;
static bool ms_massCenter = true;
static MelonLoader.MelonPreferences_Category ms_category = null; static MelonLoader.MelonPreferences_Category ms_category = null;
static List<MelonLoader.MelonPreferences_Entry> ms_entries = null; static List<MelonLoader.MelonPreferences_Entry> ms_entries = null;
@ -48,6 +50,7 @@ namespace ml_amt
static public event Action<bool> DetectEmotesChange; static public event Action<bool> DetectEmotesChange;
static public event Action<bool> FollowHipsChange; static public event Action<bool> FollowHipsChange;
static public event Action<bool> CollisionScaleChange; static public event Action<bool> CollisionScaleChange;
static public event Action<bool> MassCenterChange;
internal static void Init() internal static void Init()
{ {
@ -65,7 +68,8 @@ namespace ml_amt
ms_category.CreateEntry(ModSetting.IKOverrideJump.ToString(), true), ms_category.CreateEntry(ModSetting.IKOverrideJump.ToString(), true),
ms_category.CreateEntry(ModSetting.DetectEmotes.ToString(), true), ms_category.CreateEntry(ModSetting.DetectEmotes.ToString(), true),
ms_category.CreateEntry(ModSetting.FollowHips.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(); Load();
@ -108,6 +112,7 @@ namespace ml_amt
ms_detectEmotes = (bool)ms_entries[(int)ModSetting.DetectEmotes].BoxedValue; ms_detectEmotes = (bool)ms_entries[(int)ModSetting.DetectEmotes].BoxedValue;
ms_followHips = (bool)ms_entries[(int)ModSetting.FollowHips].BoxedValue; ms_followHips = (bool)ms_entries[(int)ModSetting.FollowHips].BoxedValue;
ms_collisionScale = (bool)ms_entries[(int)ModSetting.CollisionScale].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) static void OnSliderUpdate(string p_name, string p_value)
@ -203,6 +208,12 @@ namespace ml_amt
CollisionScaleChange?.Invoke(ms_collisionScale); CollisionScaleChange?.Invoke(ms_collisionScale);
} }
break; 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); ms_entries[(int)l_setting].BoxedValue = bool.Parse(p_value);
@ -253,5 +264,9 @@ namespace ml_amt
{ {
get => ms_collisionScale; get => ms_collisionScale;
} }
public static bool MassCenter
{
get => ms_massCenter;
}
} }
} }

View file

@ -249,6 +249,13 @@ function inp_toggle_mod_amt(_obj, _callbackName) {
<div id="DetectEmotes" class ="inp_toggle no-scroll" data-current="true"></div> <div id="DetectEmotes" class ="inp_toggle no-scroll" data-current="true"></div>
</div> </div>
</div> </div>
<div class ="row-wrapper">
<div class ="option-caption">Adjusted locomotion mass center: </div>
<div class ="option-input">
<div id="MassCenter" class ="inp_toggle no-scroll" data-current="true"></div>
</div>
</div>
<div class ="row-wrapper"> <div class ="row-wrapper">
<div class ="option-caption">Alternative avatar collider scale: </div> <div class ="option-caption">Alternative avatar collider scale: </div>

View file

@ -16,7 +16,7 @@ namespace ml_lme
static readonly Quaternion ms_offsetRightDesktop = Quaternion.Euler(0f, 270f, 0f); static readonly Quaternion ms_offsetRightDesktop = Quaternion.Euler(0f, 270f, 0f);
VRIK m_vrIK = null; VRIK m_vrIK = null;
Vector2 m_armsWeights = Vector2.zero; Vector4 m_armsWeights = Vector2.zero;
bool m_inVR = false; bool m_inVR = false;
Transform m_hips = null; Transform m_hips = null;
Transform m_origLeftHand = null; Transform m_origLeftHand = null;
@ -350,14 +350,19 @@ namespace ml_lme
void OnIKPreUpdate() 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.positionWeight = 1f;
m_vrIK.solver.leftArm.rotationWeight = 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.positionWeight = 1f;
m_vrIK.solver.rightArm.rotationWeight = 1f; m_vrIK.solver.rightArm.rotationWeight = 1f;
@ -366,10 +371,9 @@ namespace ml_lme
void OnIKPostUpdate() void OnIKPostUpdate()
{ {
m_vrIK.solver.leftArm.positionWeight = m_armsWeights.x; m_vrIK.solver.leftArm.positionWeight = m_armsWeights.x;
m_vrIK.solver.leftArm.rotationWeight = m_armsWeights.x; m_vrIK.solver.leftArm.rotationWeight = m_armsWeights.y;
m_vrIK.solver.rightArm.positionWeight = m_armsWeights.z;
m_vrIK.solver.rightArm.positionWeight = m_armsWeights.y; m_vrIK.solver.rightArm.rotationWeight = m_armsWeights.w;
m_vrIK.solver.rightArm.rotationWeight = m_armsWeights.y;
} }
void RestoreVRIK() void RestoreVRIK()

View file

@ -1,10 +1,10 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyTitle("LeapMotionExtension")] [assembly: AssemblyTitle("LeapMotionExtension")]
[assembly: AssemblyVersion("1.3.0")] [assembly: AssemblyVersion("1.3.1")]
[assembly: AssemblyFileVersion("1.3.0")] [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.MelonGame(null, "ChilloutVR")]
[assembly: MelonLoader.MelonPlatform(MelonLoader.MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] [assembly: MelonLoader.MelonPlatform(MelonLoader.MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonLoader.MelonPlatformDomain(MelonLoader.MelonPlatformDomainAttribute.CompatibleDomains.MONO)] [assembly: MelonLoader.MelonPlatformDomain(MelonLoader.MelonPlatformDomainAttribute.CompatibleDomains.MONO)]

View file

@ -12,15 +12,15 @@ This mod allows you to use your Leap Motion controller for hands and fingers tra
# Usage # Usage
## Settings ## Settings
Available mod's settings in `Settings - Implementation - Leap Motion Tracking`: Available mod's settings in `Settings - Implementation - Leap Motion Tracking`:
* **Enable tracking:** enable hands tracking from Leap Motion data, disabled by default. * **Enable tracking:** enables/disables hands tracking from Leap Motion data, disabled by default.
* **Tracking mode:** set Leap Motion tracking mode, available values: `Screentop`, `Desktop` (by default), `HMD`. * **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. * **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. * **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. * **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. * **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. * **Fingers tracking only:** applies only fingers tracking, disabled by default.
* **Model visibility:** show Leap Motion controller model, useful for tracking visualizing, 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. * **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. * **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. * **Grip gesture threadhold:** activation limit for grip based on hand gesture; 40 by default.

View file

@ -1,5 +1,8 @@
using ABI.CCK.Components; using ABI.CCK.Components;
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.Player; using ABI_RC.Core.Player;
using RootMotion.FinalIK;
using System.Reflection;
using UnityEngine; using UnityEngine;
namespace ml_pam namespace ml_pam
@ -7,57 +10,275 @@ namespace ml_pam
[DisallowMultipleComponent] [DisallowMultipleComponent]
class ArmMover : MonoBehaviour 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 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; bool m_enabled = true;
CVRPickupObject m_target = null; ArmIK m_armIK = null;
Transform m_target = null;
Transform m_rotationTarget = null;
CVRPickupObject m_pickup = null;
Matrix4x4 m_offset = Matrix4x4.identity; Matrix4x4 m_offset = Matrix4x4.identity;
bool m_targetActive = false;
void Start() void Start()
{ {
m_animator = PlayerSetup.Instance._animator; m_inVR = Utils.IsInVR();
m_mainLayer = m_animator.GetLayerIndex("Locomotion/Emotes");
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); void OnIKPreUpdate()
m_animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f); {
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<VRIK>();
if(PlayerSetup.Instance._animator.isHuman)
{
HumanPose l_currentPose = new HumanPose();
HumanPoseHandler l_poseHandler = null;
if(!m_inVR)
{ {
case CVRPickupObject.GripType.Origin: l_poseHandler = new HumanPoseHandler(PlayerSetup.Instance._animator.avatar, PlayerSetup.Instance._avatar.transform);
{ l_poseHandler.GetHumanPose(ref l_currentPose);
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;
case CVRPickupObject.GripType.Free: HumanPose l_tPose = new HumanPose
{ {
Matrix4x4 l_result = m_target.transform.GetMatrix() * m_offset; bodyPosition = l_currentPose.bodyPosition,
m_animator.SetIKPosition(AvatarIKGoal.RightHand, l_result * ms_pointVector); bodyRotation = l_currentPose.bodyRotation,
m_animator.SetIKRotation(AvatarIKGoal.RightHand, l_camera.rotation * ms_rotationOffset); 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<ArmIK>();
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; if(m_pickup == p_pickup)
m_offset = (m_target != null) ? (p_target.transform.GetMatrix().inverse * Matrix4x4.Translate(p_hit)): Matrix4x4.identity; {
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;
} }
} }
} }

View file

@ -1,4 +1,5 @@
using ABI.CCK.Components; using ABI.CCK.Components;
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.Player; using ABI_RC.Core.Player;
using System; using System;
using System.Reflection; using System.Reflection;
@ -10,13 +11,15 @@ namespace ml_pam
{ {
static PickupArmMovement ms_instance = null; static PickupArmMovement ms_instance = null;
ArmMover m_localPuller = null; ArmMover m_localMover = null;
public override void OnInitializeMelon() public override void OnInitializeMelon()
{ {
if(ms_instance == null) if(ms_instance == null)
ms_instance = this; ms_instance = this;
Settings.Init();
HarmonyInstance.Patch( HarmonyInstance.Patch(
typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.ClearAvatar)), typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.ClearAvatar)),
null, null,
@ -37,6 +40,21 @@ namespace ml_pam
null, null,
new HarmonyLib.HarmonyMethod(typeof(PickupArmMovement).GetMethod(nameof(OnCVRPickupObjectDrop_Postfix), BindingFlags.Static | BindingFlags.NonPublic)) 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<ArmMover>();
} }
public override void OnDeinitializeMelon() public override void OnDeinitializeMelon()
@ -50,7 +68,8 @@ namespace ml_pam
{ {
try try
{ {
m_localPuller = null; if(m_localMover != null)
m_localMover.OnAvatarClear();
} }
catch(Exception e) catch(Exception e)
{ {
@ -63,8 +82,8 @@ namespace ml_pam
{ {
try try
{ {
if(!Utils.IsInVR()) if(m_localMover != null)
m_localPuller = PlayerSetup.Instance._avatar.AddComponent<ArmMover>(); m_localMover.OnAvatarSetup();
} }
catch(Exception e) catch(Exception e)
{ {
@ -72,15 +91,13 @@ namespace ml_pam
} }
} }
static void OnCVRPickupObjectGrab_Postfix(ref CVRPickupObject __instance, Vector3 __2) => ms_instance?.OnCVRPickupObjectGrab(__instance, __2); static void OnCVRPickupObjectGrab_Postfix(ref CVRPickupObject __instance, ControllerRay __1, Vector3 __2) => ms_instance?.OnCVRPickupObjectGrab(__instance, __1, __2);
void OnCVRPickupObjectGrab(CVRPickupObject p_pickup, Vector3 p_hit) void OnCVRPickupObjectGrab(CVRPickupObject p_pickup, ControllerRay p_ray, Vector3 p_hit)
{ {
try try
{ {
if(p_pickup.IsGrabbedByMe() && (m_localPuller != null)) if(p_pickup.IsGrabbedByMe() && (m_localMover != null))
{ m_localMover.OnPickupGrab(p_pickup, p_ray, p_hit);
m_localPuller.SetTarget(p_pickup, p_hit);
}
} }
catch(Exception e) catch(Exception e)
{ {
@ -88,15 +105,27 @@ namespace ml_pam
} }
} }
static void OnCVRPickupObjectDrop_Postfix() => ms_instance?.OnCVRPickupObjectDrop(); static void OnCVRPickupObjectDrop_Postfix(ref CVRPickupObject __instance) => ms_instance?.OnCVRPickupObjectDrop(__instance);
void OnCVRPickupObjectDrop() void OnCVRPickupObjectDrop(CVRPickupObject p_pickup)
{ {
try try
{ {
if(m_localPuller != null) if(m_localMover != null)
{ m_localMover.OnPickupDrop(p_pickup);
m_localPuller.SetTarget(null, Vector3.zero); }
} 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) catch(Exception e)
{ {

View file

@ -1,10 +1,11 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyTitle("PickupArmMovement")] [assembly: AssemblyTitle("PickupArmMovement")]
[assembly: AssemblyVersion("1.0.0")] [assembly: AssemblyVersion("1.0.1")]
[assembly: AssemblyFileVersion("1.0.0")] [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.MelonGame(null, "ChilloutVR")]
[assembly: MelonLoader.MelonPriority(1)]
[assembly: MelonLoader.MelonPlatform(MelonLoader.MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] [assembly: MelonLoader.MelonPlatform(MelonLoader.MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonLoader.MelonPlatformDomain(MelonLoader.MelonPlatformDomainAttribute.CompatibleDomains.MONO)] [assembly: MelonLoader.MelonPlatformDomain(MelonLoader.MelonPlatformDomainAttribute.CompatibleDomains.MONO)]

View file

@ -1,7 +1,12 @@
# Pickup Arm Movement # Pickup Arm Movement
This mod adds arm tracking upon holding pickup. This mod adds arm tracking upon holding pickup in desktop mode.
# Installation # Installation
* Install [latest MelonLoader](https://github.com/LavaGang/MelonLoader) * Install [latest MelonLoader](https://github.com/LavaGang/MelonLoader)
* Get [latest release DLL](../../../releases/latest): * Get [latest release DLL](../../../releases/latest):
* Put `ml_pam.dll` in `Mods` folder of game * 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`.

26
ml_pam/Scripts.cs Normal file
View file

@ -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;
}
}
}

113
ml_pam/Settings.cs Normal file
View file

@ -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<MelonLoader.MelonPreferences_Entry> ms_entries = null;
static public event Action<bool> EnabledChange;
static public event Action<float> GrabOffsetChange;
internal static void Init()
{
ms_category = MelonLoader.MelonPreferences.CreateCategory("PAM");
ms_entries = new List<MelonLoader.MelonPreferences_Entry>()
{
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<string, string>(OnToggleUpdate));
ViewManager.Instance.gameMenuView.View.BindCall("MelonMod_PAM_Call_InpSlider", new Action<string, string>(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;
}
}
}

View file

@ -42,6 +42,16 @@
<HintPath>D:\Games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp.dll</HintPath> <HintPath>D:\Games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<Private>False</Private>
</Reference>
<Reference Include="cohtml.Net">
<Private>False</Private>
</Reference>
<Reference Include="Cohtml.Runtime">
<Private>False</Private>
</Reference>
<Reference Include="MelonLoader, Version=0.5.7.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="MelonLoader, Version=0.5.7.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>D:\Games\Steam\steamapps\common\ChilloutVR\MelonLoader\MelonLoader.dll</HintPath> <HintPath>D:\Games\Steam\steamapps\common\ChilloutVR\MelonLoader\MelonLoader.dll</HintPath>
@ -66,8 +76,13 @@
<Compile Include="ArmMover.cs" /> <Compile Include="ArmMover.cs" />
<Compile Include="Main.cs" /> <Compile Include="Main.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Scripts.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Utils.cs" /> <Compile Include="Utils.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="resources\menu.js" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup> <PropertyGroup>
<PostBuildEvent>copy /y "$(TargetPath)" "D:\Games\Steam\steamapps\common\ChilloutVR\Mods\"</PostBuildEvent> <PostBuildEvent>copy /y "$(TargetPath)" "D:\Games\Steam\steamapps\common\ChilloutVR\Mods\"</PostBuildEvent>

209
ml_pam/resources/menu.js Normal file
View file

@ -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 + "&apos;" + inches + '&apos;&apos;';
}
}
}
return {
name: this.name,
value: this.getValue,
updateValue: this.updateValue
}
}
// Add own menu
{
let l_block = document.createElement('div');
l_block.innerHTML = `
<div class ="settings-subcategory">
<div class ="subcategory-name">Pickup Arm Mover</div>
<div class ="subcategory-description"></div>
</div>
<div class ="row-wrapper">
<div class ="option-caption">Enable hand movement: </div>
<div class ="option-input">
<div id="Enabled" class ="inp_toggle no-scroll" data-current="true"></div>
</div>
</div>
<div class ="row-wrapper">
<div class ="option-caption">Grab offset: </div>
<div class ="option-input">
<div id="GrabOffset" class ="inp_slider no-scroll" data-min="0" data-max="100" data-current="25"></div>
</div>
</div>
`;
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');
}
}