mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-02 06:19:22 +00:00
GrabbableSteeringWheel: commit
This commit is contained in:
parent
39da88d3d3
commit
3a00ff104a
9 changed files with 825 additions and 0 deletions
17
GrabbableSteeringWheel/GrabbableSteeringWheel.csproj
Normal file
17
GrabbableSteeringWheel/GrabbableSteeringWheel.csproj
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net48</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="BTKUILib">
|
||||||
|
<HintPath>..\.ManagedLibs\BTKUILib.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="TheClapper">
|
||||||
|
<HintPath>..\.ManagedLibs\TheClapper.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Utility\" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -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<BoneVertexBoundsUtility>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the bounds of a transform based on:
|
||||||
|
/// - Children Renderers
|
||||||
|
/// - Skinned Mesh Weights
|
||||||
|
/// - Constrained Child Renderers & Skinned Mesh Weights
|
||||||
|
/// </summary>
|
||||||
|
public static void CalculateBoneWeightedBounds(Transform bone, float weightThreshold, BoundsCalculationFlags flags, Action<BoundsResult> onComplete)
|
||||||
|
=> Instance.StartCoroutine(Instance.CalculateBoundsCoroutine(bone, weightThreshold, flags, onComplete));
|
||||||
|
|
||||||
|
private IEnumerator CalculateBoundsCoroutine(Transform bone, float weightThreshold, BoundsCalculationFlags flags, Action<BoundsResult> onComplete)
|
||||||
|
{
|
||||||
|
using (s_calculateBoundsMarker.Auto())
|
||||||
|
{
|
||||||
|
BoundsResult result = new();
|
||||||
|
var allWeightedPoints = new List<Vector3>();
|
||||||
|
bool hasValidPoints = false;
|
||||||
|
|
||||||
|
// Child renderers
|
||||||
|
IEnumerator ProcessChildRenderersLocal(Transform targetBone, Action<List<Vector3>> onChildPoints)
|
||||||
|
{
|
||||||
|
using (s_processChildRenderersMarker.Auto())
|
||||||
|
{
|
||||||
|
var points = new List<Vector3>();
|
||||||
|
var childRenderers = targetBone.GetComponentsInChildren<Renderer>()
|
||||||
|
.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<List<Vector3>> onSkinnedPoints)
|
||||||
|
{
|
||||||
|
using (s_processSkinnedMeshesMarker.Auto())
|
||||||
|
{
|
||||||
|
var points = new List<Vector3>();
|
||||||
|
var siblingAndParentSkinnedMesh = targetBone.root.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||||
|
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<List<Vector3>> onConstraintPoints)
|
||||||
|
{
|
||||||
|
using (s_processConstraintsMarker.Auto())
|
||||||
|
{
|
||||||
|
var points = new List<Vector3>();
|
||||||
|
var processedTransforms = new HashSet<Transform>();
|
||||||
|
var constrainedTransforms = new List<Transform>();
|
||||||
|
|
||||||
|
// Find all constrained objects that reference our bone
|
||||||
|
var constraints = targetBone.root.GetComponentsInChildren<IConstraint>();
|
||||||
|
|
||||||
|
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<Vector3>();
|
||||||
|
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<Vector3[]> 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<SkinnedMeshRenderer>();
|
||||||
|
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<Vector3>(vertexCount, Allocator.TempJob);
|
||||||
|
var weights = new NativeArray<BoneWeight>(vertexCount, Allocator.TempJob);
|
||||||
|
var results = new NativeArray<VertexResult>(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<Vector3>();
|
||||||
|
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<Vector3> Vertices;
|
||||||
|
[ReadOnly] public NativeArray<BoneWeight> BoneWeights;
|
||||||
|
[NativeDisableParallelForRestriction]
|
||||||
|
public NativeArray<VertexResult> 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<RCC_CarControllerV3, SteeringWheelPickup> 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<Transform> _trackedTransforms = new();
|
||||||
|
private readonly List<Vector3> _lastPositions = new();
|
||||||
|
private readonly List<float> _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
|
||||||
|
}
|
61
GrabbableSteeringWheel/Main.cs
Normal file
61
GrabbableSteeringWheel/Main.cs
Normal file
|
@ -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<bool> 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
|
||||||
|
}
|
24
GrabbableSteeringWheel/ModSettings.cs
Normal file
24
GrabbableSteeringWheel/ModSettings.cs
Normal file
|
@ -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<bool> 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
|
||||||
|
}
|
49
GrabbableSteeringWheel/Patches.cs
Normal file
49
GrabbableSteeringWheel/Patches.cs
Normal file
|
@ -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>();
|
||||||
|
boxCollider.center = result.LocalBounds.center;
|
||||||
|
boxCollider.size = result.LocalBounds.size;
|
||||||
|
|
||||||
|
SteeringWheelPickup steeringWheel = steeringWheelTransform.gameObject.AddComponent<SteeringWheelPickup>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
33
GrabbableSteeringWheel/Properties/AssemblyInfo.cs
Normal file
33
GrabbableSteeringWheel/Properties/AssemblyInfo.cs
Normal file
|
@ -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";
|
||||||
|
}
|
14
GrabbableSteeringWheel/README.md
Normal file
14
GrabbableSteeringWheel/README.md
Normal file
|
@ -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.
|
23
GrabbableSteeringWheel/format.json
Normal file
23
GrabbableSteeringWheel/format.json
Normal file
|
@ -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"
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue