diff --git a/RelativeSync/FixedJointPickupable.cs b/RelativeSync/FixedJointPickupable.cs new file mode 100644 index 0000000..8305da4 --- /dev/null +++ b/RelativeSync/FixedJointPickupable.cs @@ -0,0 +1,221 @@ +using ABI_RC.Core.InteractionSystem.Base; +using ABI.CCK.Components; +using UnityEngine; + +// https://wirewhiz.com/vr-grabbing-tutorial/ + +public class CollisionLogger : MonoBehaviour +{ + private int _touchCount; + public int touchCount + { + get => _touchCount; + private set + { + if (!countChangedThisFrame && _touchCount != value) countChangedThisFrame = true; + _touchCount = Mathf.Max(0, value); + } + } + + public bool countChangedThisFrame {get; private set;} = true; + + private void OnCollisionEnter(Collision _) + => touchCount++; + private void OnCollisionExit(Collision _) + => touchCount--; + private void LateUpdate() + => countChangedThisFrame = false; +} + +public class FixedJointPickupable : Pickupable +{ + private Rigidbody _rigidbody; + private bool _originalGravityState; + + private Vector3 _initialGrabOffset; + private Vector3 _unCollidedOffset; + private float _unCollideInterpolationTime; + + private readonly JointDrive positionDrive = new() { positionSpring = 2000, positionDamper = 10, maximumForce = 3.402823e+38f }; + private readonly JointDrive rotationDrive = new() { positionSpring = 2000, positionDamper = 0, maximumForce = 3.402823e+38f }; + private ConfigurableJoint _configurableJoint; + private FixedJoint _fixedJoint; + + private CollisionLogger _collisionLogger; + + private Transform _pickupHandTransform; + private Vector3 _previousHandPosition; + private Vector3 _previousHandRotation; + private float _grabStartTime; + + public override bool CanPickup => IsPickupable && isActiveAndEnabled; + + public override bool DisallowTheft => false; + + public override bool IsAutoHold => false; + + public override float MaxGrabDistance => 100f; + + public override float MaxPushDistance => 100f; + + public override bool IsObjectRotationAllowed => true; + + public override bool IsObjectPushPullAllowed => true; + + public override bool IsObjectInteractionAllowed => true; + + public override Transform RootTransform => base.transform; + + private void Start() + { + _rigidbody = GetComponent(); + if (_rigidbody == null) + { + _rigidbody = gameObject.AddComponent(); + _rigidbody.useGravity = true; + _rigidbody.isKinematic = false; + } + } + + public override void OnGrab(Vector3 grabPoint) + { + _grabStartTime = Time.time; + _pickupHandTransform = ControllerRay.pivotPoint.transform; + + ControllerRay.UpdateGrabDistance(Vector3.Distance(ControllerRay.transform.position, grabPoint)); + _pickupHandTransform.rotation = transform.rotation; + + // handTransform in local space of the object (initial rotation is already known) + _initialGrabOffset = transform.InverseTransformPoint(grabPoint); + + // ensure our object has a CollisionLogger (hack) + if (!gameObject.TryGetComponent(out _collisionLogger)) + _collisionLogger = gameObject.AddComponent(); + + // ensure ControllerRay has a rigidbody (hack) + if (!_pickupHandTransform.TryGetComponent(out Rigidbody handRigidBody)) + { + handRigidBody = _pickupHandTransform.gameObject.AddComponent(); + handRigidBody.isKinematic = true; + handRigidBody.useGravity = false; + } + + // ensure Pickupable has a fixed joint (hack) + if (!_pickupHandTransform.gameObject.TryGetComponent(out _fixedJoint)) + { + _fixedJoint = _pickupHandTransform.gameObject.AddComponent(); + _fixedJoint.enableCollision = false; + _fixedJoint.autoConfigureConnectedAnchor = false; + _fixedJoint.connectedAnchor = _initialGrabOffset; // it'll snap to this position after interpolation + } + + // ensure Pickupable has a configurable joint (hack) + if (!_pickupHandTransform.gameObject.TryGetComponent(out _configurableJoint)) + { + _configurableJoint = _pickupHandTransform.gameObject.AddComponent(); + + _configurableJoint.enableCollision = false; + _configurableJoint.autoConfigureConnectedAnchor = false; + + _configurableJoint.xMotion = ConfigurableJointMotion.Free; + _configurableJoint.yMotion = ConfigurableJointMotion.Free; + _configurableJoint.zMotion = ConfigurableJointMotion.Free; + _configurableJoint.angularXMotion = ConfigurableJointMotion.Free; + _configurableJoint.angularYMotion = ConfigurableJointMotion.Free; + _configurableJoint.angularZMotion = ConfigurableJointMotion.Free; + + _configurableJoint.xDrive = positionDrive; + _configurableJoint.yDrive = positionDrive; + _configurableJoint.zDrive = positionDrive; + + _configurableJoint.rotationDriveMode = RotationDriveMode.Slerp; + _configurableJoint.slerpDrive = rotationDrive; + } + + //_fixedJoint.connectedBody = _rigidbody; + _configurableJoint.connectedBody = _rigidbody; // start off floppy + + _originalGravityState = _rigidbody.useGravity; + _rigidbody.useGravity = false; + + // Reset velocities to avoid unintended motion + _rigidbody.velocity = Vector3.zero; + _rigidbody.angularVelocity = Vector3.zero; + } + + public override void OnDrop() + { + if (_collisionLogger != null) + { + Destroy(_collisionLogger); + _collisionLogger = null; + } + + if (_fixedJoint != null) + { + Destroy(_fixedJoint); + _fixedJoint = null; + } + + if (_configurableJoint != null) + { + Destroy(_configurableJoint); + _configurableJoint = null; + } + + _rigidbody.useGravity = _originalGravityState; + } + + public override void OnFlingTowardsTarget(Vector3 target) + { + // Implement fling logic if needed + } + + private void Update() + { + if (!IsPickedUp) + return; + + if (_unCollideInterpolationTime < 1f) + { + _unCollideInterpolationTime += Time.deltaTime / 0.1f; // 0.01s to interpolate to hand + _unCollideInterpolationTime = Mathf.Min(_unCollideInterpolationTime, 1f); + + _fixedJoint.connectedBody = null; // hack so we don't need to lerp connectedAnchor ourselves + transform.rotation = Quaternion.Slerp(transform.rotation, _pickupHandTransform.rotation, _unCollideInterpolationTime); + //transform.position = Vector3.Lerp(transform.position, transform.TransformPoint(_initialGrabOffset), _unCollideInterpolationTime); // no work + _fixedJoint.connectedBody = _rigidbody; // keep interpolation start point + _fixedJoint.connectedAnchor = Vector3.Lerp(_unCollidedOffset, _initialGrabOffset, _unCollideInterpolationTime); + } + + if (_collisionLogger.countChangedThisFrame) + { + if (_collisionLogger.touchCount == 0) + { + _configurableJoint.connectedBody = null; + _fixedJoint.connectedBody = _rigidbody; // keep interpolation start point + _fixedJoint.connectedAnchor = _unCollidedOffset = transform.InverseTransformPoint(_pickupHandTransform.position); + _unCollideInterpolationTime = 0f; // interpolate to hand + } + else if(_collisionLogger.touchCount > 0) + { + _fixedJoint.connectedBody = null; + _configurableJoint.connectedBody = _rigidbody; + _unCollideInterpolationTime = 1f; // no interpolation + } + } + + UpdatePositionAndRotation(); + + _previousHandPosition = _pickupHandTransform.position; + _previousHandRotation = _pickupHandTransform.rotation.eulerAngles; + } + + private void UpdatePositionAndRotation() + { + if (_rigidbody.isKinematic) + { + transform.SetPositionAndRotation(_pickupHandTransform.position, _pickupHandTransform.rotation); + } + } +} \ No newline at end of file diff --git a/RelativeSync/Main.cs b/RelativeSync/Main.cs index 83ec331..1104510 100644 --- a/RelativeSync/Main.cs +++ b/RelativeSync/Main.cs @@ -18,6 +18,9 @@ public class RelativeSyncMod : MelonMod // Experimental sync hack ApplyPatches(typeof(CVRSpawnablePatches)); + // Experimental pickup in chair hack + ApplyPatches(typeof(CVRPickupObjectPatches)); + // Experimental no interpolation on Better Better Character Controller ApplyPatches(typeof(BetterBetterCharacterControllerPatches)); diff --git a/RelativeSync/ModSettings.cs b/RelativeSync/ModSettings.cs index 91a60a7..4d73cca 100644 --- a/RelativeSync/ModSettings.cs +++ b/RelativeSync/ModSettings.cs @@ -28,6 +28,10 @@ internal static class ModSettings Category.CreateEntry("ExpNoInterpolationOnBBCC", true, "Exp Disable Interpolation on BBCC", description: "Disable interpolation on Better Better Character Controller. May help reduce local jitter on synced movement parents."); + private static readonly MelonPreferences_Entry ExpSeatAndPickupsHack = + Category.CreateEntry("ExpSeatAndPickupsHack", true, + "Exp Seat and Pickups Hack", description: "Forces CVRSeat to update after Character Controller update."); + #endregion Melon Preferences internal static void Initialize() @@ -44,5 +48,6 @@ internal static class ModSettings ModNetwork.Debug_NetworkOutbound = DebugLogOutbound.Value; Patches.CVRSpawnablePatches.UseHack = ExpSyncedObjectHack.Value; Patches.BetterBetterCharacterControllerPatches.NoInterpolation = ExpNoInterpolationOnBBCC.Value; + Patches.BetterBetterCharacterControllerPatches.UseSeatAndPickupsHack = ExpSeatAndPickupsHack.Value; } } \ No newline at end of file diff --git a/RelativeSync/Patches.cs b/RelativeSync/Patches.cs index 71d4ec8..c63400a 100644 --- a/RelativeSync/Patches.cs +++ b/RelativeSync/Patches.cs @@ -1,7 +1,10 @@ using ABI_RC.Core.Base; using ABI_RC.Core.InteractionSystem; +using ABI_RC.Core.InteractionSystem.Base; +using ABI_RC.Core.IO; using ABI_RC.Core.Networking.Jobs; using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; using ABI_RC.Systems.Movement; using ABI.CCK.Components; using HarmonyLib; @@ -19,7 +22,6 @@ internal static class PlayerSetupPatches { __instance.AddComponentIfMissing(); } - } internal static class PuppetMasterPatches @@ -40,6 +42,13 @@ internal static class CVRSeatPatches { __instance.AddComponentIfMissing(); } + + internal static bool canUpdate; + + [HarmonyPrefix] + [HarmonyPatch(typeof(CVRSeat), nameof(CVRSeat.Update))] + private static bool Prefix_CVRSpawnable_Update() + => !BetterBetterCharacterControllerPatches.UseSeatAndPickupsHack || canUpdate; } internal static class CVRMovementParentPatches @@ -110,4 +119,203 @@ internal static class BetterBetterCharacterControllerPatches _initialInterpolation = _rigidbody.interpolation; NoInterpolation = _noInterpolation; // get initial value as patch runs later than settings init } + + internal static bool UseSeatAndPickupsHack; + + [HarmonyPostfix] + [HarmonyPatch(typeof(BetterBetterCharacterController), nameof(BetterBetterCharacterController.OnAfterSimulationUpdate))] + private static void Postfix_BetterBetterCharacterController_OnAfterSimulationUpdate(ref CVRSeat ____lastCvrSeat) + { + if (!UseSeatAndPickupsHack) + return; + + // solve chairs + if (____lastCvrSeat != null) + { + CVRSeatPatches.canUpdate = true; + ____lastCvrSeat.Update(); + CVRSeatPatches.canUpdate = false; + } + + // now solve held pickups (very hacky) + CVRPickupObjectPatches.canUpdate = true; + + Pickupable heldPickup; + if (!MetaPort.Instance.isUsingVr) + { + heldPickup = PlayerSetup.Instance.desktopRay.grabbedObject; + if (heldPickup != null && heldPickup is CVRPickupObject pickupObject) + CVRPickupObjectPatches.FixedFixedUpdate(pickupObject); + } + else + { + heldPickup = PlayerSetup.Instance.vrRayLeft.grabbedObject; + if (heldPickup != null && heldPickup is CVRPickupObject pickupObjectLeft) + CVRPickupObjectPatches.FixedFixedUpdate(pickupObjectLeft); + heldPickup = PlayerSetup.Instance.vrRayRight.grabbedObject; + if (heldPickup != null && heldPickup is CVRPickupObject pickupObjectRight) + CVRPickupObjectPatches.FixedFixedUpdate(pickupObjectRight); + } + + CVRPickupObjectPatches.canUpdate = false; + } +} + +internal static class CVRPickupObjectPatches +{ + internal static bool canUpdate; + internal static float themultiplier = 20f; + + // if (!(pickupObject.transform.position.y >= pickupObject._respawnHeight)) + // pickupObject.ResetLocation(); // reset if below respawn height + + internal static void FixedBasicUpdate(CVRPickupObject pickupObject) + { + if (pickupObject._currentlyFlung && pickupObject._directFlung) + { + pickupObject.isTeleGrabbed = true; + pickupObject.transform.position = + Vector3.SmoothDamp(pickupObject.transform.position, pickupObject._flingTarget, ref pickupObject._flingVelocity, 0.25f); + pickupObject._rigidbody.useGravity = false; + if (Vector3.Distance(pickupObject.transform.position, pickupObject._flingTarget) < 0.1f) + { + pickupObject.ResetFlungStatus(); + SchedulerSystem.RemoveJob(pickupObject.ResetFlungStatus); + } + } + + if (pickupObject.updateWithPhysics + || pickupObject.ControllerRay == null) + return; + + if (pickupObject.gripType == CVRPickupObject.GripType.Free) + pickupObject.transform.rotation = pickupObject.ControllerRay.pivotPoint.rotation; + + else if (pickupObject.gripType == CVRPickupObject.GripType.Origin) + { + if (pickupObject.gripOrigin == null || pickupObject.gripOrigin == pickupObject.transform) + { + pickupObject.transform.rotation = pickupObject.ControllerRay.pivotPoint.rotation; + } + else + { + pickupObject.transform.rotation = + Quaternion.Inverse(pickupObject.gripOrigin.localRotation * Quaternion.Inverse(pickupObject.ControllerRay.pivotPoint.rotation)); + Vector3 b = pickupObject.ControllerRay.pivotPoint.position - pickupObject.gripOrigin.position; + pickupObject.transform.position += b; + } + } + + Vector3 vector = pickupObject.transform.position; + if (pickupObject.gripType == CVRPickupObject.GripType.Free) + { + vector = pickupObject.ControllerRay.pivotPoint.position + pickupObject.ControllerRay.pivotPoint.TransformDirection(pickupObject._initialPositionOffset) - pickupObject.transform.position; + } + if (pickupObject.gripType == CVRPickupObject.GripType.Origin) + { + if (pickupObject.gripOrigin == null) + { + vector = pickupObject.ControllerRay.pivotPoint.position - pickupObject.transform.position; + } + else + { + vector = pickupObject.ControllerRay.pivotPoint.position - pickupObject.gripOrigin.position; + } + } + + var snappingVelocity = pickupObject.GetSnappingVelocity( + out Vector3 snappingPointPosition, + out CVRSnappingPoint snappingPoint, + out SnappingReference snappingReference); + + if (snappingVelocity.HasValue + && snappingPoint != null + && snappingReference != null && + Vector3.Distance( + pickupObject.transform.position + vector + snappingReference.referencePoint.position - + pickupObject.transform.position, snappingPointPosition) < snappingPoint.distance * 1.25) + vector = snappingVelocity.Value; + + pickupObject.transform.position += vector; + pickupObject._resultVelocity = vector / Time.deltaTime; + } + + internal static void FixedFixedUpdate(CVRPickupObject pickupObject) + { + if (!pickupObject.updateWithPhysics || pickupObject.ControllerRay == null) + return; + + // Determine the target position and rotation based on the grip type + Vector3 targetPosition = Vector3.zero; + Quaternion targetRotation = pickupObject.ControllerRay.pivotPoint.rotation; + + if (pickupObject.gripType == CVRPickupObject.GripType.Free) + { + targetPosition = pickupObject.ControllerRay.pivotPoint.position + + pickupObject.ControllerRay.pivotPoint.TransformDirection(pickupObject._initialPositionOffset); + } + else if (pickupObject.gripType == CVRPickupObject.GripType.Origin) + { + targetPosition = pickupObject.gripOrigin != null + ? pickupObject.ControllerRay.pivotPoint.position - pickupObject.gripOrigin.position + pickupObject.transform.position + : pickupObject.ControllerRay.pivotPoint.position; + } + + // Handle snapping if necessary + var snappingVelocity = pickupObject.GetSnappingVelocity( + out Vector3 snappingPointPosition, + out CVRSnappingPoint snappingPoint, + out SnappingReference snappingReference); + + if (snappingVelocity.HasValue && + snappingPoint != null && + snappingReference != null && + Vector3.Distance(targetPosition + snappingReference.referencePoint.position - pickupObject.transform.position, snappingPointPosition) < snappingPoint.distance * 1.25) + { + targetPosition = snappingVelocity.Value + pickupObject.transform.position; + } + + // Smoothly interpolate to the target position and rotation + // pickupObject._rigidbody.position = Vector3.Lerp(pickupObject._rigidbody.position, targetPosition, Time.fixedDeltaTime * 10f); + // pickupObject._rigidbody.rotation = Quaternion.Slerp(pickupObject._rigidbody.rotation, targetRotation, Time.fixedDeltaTime * 10f); + + // Apply small corrective forces to handle collisions more naturally + Vector3 correctiveForce = (targetPosition - pickupObject._rigidbody.position) / Time.fixedDeltaTime; + pickupObject._rigidbody.AddForce(correctiveForce, ForceMode.VelocityChange); + //pickupObject._rigidbody.AddTorque(Vector3.Cross(pickupObject._rigidbody.angularVelocity, correctiveForce) * themultiplier, ForceMode.VelocityChange); + + // Reset angular velocity to ensure precise control over rotation + //pickupObject._rigidbody.angularVelocity = Vector3.zero; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(CVRPickupObject), nameof(CVRPickupObject.FixedUpdate))] + private static bool Prefix_CVRPickupObject_FixedUpdate(ref CVRPickupObject __instance) + { + if (!BetterBetterCharacterControllerPatches.UseSeatAndPickupsHack) + return true; + + if (!(__instance.transform.position.y >= __instance._respawnHeight)) + __instance.ResetLocation(); // reset if below respawn height + + return canUpdate; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(CVRPickupObject), nameof(CVRPickupObject.Update))] + private static bool Prefix_CVRPickupObject_Update(ref CVRPickupObject __instance) + { + if (!BetterBetterCharacterControllerPatches.UseSeatAndPickupsHack) + return true; + + return canUpdate; + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(CVRPickupObject), nameof(CVRPickupObject.Start))] + private static void Postfix_CVRPickupObject_Start(ref CVRPickupObject __instance) + { + __instance.AddComponentIfMissing(); + UnityEngine.Object.Destroy(__instance); + } } \ No newline at end of file