diff --git a/DesktopInteractions/DesktopInteractions.csproj b/DesktopInteractions/DesktopInteractions.csproj
new file mode 100644
index 0000000..14fc5af
--- /dev/null
+++ b/DesktopInteractions/DesktopInteractions.csproj
@@ -0,0 +1,9 @@
+
+
+
+ PlayerCloneAttachment
+
+
+
+
+
diff --git a/DesktopInteractions/Main.cs b/DesktopInteractions/Main.cs
new file mode 100644
index 0000000..a92b976
--- /dev/null
+++ b/DesktopInteractions/Main.cs
@@ -0,0 +1,235 @@
+using System.Reflection;
+using ABI_RC.Core.Player;
+using ABI_RC.Core.Savior;
+using ABI_RC.Systems.ChatBox;
+using ABI_RC.Systems.GameEventSystem;
+using ABI_RC.Systems.IK;
+using ABI.CCK.Components;
+using HarmonyLib;
+using MelonLoader;
+using RootMotion.FinalIK;
+using UnityEngine;
+
+namespace NAK.DesktopInteractions;
+
+public class DesktopInteractionsMod : MelonMod
+{
+ private static readonly MelonPreferences_Category Category =
+ MelonPreferences.CreateCategory(nameof(DesktopInteractions));
+
+ private static readonly MelonPreferences_Entry EntryTypingGesture =
+ Category.CreateEntry("enable_typing_gesture", true,
+ "Typing Gesture", description: "When enabled you will place your arm up to your ear when typing in ChatBox.");
+
+ private static readonly MelonPreferences_Entry EntryZoomGesture =
+ Category.CreateEntry("enable_zoom_gesture", false,
+ "Zoom Gesture", description: "When enabled you will cup your hands around your eyes while zooming.");
+
+ public override void OnInitializeMelon()
+ {
+ CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(OnPlayerSetupStart);
+ CVRGameEventSystem.Avatar.OnLocalAvatarLoad.AddListener(OnLocalAvatarLoad);
+ CVRGameEventSystem.Avatar.OnLocalAvatarHeightScale.AddListener(OnLocalAvatarHeightScale);
+
+ HarmonyInstance.Patch(
+ typeof(IKSystem).GetMethod(nameof(IKSystem.OnPreSolverUpdateGeneral),
+ BindingFlags.NonPublic | BindingFlags.Instance),
+ prefix: new HarmonyMethod(typeof(DesktopInteractionsMod).GetMethod(nameof(OnPreSolverUpdateGeneral),
+ BindingFlags.NonPublic | BindingFlags.Static))
+ );
+ }
+
+ private static Transform _cameraTargetContainerTransform;
+ private static Transform _earIKTargetTransform;
+ private static Transform _leftEyeIKTargetTransform;
+ private static Transform _rightEyeIKTargetTransform;
+ private static Transform _calibratedEarIKTargetTransform;
+ private static Transform _calibratedLeftEyeIKTargetTransform;
+ private static Transform _calibratedRightEyeIKTargetTransform;
+
+ private static IKLimbController _leftArmController;
+ private static IKLimbController _rightArmController;
+
+ private static void OnPlayerSetupStart()
+ {
+ Transform cameraTransform = PlayerSetup.Instance.desktopCamera.transform;
+
+ _cameraTargetContainerTransform = new GameObject("ScaledTargetsContainer").transform;
+ _cameraTargetContainerTransform.SetParent(cameraTransform, false);
+ _cameraTargetContainerTransform.localScale = Vector3.one * PlayerSetup.Instance.GetPlaySpaceScale() * 1.8f;
+
+ _earIKTargetTransform = new GameObject("LeftEarIKTarget").transform;
+ _earIKTargetTransform.SetParent(_cameraTargetContainerTransform);
+ _earIKTargetTransform.localPosition = new Vector3(-0.1141031f, -0.05610896f, 0.01008159f);
+ _earIKTargetTransform.localRotation = Quaternion.Euler(new Vector3(-63.037f, -108.763f, 141.237f));
+ _calibratedEarIKTargetTransform = new GameObject("CalibratedOffset").transform;
+ _calibratedEarIKTargetTransform.SetParent(_earIKTargetTransform, false);
+
+ _leftEyeIKTargetTransform = new GameObject("LeftEyeIKTarget").transform;
+ _leftEyeIKTargetTransform.SetParent(_cameraTargetContainerTransform);
+ _leftEyeIKTargetTransform.localPosition = new Vector3(-0.0768f, -0.0551f, 0.0278f);
+ _leftEyeIKTargetTransform.localRotation = Quaternion.Euler(new Vector3(-47.436f, -34.336f, 84.604f));
+ _calibratedLeftEyeIKTargetTransform = new GameObject("CalibratedOffset").transform;
+ _calibratedLeftEyeIKTargetTransform.SetParent(_leftEyeIKTargetTransform, false);
+
+ _rightEyeIKTargetTransform = new GameObject("RightEyeIKTarget").transform;
+ _rightEyeIKTargetTransform.SetParent(_cameraTargetContainerTransform);
+ _rightEyeIKTargetTransform.localPosition = new Vector3(0.0768f, -0.0551f, 0.0278f);
+ _rightEyeIKTargetTransform.localRotation = Quaternion.Euler(new Vector3(-132.564f, -145.664f, 95.396f));
+ _calibratedRightEyeIKTargetTransform = new GameObject("CalibratedOffset").transform;
+ _calibratedRightEyeIKTargetTransform.SetParent(_rightEyeIKTargetTransform, false);
+
+ _leftArmController = new IKLimbController(8f);
+ _rightArmController = new IKLimbController(8f);
+ }
+
+ private static void OnLocalAvatarLoad(CVRAvatar _)
+ {
+ if (!IKSystem.Instance.IsAvatarCalibrated()) return; // IKSystem did not consider for setup
+
+ IKSystem.Instance.SetAvatarPose(IKSystem.AvatarPose.Default);
+
+ VRIK vrik = IKSystem.vrik;
+ Quaternion localHandRotationLeft = IKCalibrator.CalculateLocalRotation(vrik.references.root, vrik.references.leftHand);
+ _calibratedEarIKTargetTransform.localRotation = localHandRotationLeft;
+ _calibratedLeftEyeIKTargetTransform.localRotation = localHandRotationLeft;
+
+ Quaternion localHandRotationRight = IKCalibrator.CalculateLocalRotation(vrik.references.root, vrik.references.rightHand);
+ _calibratedRightEyeIKTargetTransform.localRotation = localHandRotationRight;
+
+ IKSystem.Instance.SetAvatarPose(IKSystem.AvatarPose.LastSaved);
+ }
+
+ // I fucked the offsets and didn't account for scale until now, so we have to adjust it
+ private static void OnLocalAvatarHeightScale(float height, float scale)
+ => _cameraTargetContainerTransform.localScale = Vector3.one * scale * 1.8f;
+
+ private static void OnPreSolverUpdateGeneral()
+ {
+ if (MetaPort.Instance.isUsingVr) return;
+
+ bool isTyping = EntryTypingGesture.Value && ChatBoxManager.Instance.LocalPlayerBubble.IsTypingIndicatorActive;
+ bool isZooming = EntryZoomGesture.Value && CVR_DesktopCameraController.GetCurrentZoomModifier() > 0.25f;
+
+ _leftArmController.SetInfluence(10, isTyping, _calibratedEarIKTargetTransform);
+ _leftArmController.SetInfluence(5, isZooming, _calibratedLeftEyeIKTargetTransform);
+ _leftArmController.Update(Time.deltaTime);
+
+ IKSolverVR solver = IKSystem.vrik.solver;
+
+ IKSolverVR.Arm leftArm = solver.leftArm;
+ leftArm.positionWeight = _leftArmController.Weight;
+ leftArm.rotationWeight = _leftArmController.Weight;
+ leftArm.IKPosition = _leftArmController.Position;
+ leftArm.IKRotation = _leftArmController.Rotation;
+
+ _rightArmController.SetInfluence(5, isZooming, _calibratedRightEyeIKTargetTransform);
+ _rightArmController.Update(Time.deltaTime);
+
+ IKSolverVR.Arm rightArm = solver.rightArm;
+ rightArm.positionWeight = _rightArmController.Weight;
+ rightArm.rotationWeight = _rightArmController.Weight;
+ rightArm.IKPosition = _rightArmController.Position;
+ rightArm.IKRotation = _rightArmController.Rotation;
+ }
+}
+
+public class IKLimbController(float blendSpeed)
+{
+ private struct Influence
+ {
+ public int Priority;
+ public bool IsActive;
+ public Transform Target;
+ }
+
+ private readonly Influence[] _influences = new Influence[8];
+ private int _count;
+
+ private Vector3 _position;
+ private Quaternion _rotation = Quaternion.identity;
+ private float _weight;
+
+ private Transform _fromTarget;
+ private Transform _toTarget;
+ /*private Vector3 _fromPosition;
+ private Quaternion _fromRotation;*/
+ private float _blendT;
+
+ public Vector3 Position => _position;
+ public Quaternion Rotation => _rotation;
+ public float Weight => _weight;
+
+ public void SetInfluence(int priority, bool isActive, Transform target)
+ {
+ int index = -1;
+ for (int i = 0; i < _count; i++)
+ {
+ if (_influences[i].Priority == priority)
+ {
+ index = i;
+ break;
+ }
+ if (_influences[i].Priority < priority)
+ {
+ index = i;
+ for (int j = _count; j > i; j--) _influences[j] = _influences[j - 1];
+ _count++;
+ break;
+ }
+ }
+
+ if (index == -1)
+ {
+ if (_count >= _influences.Length) return;
+ index = _count++;
+ }
+
+ _influences[index] = new Influence { Priority = priority, IsActive = isActive, Target = target };
+ }
+
+ public void Update(float deltaTime)
+ {
+ Transform target = null;
+ for (int i = 0; i < _count; i++)
+ {
+ if (!_influences[i].IsActive) continue;
+ target = _influences[i].Target;
+ break;
+ }
+
+ float targetWeight;
+ if (target != null)
+ {
+ targetWeight = 1f;
+
+ if (_toTarget != target)
+ {
+ /*if (_toTarget != null)
+ {
+ _fromPosition = _toTarget.position;
+ _fromRotation = _toTarget.rotation;
+ }
+ else
+ {
+ _fromPosition = target.position;
+ _fromRotation = target.rotation;
+ }*/
+ _fromTarget = _toTarget ?? target;
+ _toTarget = target;
+ _blendT = 0f;
+ }
+
+ _blendT = Mathf.Min(_blendT + blendSpeed * deltaTime, 1f);
+ _position = Vector3.Lerp(_fromTarget.position, _toTarget.position, _blendT);
+ _rotation = Quaternion.Slerp(_fromTarget.rotation, _toTarget.rotation, _blendT);
+ }
+ else
+ {
+ targetWeight = 0f;
+ _toTarget = null;
+ }
+
+ _weight = Mathf.MoveTowards(_weight, targetWeight, blendSpeed * deltaTime);
+ }
+}
\ No newline at end of file
diff --git a/DesktopInteractions/Properties/AssemblyInfo.cs b/DesktopInteractions/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..1f42b09
--- /dev/null
+++ b/DesktopInteractions/Properties/AssemblyInfo.cs
@@ -0,0 +1,32 @@
+using MelonLoader;
+using NAK.DesktopInteractions.Properties;
+using System.Reflection;
+
+[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyTitle(nameof(NAK.DesktopInteractions))]
+[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
+[assembly: AssemblyProduct(nameof(NAK.DesktopInteractions))]
+
+[assembly: MelonInfo(
+ typeof(NAK.DesktopInteractions.DesktopInteractionsMod),
+ nameof(NAK.DesktopInteractions),
+ AssemblyInfoParams.Version,
+ AssemblyInfoParams.Author,
+ downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/DesktopInteractions"
+)]
+
+[assembly: MelonGame("ChilloutVR", "ChilloutVR")]
+[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
+[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
+[assembly: MelonColor(255, 246, 25, 99)] // red-pink
+[assembly: MelonAuthorColor(255, 158, 21, 32)] // red
+[assembly: HarmonyDontPatchAll]
+
+namespace NAK.DesktopInteractions.Properties;
+internal static class AssemblyInfoParams
+{
+ public const string Version = "1.0.0";
+ public const string Author = "NotAKidoS";
+}
\ No newline at end of file
diff --git a/DesktopInteractions/README.md b/DesktopInteractions/README.md
new file mode 100644
index 0000000..2ba4272
--- /dev/null
+++ b/DesktopInteractions/README.md
@@ -0,0 +1,14 @@
+# DesktopInteractions
+
+Adds IK-driven hand gestures to your avatar in Desktop: earpiece grab (GMOD-style) when typing in ChatBox, and binocular cupping when zooming. Both gestures are toggleable in settings.
+
+---
+
+Here is the block of text where I tell you this mod is not affiliated with or endorsed by ABI.
+https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games
+
+> This mod is an independent creation not affiliated with, supported by, or approved by Alpha Blend Interactive.
+
+> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use.
+
+> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive.
diff --git a/DesktopInteractions/format.json b/DesktopInteractions/format.json
new file mode 100644
index 0000000..bcb57d6
--- /dev/null
+++ b/DesktopInteractions/format.json
@@ -0,0 +1,23 @@
+{
+ "_id": -1,
+ "name": "DesktopInteractions",
+ "modversion": "1.0.0",
+ "gameversion": "2025r181",
+ "loaderversion": "0.7.2",
+ "modtype": "Mod",
+ "author": "NotAKidoS",
+ "description": "Adds IK-driven hand gestures to your avatar in Desktop: earpiece grab (GMOD-style) when typing in ChatBox, and binocular cupping when zooming. Both gestures are toggleable in settings.",
+ "searchtags": [
+ "gmod",
+ "typing",
+ "zooming",
+ "desktop"
+ ],
+ "requirements": [
+ "None"
+ ],
+ "downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/DesktopInteractions.dll",
+ "sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/DesktopInteractions/",
+ "changelog": "- Initial release",
+ "embedcolor": "#f61963"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index b4ed498..d317016 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,10 @@
| [ASTExtension](ASTExtension/README.md) | Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool): | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/ASTExtension.dll) |
| [ConfigureCalibrationPose](ConfigureCalibrationPose/README.md) | Select FBT calibration pose. | No Download |
| [CustomSpawnPoint](CustomSpawnPoint/README.md) | Replaces the unused Images button in the World Details page with a button to set a custom spawn point. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/CustomSpawnPoint.dll) |
+| [DesktopInteractions](DesktopInteractions/README.md) | Adds IK-driven hand gestures to your avatar in Desktop: earpiece grab (GMOD-style) when typing in ChatBox, and binocular cupping when zooming. Both gestures are toggleable in settings. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/DesktopInteractions.dll) |
| [DoubleTapJumpToExitSeat](DoubleTapJumpToExitSeat/README.md) | Replaces seat exit controls with a double-tap of the jump button, avoiding accidental exits from joystick drift or opening the menu. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/DoubleTapJumpToExitSeat.dll) |
| [FuckToes](FuckToes/README.md) | Prevents VRIK from autodetecting toes in Halfbody or Fullbody. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/FuckToes.dll) |
-| [PlapPlapForAll](PlapPlapForAll/README.md) | Penetrator SFX mod which adds Noach's PlapPlap prefab to any detected DPS setup on avatars that do not already have it. | No Download |
+| [PlapPlapForAll](PlapPlapForAll/README.md) | Penetrator SFX mod which adds Noach's PlapPlap prefab to any detected DPS setup on avatars that do not already have it. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/PlapPlapForAll.dll) |
| [PropLoadingHexagon](PropLoadingHexagon/README.md) | https://github.com/NotAKidoS/NAK_CVR_Mods/assets/37721153/a892c765-71c1-47f3-a781-bdb9b60ba117 | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/PropLoadingHexagon.dll) |
| [RCCVirtualSteeringWheel](RCCVirtualSteeringWheel/README.md) | Allows you to physically grab rigged RCC steering wheels in VR to provide steering input. No explicit setup required other than defining the Steering Wheel transform within the RCC component. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/RCCVirtualSteeringWheel.dll) |
| [RelativeSyncJitterFix](RelativeSyncJitterFix/README.md) | Relative sync jitter fix is the single harmony patch that could not make it into the native release of RelativeSync. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/RelativeSyncJitterFix.dll) |