diff --git a/GrabbableSteeringWheel/GrabbableSteeringWheel.csproj b/GrabbableSteeringWheel/GrabbableSteeringWheel.csproj new file mode 100644 index 0000000..9855f81 --- /dev/null +++ b/GrabbableSteeringWheel/GrabbableSteeringWheel.csproj @@ -0,0 +1,17 @@ + + + + net48 + + + + ..\.ManagedLibs\BTKUILib.dll + + + ..\.ManagedLibs\TheClapper.dll + + + + + + diff --git a/GrabbableSteeringWheel/GrabbableSteeringWheel/BoneVertexBoundsUtility.cs b/GrabbableSteeringWheel/GrabbableSteeringWheel/BoneVertexBoundsUtility.cs new file mode 100644 index 0000000..2864f49 --- /dev/null +++ b/GrabbableSteeringWheel/GrabbableSteeringWheel/BoneVertexBoundsUtility.cs @@ -0,0 +1,401 @@ +using System.Collections; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Profiling; +using UnityEngine; +using UnityEngine.Animations; + +public class BoneVertexBoundsUtility : MonoBehaviour +{ + private static readonly ProfilerMarker s_calculateBoundsMarker = new("BoneVertexBounds.Calculate"); + private static readonly ProfilerMarker s_processChildRenderersMarker = new("BoneVertexBounds.ProcessChildRenderers"); + private static readonly ProfilerMarker s_processSkinnedMeshesMarker = new("BoneVertexBounds.ProcessSkinnedMeshes"); + private static readonly ProfilerMarker s_processVerticesMarker = new("BoneVertexBounds.ProcessVertices"); + private static readonly ProfilerMarker s_processConstraintsMarker = new("BoneVertexBounds.ProcessConstraints"); + private static readonly ProfilerMarker s_jobExecutionMarker = new("BoneVertexBounds.JobExecution"); + private static readonly ProfilerMarker s_meshCopyMarker = new("BoneVertexBounds.MeshCopy"); + + private static BoneVertexBoundsUtility instance; + + private static BoneVertexBoundsUtility Instance + { + get + { + if (instance != null) return instance; + GameObject go = new("BoneVertexBoundsUtility"); + instance = go.AddComponent(); + DontDestroyOnLoad(go); + return instance; + } + } + + [Flags] + public enum BoundsCalculationFlags + { + None = 0, + IncludeChildren = 1 << 0, + IncludeSkinnedMesh = 1 << 1, + IncludeConstraints = 1 << 2, + All = IncludeChildren | IncludeSkinnedMesh | IncludeConstraints + } + + public struct BoundsResult + { + public bool IsValid; + public Bounds LocalBounds; + } + + /// + /// Calculates the bounds of a transform based on: + /// - Children Renderers + /// - Skinned Mesh Weights + /// - Constrained Child Renderers & Skinned Mesh Weights + /// + public static void CalculateBoneWeightedBounds(Transform bone, float weightThreshold, BoundsCalculationFlags flags, Action onComplete) + => Instance.StartCoroutine(Instance.CalculateBoundsCoroutine(bone, weightThreshold, flags, onComplete)); + + private IEnumerator CalculateBoundsCoroutine(Transform bone, float weightThreshold, BoundsCalculationFlags flags, Action onComplete) + { + using (s_calculateBoundsMarker.Auto()) + { + BoundsResult result = new(); + var allWeightedPoints = new List(); + bool hasValidPoints = false; + + // Child renderers + IEnumerator ProcessChildRenderersLocal(Transform targetBone, Action> onChildPoints) + { + using (s_processChildRenderersMarker.Auto()) + { + var points = new List(); + var childRenderers = targetBone.GetComponentsInChildren() + .Where(r => r is not SkinnedMeshRenderer) + .ToArray(); + + foreach (Renderer childRend in childRenderers) + { + Bounds bounds = childRend.localBounds; + var corners = new Vector3[8]; + Vector3 ext = bounds.extents; + Vector3 center = bounds.center; + + corners[0] = new Vector3(center.x - ext.x, center.y - ext.y, center.z - ext.z); + corners[1] = new Vector3(center.x + ext.x, center.y - ext.y, center.z - ext.z); + corners[2] = new Vector3(center.x - ext.x, center.y + ext.y, center.z - ext.z); + corners[3] = new Vector3(center.x + ext.x, center.y + ext.y, center.z - ext.z); + corners[4] = new Vector3(center.x - ext.x, center.y - ext.y, center.z + ext.z); + corners[5] = new Vector3(center.x + ext.x, center.y - ext.y, center.z + ext.z); + corners[6] = new Vector3(center.x - ext.x, center.y + ext.y, center.z + ext.z); + corners[7] = new Vector3(center.x + ext.x, center.y + ext.y, center.z + ext.z); + + for (int i = 0; i < 8; i++) + points.Add(targetBone.InverseTransformPoint(childRend.transform.TransformPoint(corners[i]))); + } + + onChildPoints?.Invoke(points); + } + yield break; + } + + // Skinned mesh renderers + IEnumerator ProcessSkinnedMeshRenderersLocal(Transform targetBone, float threshold, Action> onSkinnedPoints) + { + using (s_processSkinnedMeshesMarker.Auto()) + { + var points = new List(); + var siblingAndParentSkinnedMesh = targetBone.root.GetComponentsInChildren(); + var relevantMeshes = siblingAndParentSkinnedMesh.Where(smr => DoesMeshUseBone(smr, targetBone)).ToArray(); + + foreach (SkinnedMeshRenderer smr in relevantMeshes) + { + yield return StartCoroutine(ProcessSkinnedMesh(smr, targetBone, threshold, meshPoints => + { + if (meshPoints is { Length: > 0 }) + points.AddRange(meshPoints); + })); + } + + onSkinnedPoints?.Invoke(points); + } + } + + // Constraints + IEnumerator ProcessConstraintsLocal(Transform targetBone, float threshold, BoundsCalculationFlags constraintFlags, Action> onConstraintPoints) + { + using (s_processConstraintsMarker.Auto()) + { + var points = new List(); + var processedTransforms = new HashSet(); + var constrainedTransforms = new List(); + + // Find all constrained objects that reference our bone + var constraints = targetBone.root.GetComponentsInChildren(); + + foreach (IConstraint constraint in constraints) + { + for (int i = 0; i < constraint.sourceCount; i++) + { + if (constraint.GetSource(i).sourceTransform != targetBone) continue; + constrainedTransforms.Add(((Behaviour)constraint).transform); + break; + } + } + + // Process each constrained transform + foreach (Transform constrainedTransform in constrainedTransforms) + { + if (!processedTransforms.Add(constrainedTransform)) + continue; + + var localPoints = new List(); + bool hasLocalPoints = false; + + // Process child renderers if enabled + if ((constraintFlags & BoundsCalculationFlags.IncludeChildren) != 0) + { + yield return StartCoroutine(ProcessChildRenderersLocal(constrainedTransform, childPoints => + { + if (childPoints is not { Count: > 0 }) return; + localPoints.AddRange(childPoints); + hasLocalPoints = true; + })); + } + + // Process skinned mesh if enabled + if ((constraintFlags & BoundsCalculationFlags.IncludeSkinnedMesh) != 0) + { + yield return StartCoroutine(ProcessSkinnedMeshRenderersLocal(constrainedTransform, threshold, skinnedPoints => + { + if (skinnedPoints is not { Count: > 0 }) return; + localPoints.AddRange(skinnedPoints); + hasLocalPoints = true; + })); + } + + if (!hasLocalPoints) + continue; + + // Convert all points to bone space + foreach (Vector3 point in localPoints) + points.Add(targetBone.InverseTransformPoint(constrainedTransform.TransformPoint(point))); + } + + onConstraintPoints?.Invoke(points); + } + } + + // Process child renderers + if ((flags & BoundsCalculationFlags.IncludeChildren) != 0) + { + yield return StartCoroutine(ProcessChildRenderersLocal(bone, childPoints => + { + if (childPoints is not { Count: > 0 }) return; + allWeightedPoints.AddRange(childPoints); + hasValidPoints = true; + })); + } + + // Process skinned mesh renderers + if ((flags & BoundsCalculationFlags.IncludeSkinnedMesh) != 0) + { + yield return StartCoroutine(ProcessSkinnedMeshRenderersLocal(bone, weightThreshold, skinnedPoints => + { + if (skinnedPoints == null || skinnedPoints.Count <= 0) return; + allWeightedPoints.AddRange(skinnedPoints); + hasValidPoints = true; + })); + } + + // Process constraints + if ((flags & BoundsCalculationFlags.IncludeConstraints) != 0) + { + // Use only Children and SkinnedMesh flags for constraint processing to prevent recursion (maybe make optional)? + BoundsCalculationFlags constraintFlags = flags & ~BoundsCalculationFlags.IncludeConstraints; + yield return StartCoroutine(ProcessConstraintsLocal(bone, weightThreshold, constraintFlags, constraintPoints => + { + if (constraintPoints is not { Count: > 0 }) return; + allWeightedPoints.AddRange(constraintPoints); + hasValidPoints = true; + })); + } + + if (!hasValidPoints) + { + result.IsValid = false; + onComplete?.Invoke(result); + yield break; + } + + // Calculate final bounds in bone space + Bounds bounds = new(allWeightedPoints[0], Vector3.zero); + foreach (Vector3 point in allWeightedPoints) + bounds.Encapsulate(point); + + // Ensure minimum size + Vector3 size = bounds.size; + size = Vector3.Max(size, Vector3.one * 0.01f); + bounds.size = size; + + result.IsValid = true; + result.LocalBounds = bounds; + + onComplete?.Invoke(result); + } + } + + private static bool DoesMeshUseBone(SkinnedMeshRenderer smr, Transform bone) + => smr.bones != null && smr.bones.Contains(bone); + + private IEnumerator ProcessSkinnedMesh(SkinnedMeshRenderer smr, Transform bone, float weightThreshold, + Action onComplete) + { + Mesh mesh = smr.sharedMesh; + if (mesh == null) + { + onComplete?.Invoke(null); + yield break; + } + + // Find bone index + int boneIndex = Array.IndexOf(smr.bones, bone); + if (boneIndex == -1) + { + onComplete?.Invoke(null); + yield break; + } + + Mesh meshToUse = mesh; + GameObject tempGO = null; + + try + { + // Handle non-readable meshes (ReadItAnyway lmao) + if (!mesh.isReadable) + { + using (s_meshCopyMarker.Auto()) + { + tempGO = new GameObject("TempMeshReader"); + SkinnedMeshRenderer tempSMR = tempGO.AddComponent(); + tempSMR.sharedMesh = mesh; + meshToUse = new Mesh(); + tempSMR.BakeMesh(meshToUse); + } + } + + Mesh.MeshDataArray meshDataArray = Mesh.AcquireReadOnlyMeshData(meshToUse); + Mesh.MeshData meshData = meshDataArray[0]; + + var vertexCount = meshData.vertexCount; + var vertices = new NativeArray(vertexCount, Allocator.TempJob); + var weights = new NativeArray(vertexCount, Allocator.TempJob); + var results = new NativeArray(vertexCount, Allocator.TempJob); + + meshData.GetVertices(vertices); + weights.CopyFrom(mesh.boneWeights); + + // Debug.Log(vertices.Length); + // Debug.Log(weights.Length); + + using (s_processVerticesMarker.Auto()) + { + try + { + Transform rootBone = smr.rootBone ? smr.rootBone.transform : smr.transform; + Matrix4x4 meshToWorld = Matrix4x4.TRS(smr.transform.position, smr.transform.rotation, rootBone.lossyScale); + + // Fixes setup where mesh was in diff hierarchy & 0.001 scale, bone & root bone outside & above + meshToWorld *= Matrix4x4.TRS(Vector3.zero, Quaternion.identity, smr.transform.localScale); + + ProcessVerticesJob processJob = new() + { + Vertices = vertices, + BoneWeights = weights, + Results = results, + BoneIndex = boneIndex, + WeightThreshold = weightThreshold, + MeshToWorld = meshToWorld, + WorldToBone = bone.worldToLocalMatrix + }; + + using (s_jobExecutionMarker.Auto()) + { + int batchCount = Mathf.Max(1, vertexCount / 64); + JobHandle jobHandle = processJob.Schedule(vertexCount, batchCount); + while (!jobHandle.IsCompleted) + yield return null; + + jobHandle.Complete(); + } + + // Collect valid points + var validPoints = new List(); + for (int i = 0; i < results.Length; i++) + if (results[i].IsValid) validPoints.Add(results[i].Position); + + onComplete?.Invoke(validPoints.ToArray()); + } + finally + { + vertices.Dispose(); + weights.Dispose(); + results.Dispose(); + meshDataArray.Dispose(); + } + } + } + finally + { + // Destroy duplicated baked mesh if we created one to read mesh data + if (!mesh.isReadable && meshToUse != mesh) Destroy(meshToUse); + if (tempGO != null) Destroy(tempGO); + } + } + + private struct VertexResult + { + public Vector3 Position; + public bool IsValid; + } + + [BurstCompile] + private struct ProcessVerticesJob : IJobParallelFor + { + [ReadOnly] public NativeArray Vertices; + [ReadOnly] public NativeArray BoneWeights; + [NativeDisableParallelForRestriction] + public NativeArray Results; + [ReadOnly] public int BoneIndex; + [ReadOnly] public float WeightThreshold; + [ReadOnly] public Matrix4x4 MeshToWorld; + [ReadOnly] public Matrix4x4 WorldToBone; + + public void Execute(int i) + { + BoneWeight weight = BoneWeights[i]; + float totalWeight = 0f; + + if (weight.boneIndex0 == BoneIndex) totalWeight += weight.weight0; + if (weight.boneIndex1 == BoneIndex) totalWeight += weight.weight1; + if (weight.boneIndex2 == BoneIndex) totalWeight += weight.weight2; + if (weight.boneIndex3 == BoneIndex) totalWeight += weight.weight3; + + if (totalWeight >= WeightThreshold) + { + // Transform vertex to bone space + Vector3 worldPos = MeshToWorld.MultiplyPoint3x4(Vertices[i]); + Vector3 boneLocalPos = WorldToBone.MultiplyPoint3x4(worldPos); + + Results[i] = new VertexResult + { + Position = boneLocalPos, + IsValid = true + }; + } + else + { + Results[i] = new VertexResult { IsValid = false }; + } + } + } +} \ No newline at end of file diff --git a/GrabbableSteeringWheel/GrabbableSteeringWheel/SteeringWheelPickup.cs b/GrabbableSteeringWheel/GrabbableSteeringWheel/SteeringWheelPickup.cs new file mode 100644 index 0000000..0807a2a --- /dev/null +++ b/GrabbableSteeringWheel/GrabbableSteeringWheel/SteeringWheelPickup.cs @@ -0,0 +1,203 @@ +using ABI_RC.Core.InteractionSystem; +using ABI_RC.Core.InteractionSystem.Base; +using UnityEngine; + +// TODO: +// Fix multi-grab (limitation of Pickupable) +// Think this can be fixed by forcing ungrab & monitoring ourselves for release +// Add configurable override for steering range +// Fix steering wheel resetting immediatly on release +// Fix input patch not being multiplicative (so joysticks can still work) +// Prevent pickup in Desktop + +public class SteeringWheelPickup : Pickupable +{ + private RCC_CarControllerV3 _carController; + + private static readonly Dictionary ActiveWheels = new(); + + public static float GetSteerInput(RCC_CarControllerV3 carController) + { + if (ActiveWheels.TryGetValue(carController, out SteeringWheelPickup wheel) && wheel.IsPickedUp) + return wheel.GetNormalizedValue(); + return 0f; + } + + public void SetupSteeringWheel(RCC_CarControllerV3 carController) + { + _carController = carController; + if (!ActiveWheels.ContainsKey(carController)) + { + ActiveWheels[carController] = this; + carController.useCounterSteering = false; + carController.useSteeringSmoother = false; + } + } + + private void OnDestroy() + { + if (_carController != null && ActiveWheels.ContainsKey(_carController)) + ActiveWheels.Remove(_carController); + } + + #region Configuration Properties Override + + public override bool DisallowTheft => true; + public override float MaxGrabDistance => 0.8f; + public override float MaxPushDistance => 0f; + public override bool IsAutoHold => false; + public override bool IsObjectRotationAllowed => false; + public override bool IsObjectPushPullAllowed => false; + public override bool IsObjectUseAllowed => false; + + public override bool CanPickup => IsPickupable && _carController?.SteeringWheel != null; + + #endregion Configuration Properties Override + + #region RCC Stuff + + private float GetMaxSteeringRange() + => _carController.steerAngle * Mathf.Abs(_carController.steeringWheelAngleMultiplier); + + private float GetSteeringWheelSign() + => Mathf.Sign(_carController.steeringWheelAngleMultiplier) * -1f; // Idk + + private Vector3 GetSteeringWheelLocalAxis() + { + return _carController.steeringWheelRotateAround switch + { + RCC_CarControllerV3.SteeringWheelRotateAround.XAxis => Vector3.right, + RCC_CarControllerV3.SteeringWheelRotateAround.YAxis => Vector3.up, + RCC_CarControllerV3.SteeringWheelRotateAround.ZAxis => Vector3.forward, + _ => Vector3.forward + }; + } + + #endregion RCC Stuff + + #region Rotation Tracking + + private readonly List _trackedTransforms = new(); + private readonly List _lastPositions = new(); + private readonly List _totalAngles = new(); + private bool _isTracking; + private float _averageAngle; + + private void StartTrackingTransform(Transform trans) + { + if (trans == null) return; + + _trackedTransforms.Add(trans); + _lastPositions.Add(GetLocalPositionWithoutRotation(transform.position)); + _totalAngles.Add(0f); + _isTracking = true; + } + + private void StopTrackingTransform(Transform trans) + { + int index = _trackedTransforms.IndexOf(trans); + if (index != -1) + { + _trackedTransforms.RemoveAt(index); + _lastPositions.RemoveAt(index); + _totalAngles.RemoveAt(index); + } + + _isTracking = _trackedTransforms.Count > 0; + } + + private void UpdateRotationTracking() + { + if (!_isTracking || _trackedTransforms.Count == 0) return; + + Vector3 trackingAxis = GetSteeringWheelLocalAxis(); + + for (int i = 0; i < _trackedTransforms.Count; i++) + { + if (_trackedTransforms[i] == null) continue; + + Vector3 currentPosition = GetLocalPositionWithoutRotation(_trackedTransforms[i].position); + if (currentPosition == _lastPositions[i]) continue; + + Vector3 previousVector = _lastPositions[i]; + Vector3 currentVector = currentPosition; + + previousVector = Vector3.ProjectOnPlane(previousVector, trackingAxis).normalized; + currentVector = Vector3.ProjectOnPlane(currentVector, trackingAxis).normalized; + + if (previousVector.sqrMagnitude > 0.001f && currentVector.sqrMagnitude > 0.001f) + { + float deltaAngle = Vector3.SignedAngle(previousVector, currentVector, trackingAxis); + if (Mathf.Abs(deltaAngle) < 90f) _totalAngles[i] += deltaAngle; // Prevent big tracking jumps + } + + _lastPositions[i] = currentPosition; + } + + // Calculate average every frame using only valid transforms + float sumAngles = 0f; + int validTransforms = 0; + + for (int i = 0; i < _trackedTransforms.Count; i++) + { + if (_trackedTransforms[i] == null) continue; + sumAngles += _totalAngles[i]; + validTransforms++; + } + + if (validTransforms > 0) + _averageAngle = sumAngles / validTransforms; + } + + private float GetNormalizedValue() + { + float maxRange = GetMaxSteeringRange(); + // return Mathf.Clamp(_averageAngle / (maxRange * 0.5f), -1f, 1f); + return Mathf.Clamp(_averageAngle / (maxRange), -1f, 1f) * GetSteeringWheelSign(); + } + + private Vector3 GetLocalPositionWithoutRotation(Vector3 worldPosition) + { + Transform steeringTransform = _carController.SteeringWheel; + + Quaternion localRotation = steeringTransform.localRotation; + steeringTransform.localRotation = _carController.orgSteeringWheelRot; + + Vector3 localPosition = steeringTransform.InverseTransformPoint(worldPosition); + steeringTransform.localRotation = localRotation; + + return localPosition; + } + + #endregion Rotation Tracking + + public override void OnGrab(InteractionContext context, Vector3 grabPoint) + { + if (ControllerRay?.pivotPoint == null) + return; + + StartTrackingTransform(ControllerRay.transform); + } + + public override void OnDrop(InteractionContext context) + { + if (ControllerRay?.transform != null) + StopTrackingTransform(ControllerRay.transform); + } + + private void Update() + { + if (!IsPickedUp || ControllerRay?.pivotPoint == null || _carController == null) + return; + + UpdateRotationTracking(); + } + + #region Unused Abstract Method Implementations + + public override void OnUseDown(InteractionContext context) { } + public override void OnUseUp(InteractionContext context) { } + public override void OnFlingTowardsTarget(Vector3 target) { } + + #endregion Unused Abstract Method Implementations +} \ No newline at end of file diff --git a/GrabbableSteeringWheel/Main.cs b/GrabbableSteeringWheel/Main.cs new file mode 100644 index 0000000..1ced7e0 --- /dev/null +++ b/GrabbableSteeringWheel/Main.cs @@ -0,0 +1,61 @@ +using MelonLoader; +using NAK.GrabbableSteeringWheel.Patches; + +namespace NAK.GrabbableSteeringWheel; + +public class GrabbableSteeringWheelMod : MelonMod +{ + internal static MelonLogger.Instance Logger; + + #region Melon Preferences + + // private static readonly MelonPreferences_Category Category = + // MelonPreferences.CreateCategory(nameof(GrabbableSteeringWheelMod)); + // + // private static readonly MelonPreferences_Entry EntryEnabled = + // Category.CreateEntry( + // "use_legacy_mitigation", + // true, + // "Enabled", + // description: "Enable legacy content camera hack when in Legacy worlds."); + + #endregion Melon Preferences + + #region Melon Events + + public override void OnInitializeMelon() + { + Logger = LoggerInstance; + + ApplyPatches(typeof(RCCCarControllerV3_Patches)); + ApplyPatches(typeof(CVRInputManager_Patches)); + } + + #endregion Melon Events + + #region Melon Mod Utilities + + private static void InitializeIntegration(string modName, Action integrationAction) + { + if (RegisteredMelons.All(it => it.Info.Name != modName)) + return; + + Logger.Msg($"Initializing {modName} integration."); + integrationAction.Invoke(); + } + + private void ApplyPatches(Type type) + { + try + { + HarmonyInstance.PatchAll(type); + } + catch (Exception e) + { + LoggerInstance.Msg($"Failed while patching {type.Name}!"); + LoggerInstance.Error(e); + } + } + + #endregion Melon Mod Utilities +} \ No newline at end of file diff --git a/GrabbableSteeringWheel/ModSettings.cs b/GrabbableSteeringWheel/ModSettings.cs new file mode 100644 index 0000000..26a56be --- /dev/null +++ b/GrabbableSteeringWheel/ModSettings.cs @@ -0,0 +1,24 @@ +using MelonLoader; + +namespace NAK.GrabbableSteeringWheel; + +internal static class ModSettings +{ + #region Constants + + internal const string ModName = nameof(GrabbableSteeringWheel); + // internal const string LCM_SettingsCategory = "Legacy Content Mitigation"; + + #endregion Constants + + #region Melon Preferences + + private static readonly MelonPreferences_Category Category = + MelonPreferences.CreateCategory(ModName); + + // internal static readonly MelonPreferences_Entry EntryAutoForLegacyWorlds = + // Category.CreateEntry("auto_for_legacy_worlds", true, + // "Auto For Legacy Worlds", description: "Should Legacy View be auto enabled for detected Legacy worlds?"); + // + #endregion Melon Preferences +} \ No newline at end of file diff --git a/GrabbableSteeringWheel/Patches.cs b/GrabbableSteeringWheel/Patches.cs new file mode 100644 index 0000000..c6a828c --- /dev/null +++ b/GrabbableSteeringWheel/Patches.cs @@ -0,0 +1,49 @@ +using ABI_RC.Systems.InputManagement; +using ABI_RC.Systems.Movement; +using HarmonyLib; +using UnityEngine; + +namespace NAK.GrabbableSteeringWheel.Patches; + +internal static class RCCCarControllerV3_Patches +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(RCC_CarControllerV3), nameof(RCC_CarControllerV3.Awake))] + private static void Postfix_RCC_CarControllerV3_Awake(ref RCC_CarControllerV3 __instance) + { + Transform steeringWheelTransform = __instance.SteeringWheel; + if (steeringWheelTransform == null) + return; + + RCC_CarControllerV3 v3 = __instance; + BoneVertexBoundsUtility.CalculateBoneWeightedBounds( + steeringWheelTransform, + 0.8f, + BoneVertexBoundsUtility.BoundsCalculationFlags.All, + result => + { + if (!result.IsValid) + return; + + BoxCollider boxCollider = steeringWheelTransform.gameObject.AddComponent(); + boxCollider.center = result.LocalBounds.center; + boxCollider.size = result.LocalBounds.size; + + SteeringWheelPickup steeringWheel = steeringWheelTransform.gameObject.AddComponent(); + steeringWheel.SetupSteeringWheel(v3); + }); + } +} + +internal static class CVRInputManager_Patches +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(CVRInputManager), nameof(CVRInputManager.UpdateInput))] + private static void Postfix_CVRInputManager_UpdateInput(ref CVRInputManager __instance) + { + // Steering input is clamped in RCC component + if (BetterBetterCharacterController.Instance.IsSittingOnControlSeat()) + __instance.steering += SteeringWheelPickup.GetSteerInput( + BetterBetterCharacterController.Instance._lastCvrSeat._carController); + } +} \ No newline at end of file diff --git a/GrabbableSteeringWheel/Properties/AssemblyInfo.cs b/GrabbableSteeringWheel/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6441f79 --- /dev/null +++ b/GrabbableSteeringWheel/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using MelonLoader; +using NAK.GrabbableSteeringWheel; +using NAK.GrabbableSteeringWheel.Properties; + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.GrabbableSteeringWheel))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.GrabbableSteeringWheel))] + +[assembly: MelonInfo( + typeof(GrabbableSteeringWheelMod), + nameof(NAK.GrabbableSteeringWheel), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/GrabbableSteeringWheel" +)] + +[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] +[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] +[assembly: MelonColor(255, 246, 25, 99)] // red-pink +[assembly: MelonAuthorColor(255, 158, 21, 32)] // red +[assembly: HarmonyDontPatchAll] + +namespace NAK.GrabbableSteeringWheel.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/GrabbableSteeringWheel/README.md b/GrabbableSteeringWheel/README.md new file mode 100644 index 0000000..08942df --- /dev/null +++ b/GrabbableSteeringWheel/README.md @@ -0,0 +1,14 @@ +# GrabbableSteeringWheel + +Experiment with making rigged RCC vehicle steering wheels work for VR steering input. +~~~~ +--- + +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/GrabbableSteeringWheel/format.json b/GrabbableSteeringWheel/format.json new file mode 100644 index 0000000..480d0fb --- /dev/null +++ b/GrabbableSteeringWheel/format.json @@ -0,0 +1,23 @@ +{ + "_id": -1, + "name": "LegacyContentMitigation", + "modversion": "1.0.1", + "gameversion": "2024r177", + "loaderversion": "0.6.1", + "modtype": "Mod", + "author": "Exterrata, NotAKidoS", + "description": "Experimental mod that manually renders both VR eyes separately while in Legacy Worlds.\n\nThe mod will automatically kick-in when loading NonSpi Worlds & prevent Shader Replacement for all content while active. You can also manually toggle it within the BTKUI Misc tab for experimenting.\n\n-# There is an obvious performance penalty with rendering the world 2x & toggling the head hiding per eye, so enabling the mod outside of Legacy Worlds is not recommended.", + "searchtags": [ + "legacy", + "content", + "spi", + "shaders" + ], + "requirements": [ + "BTKUILib" + ], + "downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r44/LegacyContentMitigation.dll", + "sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/LegacyContentMitigation/", + "changelog": "- Initial release", + "embedcolor": "#f61963" +} \ No newline at end of file