mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-02 14:29:25 +00:00
[PhysicsGunMod] PhysGun test
This commit is contained in:
parent
7e1445912d
commit
d155ea546e
10 changed files with 862 additions and 0 deletions
|
@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatBoxExtensions", "ChatBo
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EzCurls", "EzCurls\EzCurls.csproj", "{9D39E900-8F38-485B-8B67-9F75D8FF2C5E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EzCurls", "EzCurls\EzCurls.csproj", "{9D39E900-8F38-485B-8B67-9F75D8FF2C5E}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysicsGunMod", "PhysicsGunMod\PhysicsGunMod.csproj", "{F94DDB73-9041-4F5C-AD43-6960701E8417}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -87,6 +89,10 @@ Global
|
||||||
{9D39E900-8F38-485B-8B67-9F75D8FF2C5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9D39E900-8F38-485B-8B67-9F75D8FF2C5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9D39E900-8F38-485B-8B67-9F75D8FF2C5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9D39E900-8F38-485B-8B67-9F75D8FF2C5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9D39E900-8F38-485B-8B67-9F75D8FF2C5E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9D39E900-8F38-485B-8B67-9F75D8FF2C5E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F94DDB73-9041-4F5C-AD43-6960701E8417}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F94DDB73-9041-4F5C-AD43-6960701E8417}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F94DDB73-9041-4F5C-AD43-6960701E8417}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F94DDB73-9041-4F5C-AD43-6960701E8417}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
91
PhysicsGunMod/Components/ObjectSyncBridge.cs
Normal file
91
PhysicsGunMod/Components/ObjectSyncBridge.cs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
using ABI_RC.Core.Savior;
|
||||||
|
using ABI_RC.Core.Util;
|
||||||
|
using ABI.CCK.Components;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NAK.PhysicsGunMod.Components;
|
||||||
|
|
||||||
|
public class ObjectSyncBridge : MonoBehaviour
|
||||||
|
{
|
||||||
|
private PhysicsGunInteractionBehavior _physicsGun;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
// find physics gun
|
||||||
|
if (!TryGetComponent(out _physicsGun))
|
||||||
|
{
|
||||||
|
PhysicsGunMod.Logger.Msg("Failed to find physics gun!");
|
||||||
|
Destroy(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen for events
|
||||||
|
_physicsGun.OnPreGrabbedObject = o =>
|
||||||
|
{
|
||||||
|
bool canTakeOwnership = false;
|
||||||
|
|
||||||
|
CVRObjectSync objectSync = o.GetComponentInParent<CVRObjectSync>();
|
||||||
|
if (objectSync != null
|
||||||
|
&& (objectSync.SyncType == 0 // check if physics synced or synced by us
|
||||||
|
|| objectSync.SyncedByMe))
|
||||||
|
canTakeOwnership = true;
|
||||||
|
|
||||||
|
CVRSpawnable spawnable = o.GetComponentInParent<CVRSpawnable>();
|
||||||
|
if (spawnable != null)
|
||||||
|
{
|
||||||
|
CVRSyncHelper.PropData propData = CVRSyncHelper.Props.Find(match => match.InstanceId == spawnable.instanceId);
|
||||||
|
if (propData != null
|
||||||
|
&& (propData.syncType == 0 // check if physics synced or synced by us
|
||||||
|
|| propData.syncedBy == MetaPort.Instance.ownerId))
|
||||||
|
canTakeOwnership = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CVRPickupObject pickup = o.GetComponentInParent<CVRPickupObject>();
|
||||||
|
if (pickup != null
|
||||||
|
&& (pickup.grabbedBy == MetaPort.Instance.ownerId // check if already grabbed by us
|
||||||
|
|| pickup.grabbedBy == "" || !pickup.disallowTheft)) // check if not grabbed or allows theft
|
||||||
|
canTakeOwnership = true;
|
||||||
|
|
||||||
|
if (!canTakeOwnership // if we can't take ownership, don't grab, unless there is no syncing at all (local object)
|
||||||
|
&& (objectSync || spawnable || pickup ))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (pickup)
|
||||||
|
{
|
||||||
|
pickup.grabbedBy = MetaPort.Instance.ownerId;
|
||||||
|
pickup._grabStartTime = Time.time;
|
||||||
|
}
|
||||||
|
if (spawnable) spawnable.isPhysicsSynced = true;
|
||||||
|
if (objectSync) objectSync.isPhysicsSynced = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
_physicsGun.OnObjectReleased = o =>
|
||||||
|
{
|
||||||
|
// CVRObjectSync objectSync = o.GetComponentInParent<CVRObjectSync>();
|
||||||
|
// if (objectSync != null && objectSync.SyncType == 0)
|
||||||
|
// objectSync.isPhysicsSynced = false;
|
||||||
|
//
|
||||||
|
// CVRSpawnable spawnable = o.GetComponentInParent<CVRSpawnable>();
|
||||||
|
// if (spawnable != null)
|
||||||
|
// {
|
||||||
|
// CVRSyncHelper.PropData propData = CVRSyncHelper.Props.Find(match => match.InstanceId == spawnable.instanceId);
|
||||||
|
// if (propData != null && (propData.syncType == 0 || propData.syncedBy == MetaPort.Instance.ownerId))
|
||||||
|
// spawnable.isPhysicsSynced = false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// CVRPickupObject pickup = o.GetComponentInParent<CVRPickupObject>();
|
||||||
|
// if (pickup != null && pickup.grabbedBy == MetaPort.Instance.ownerId)
|
||||||
|
// pickup.grabbedBy = "";
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
// stop listening for events
|
||||||
|
_physicsGun.OnObjectGrabbed = null;
|
||||||
|
}
|
||||||
|
}
|
570
PhysicsGunMod/Components/PhysicsGunInteractionBehavior.cs
Normal file
570
PhysicsGunMod/Components/PhysicsGunInteractionBehavior.cs
Normal file
|
@ -0,0 +1,570 @@
|
||||||
|
using ABI_RC.Core.Player;
|
||||||
|
using ABI_RC.Systems.InputManagement;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
using UnityEngine.Scripting.APIUpdating;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Physics Gun script from repository - https://github.com/Laumania/Unity3d-PhysicsGun
|
||||||
|
* Created by:
|
||||||
|
* Mads Laumann, https://github.com/laumania
|
||||||
|
* WarmedxMints, https://github.com/WarmedxMints
|
||||||
|
*
|
||||||
|
* Original/initial script "Gravity Gun": https://pastebin.com/w1G8m3dH
|
||||||
|
* Original author: Jake Perry, reddit.com/user/nandos13
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace NAK.PhysicsGunMod.Components;
|
||||||
|
|
||||||
|
public class PhysicsGunInteractionBehavior : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static PhysicsGunInteractionBehavior Instance;
|
||||||
|
|
||||||
|
[Header("LayerMask")] [Tooltip("The layer which the gun can grab objects from")] [SerializeField]
|
||||||
|
private LayerMask _grabLayer;
|
||||||
|
|
||||||
|
[SerializeField] private Camera _camera;
|
||||||
|
|
||||||
|
[Header("Input Setting")] [Space(10)] public KeyCode Rotate = KeyCode.R;
|
||||||
|
public KeyCode SnapRotation = KeyCode.LeftShift;
|
||||||
|
public KeyCode SwitchAxis = KeyCode.Tab;
|
||||||
|
public KeyCode RotateZ = KeyCode.Space;
|
||||||
|
public KeyCode RotationSpeedIncrease = KeyCode.LeftControl;
|
||||||
|
public KeyCode ResetRotation = KeyCode.LeftAlt;
|
||||||
|
|
||||||
|
/// <summary>The rigidbody we are currently holding</summary>
|
||||||
|
private Rigidbody _grabbedRigidbody;
|
||||||
|
|
||||||
|
/// <summary>The offset vector from the object's position to hit point, in local space</summary>
|
||||||
|
private Vector3 _hitOffsetLocal;
|
||||||
|
|
||||||
|
/// <summary>The distance we are holding the object at</summary>
|
||||||
|
private float _currentGrabDistance;
|
||||||
|
|
||||||
|
/// <summary>The interpolation state when first grabbed</summary>
|
||||||
|
private RigidbodyInterpolation _initialInterpolationSetting;
|
||||||
|
|
||||||
|
/// <summary>The difference between player & object rotation, updated when picked up or when rotated by the player</summary>
|
||||||
|
private Quaternion _rotationDifference;
|
||||||
|
|
||||||
|
/// <summary>The start point for the Laser. This will typically be on the end of the gun</summary>
|
||||||
|
[SerializeField] private Transform _laserStartPoint;
|
||||||
|
|
||||||
|
/// <summary>Tracks player input to rotate current object. Used and reset every fixedupdate call</summary>
|
||||||
|
private Vector3 _rotationInput = Vector3.zero;
|
||||||
|
|
||||||
|
[Header("Rotation Settings")] [Tooltip("Transform of the player, that rotations should be relative to")]
|
||||||
|
public Transform PlayerTransform;
|
||||||
|
|
||||||
|
[SerializeField] private float _rotationSenstivity = 10f;
|
||||||
|
|
||||||
|
public float SnapRotationDegrees = 45f;
|
||||||
|
[SerializeField] private float _snappedRotationSens = 25f;
|
||||||
|
[SerializeField] private float _rotationSpeed = 10f;
|
||||||
|
|
||||||
|
private Quaternion _desiredRotation = Quaternion.identity;
|
||||||
|
|
||||||
|
[SerializeField] [Tooltip("Input values above this will be considered and intentional change in rotation")]
|
||||||
|
private float _rotationTollerance = 0.8f;
|
||||||
|
|
||||||
|
private bool m_UserRotation;
|
||||||
|
|
||||||
|
public bool UserRotation
|
||||||
|
{
|
||||||
|
get => m_UserRotation;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (m_UserRotation == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_UserRotation = value;
|
||||||
|
OnRotation?.Invoke(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool m_SnapRotation;
|
||||||
|
|
||||||
|
private bool _snapRotation
|
||||||
|
{
|
||||||
|
get => m_SnapRotation;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (m_SnapRotation == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_SnapRotation = value;
|
||||||
|
OnRotationSnapped?.Invoke(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool m_RotationAxis;
|
||||||
|
|
||||||
|
private bool _rotationAxis
|
||||||
|
{
|
||||||
|
get => m_RotationAxis;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (m_RotationAxis == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_RotationAxis = value;
|
||||||
|
OnAxisChanged?.Invoke(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3 _lockedRot;
|
||||||
|
|
||||||
|
private Vector3 _forward;
|
||||||
|
private Vector3 _up;
|
||||||
|
private Vector3 _right;
|
||||||
|
|
||||||
|
//ScrollWheel ObjectMovement
|
||||||
|
private Vector3 _scrollWheelInput = Vector3.zero;
|
||||||
|
|
||||||
|
[Header("Scroll Wheel Object Movement")] [Space(5)] [SerializeField]
|
||||||
|
private float _scrollWheelSensitivity = 5f;
|
||||||
|
|
||||||
|
[SerializeField] [Tooltip("The min distance the object can be from the player")]
|
||||||
|
private float _minObjectDistance = 2.5f;
|
||||||
|
|
||||||
|
/// <summary>The maximum distance at which a new object can be picked up</summary>
|
||||||
|
[SerializeField] [Tooltip("The maximum distance at which a new object can be picked up")]
|
||||||
|
private float _maxGrabDistance = 50f;
|
||||||
|
|
||||||
|
private bool _distanceChanged;
|
||||||
|
|
||||||
|
//Vector3.Zero and Vector2.zero create a new Vector3 each time they are called so these simply save that process and a small amount of cpu runtime.
|
||||||
|
private readonly Vector3 _zeroVector3 = Vector3.zero;
|
||||||
|
private readonly Vector3 _oneVector3 = Vector3.one;
|
||||||
|
private readonly Vector3 _zeroVector2 = Vector2.zero;
|
||||||
|
|
||||||
|
private bool _justReleased;
|
||||||
|
private bool _wasKinematic;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class BoolEvent : UnityEvent<bool> {}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class GrabEvent : UnityEvent<GameObject> {}
|
||||||
|
|
||||||
|
[Header("Events")] [Space(10)]
|
||||||
|
public BoolEvent OnRotation;
|
||||||
|
public BoolEvent OnRotationSnapped;
|
||||||
|
public BoolEvent OnAxisChanged;
|
||||||
|
|
||||||
|
public GrabEvent OnObjectGrabbed;
|
||||||
|
public Func<GameObject, bool> OnObjectReleased;
|
||||||
|
|
||||||
|
// pre event for OnPreGrabbedObject, return false to cancel grab
|
||||||
|
public Func<GameObject, bool> OnPreGrabbedObject;
|
||||||
|
// public Action<GameObject> OnGrabbedObject;
|
||||||
|
|
||||||
|
//public properties for the Axis Arrows. These are optional and can be safely removed
|
||||||
|
// public Vector3 CurrentForward => _forward;
|
||||||
|
// public Vector3 CurrentUp => _up;
|
||||||
|
// public Vector3 CurrentRight => _right;
|
||||||
|
|
||||||
|
/// <summary>The transfor of the rigidbody we are holding</summary>
|
||||||
|
public Transform CurrentGrabbedTransform { get; private set; }
|
||||||
|
|
||||||
|
//public properties for the Line Renderer
|
||||||
|
public Vector3 StartPoint { get; private set; }
|
||||||
|
public Vector3 MidPoint { get; private set; }
|
||||||
|
public Vector3 EndPoint { get; private set; }
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
if (Instance != null
|
||||||
|
&& Instance != this)
|
||||||
|
{
|
||||||
|
Destroy(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Instance = this;
|
||||||
|
gameObject.AddComponent<ObjectSyncBridge>();
|
||||||
|
|
||||||
|
_camera = PlayerSetup.Instance.GetActiveCamera().GetComponent<Camera>();
|
||||||
|
PlayerTransform = PlayerSetup.Instance.transform; // TODO: this might be fucked in VR
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (Instance == this)
|
||||||
|
Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!Input.GetMouseButton(0))
|
||||||
|
{
|
||||||
|
// We are not holding the mouse button. Release the object and return before checking for a new one
|
||||||
|
if (_grabbedRigidbody != null) ReleaseObject();
|
||||||
|
|
||||||
|
_justReleased = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_grabbedRigidbody == null && !_justReleased)
|
||||||
|
{
|
||||||
|
// We are not holding an object, look for one to pick up
|
||||||
|
Ray ray = CenterRay();
|
||||||
|
RaycastHit hit;
|
||||||
|
|
||||||
|
//Just so These aren't included in a build
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
Debug.DrawRay(ray.origin, ray.direction * _maxGrabDistance, Color.blue, 0.01f);
|
||||||
|
#endif
|
||||||
|
if (Physics.Raycast(ray, out hit, _maxGrabDistance, _grabLayer))
|
||||||
|
// Don't pick up kinematic rigidbodies (they can't move)
|
||||||
|
if (hit.rigidbody != null /*&& !hit.rigidbody.isKinematic*/)
|
||||||
|
{
|
||||||
|
// Check if we are allowed to pick up this object
|
||||||
|
if (OnPreGrabbedObject != null
|
||||||
|
&& !OnPreGrabbedObject(hit.rigidbody.gameObject))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Track rigidbody's initial information
|
||||||
|
_grabbedRigidbody = hit.rigidbody;
|
||||||
|
_wasKinematic = _grabbedRigidbody.isKinematic;
|
||||||
|
_grabbedRigidbody.isKinematic = false;
|
||||||
|
_grabbedRigidbody.freezeRotation = true;
|
||||||
|
_initialInterpolationSetting = _grabbedRigidbody.interpolation;
|
||||||
|
_rotationDifference = Quaternion.Inverse(PlayerTransform.rotation) * _grabbedRigidbody.rotation;
|
||||||
|
_hitOffsetLocal = hit.transform.InverseTransformVector(hit.point - hit.transform.position);
|
||||||
|
_currentGrabDistance = hit.distance; // Vector3.Distance(ray.origin, hit.point);
|
||||||
|
CurrentGrabbedTransform = _grabbedRigidbody.transform;
|
||||||
|
// Set rigidbody's interpolation for proper collision detection when being moved by the player
|
||||||
|
_grabbedRigidbody.interpolation = RigidbodyInterpolation.Interpolate;
|
||||||
|
|
||||||
|
OnObjectGrabbed?.Invoke(_grabbedRigidbody.gameObject);
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
Debug.DrawRay(hit.point, hit.normal * 10f, Color.red, 10f);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_grabbedRigidbody != null)
|
||||||
|
{
|
||||||
|
UserRotation = Input.GetKey(Rotate);
|
||||||
|
|
||||||
|
if (Input.GetKeyDown(Rotate))
|
||||||
|
_desiredRotation = _grabbedRigidbody.rotation;
|
||||||
|
|
||||||
|
if (Input.GetKey(ResetRotation))
|
||||||
|
{
|
||||||
|
_desiredRotation = Quaternion.identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are already holding an object, listen for rotation input
|
||||||
|
if (Input.GetKey(Rotate))
|
||||||
|
{
|
||||||
|
var rotateZ = Input.GetKey(RotateZ);
|
||||||
|
|
||||||
|
var increaseSens = Input.GetKey(RotationSpeedIncrease) ? 2.5f : 1f;
|
||||||
|
|
||||||
|
if (Input.GetKeyDown(SwitchAxis))
|
||||||
|
{
|
||||||
|
_rotationAxis = !_rotationAxis;
|
||||||
|
|
||||||
|
OnAxisChanged?.Invoke(_rotationAxis);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Snap Object nearest _snapRotationDegrees
|
||||||
|
if (Input.GetKeyDown(SnapRotation))
|
||||||
|
{
|
||||||
|
_snapRotation = true;
|
||||||
|
|
||||||
|
Vector3 newRot = _grabbedRigidbody.transform.rotation.eulerAngles;
|
||||||
|
|
||||||
|
newRot.x = Mathf.Round(newRot.x / SnapRotationDegrees) * SnapRotationDegrees;
|
||||||
|
newRot.y = Mathf.Round(newRot.y / SnapRotationDegrees) * SnapRotationDegrees;
|
||||||
|
newRot.z = Mathf.Round(newRot.z / SnapRotationDegrees) * SnapRotationDegrees;
|
||||||
|
|
||||||
|
Quaternion rot = Quaternion.Euler(newRot);
|
||||||
|
|
||||||
|
_desiredRotation = rot;
|
||||||
|
//_grabbedRigidbody.MoveRotation(rot);
|
||||||
|
}
|
||||||
|
else if (Input.GetKeyUp(SnapRotation))
|
||||||
|
{
|
||||||
|
_snapRotation = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = Input.GetAxisRaw("Mouse X");
|
||||||
|
var y = Input.GetAxisRaw("Mouse Y");
|
||||||
|
|
||||||
|
if (Mathf.Abs(x) > _rotationTollerance)
|
||||||
|
{
|
||||||
|
_rotationInput.x = rotateZ ? 0f : x * _rotationSenstivity * increaseSens;
|
||||||
|
_rotationInput.z = rotateZ ? x * _rotationSenstivity * increaseSens : 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Mathf.Abs(y) > _rotationTollerance) _rotationInput.y = y * _rotationSenstivity * increaseSens;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_snapRotation = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var direction = Input.GetAxis("Mouse ScrollWheel");
|
||||||
|
|
||||||
|
//Optional Keyboard inputs
|
||||||
|
if (Input.GetKeyDown(KeyCode.T))
|
||||||
|
direction = -0.1f;
|
||||||
|
else if (Input.GetKeyDown(KeyCode.G))
|
||||||
|
direction = 0.1f;
|
||||||
|
|
||||||
|
if (Mathf.Abs(direction) > 0 && CheckObjectDistance(direction))
|
||||||
|
{
|
||||||
|
_distanceChanged = true;
|
||||||
|
_scrollWheelInput = PlayerTransform.forward * (_scrollWheelSensitivity * direction);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_scrollWheelInput = _zeroVector3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input.GetMouseButtonDown(1))
|
||||||
|
{
|
||||||
|
//To prevent warnings in the inpector
|
||||||
|
_grabbedRigidbody.collisionDetectionMode = !_wasKinematic
|
||||||
|
? CollisionDetectionMode.ContinuousSpeculative
|
||||||
|
: CollisionDetectionMode.Continuous;
|
||||||
|
_grabbedRigidbody.isKinematic = _wasKinematic = !_wasKinematic;
|
||||||
|
|
||||||
|
_justReleased = true;
|
||||||
|
ReleaseObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (_grabbedRigidbody)
|
||||||
|
{
|
||||||
|
// We are holding an object, time to rotate & move it
|
||||||
|
Ray ray = CenterRay();
|
||||||
|
|
||||||
|
UpdateRotationAxis();
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
Debug.DrawRay(_grabbedTransform.position, _up * 5f , Color.green);
|
||||||
|
Debug.DrawRay(_grabbedTransform.position, _right * 5f , Color.red);
|
||||||
|
Debug.DrawRay(_grabbedTransform.position, _forward * 5f , Color.blue);
|
||||||
|
#endif
|
||||||
|
// Apply any intentional rotation input made by the player & clear tracked input
|
||||||
|
Quaternion intentionalRotation = Quaternion.AngleAxis(_rotationInput.z, _forward) *
|
||||||
|
Quaternion.AngleAxis(_rotationInput.y, _right) *
|
||||||
|
Quaternion.AngleAxis(-_rotationInput.x, _up) * _desiredRotation;
|
||||||
|
Quaternion relativeToPlayerRotation = PlayerTransform.rotation * _rotationDifference;
|
||||||
|
|
||||||
|
if (UserRotation && _snapRotation)
|
||||||
|
{
|
||||||
|
//Add mouse movement to vector so we can measure the amount of movement
|
||||||
|
_lockedRot += _rotationInput;
|
||||||
|
|
||||||
|
//If the mouse has moved far enough to rotate the snapped object
|
||||||
|
if (Mathf.Abs(_lockedRot.x) > _snappedRotationSens || Mathf.Abs(_lockedRot.y) > _snappedRotationSens ||
|
||||||
|
Mathf.Abs(_lockedRot.z) > _snappedRotationSens)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
if (_lockedRot[i] > _snappedRotationSens)
|
||||||
|
_lockedRot[i] += SnapRotationDegrees;
|
||||||
|
else if (_lockedRot[i] < -_snappedRotationSens)
|
||||||
|
_lockedRot[i] += -SnapRotationDegrees;
|
||||||
|
else
|
||||||
|
_lockedRot[i] = 0;
|
||||||
|
|
||||||
|
Quaternion q = Quaternion.AngleAxis(-_lockedRot.x, _up) *
|
||||||
|
Quaternion.AngleAxis(_lockedRot.y, _right) *
|
||||||
|
Quaternion.AngleAxis(_lockedRot.z, _forward) * _desiredRotation;
|
||||||
|
|
||||||
|
Vector3 newRot = q.eulerAngles;
|
||||||
|
|
||||||
|
newRot.x = Mathf.Round(newRot.x / SnapRotationDegrees) * SnapRotationDegrees;
|
||||||
|
newRot.y = Mathf.Round(newRot.y / SnapRotationDegrees) * SnapRotationDegrees;
|
||||||
|
newRot.z = Mathf.Round(newRot.z / SnapRotationDegrees) * SnapRotationDegrees;
|
||||||
|
|
||||||
|
_desiredRotation = Quaternion.Euler(newRot);
|
||||||
|
|
||||||
|
_lockedRot = _zeroVector2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Rotate the object to remain consistent with any changes in player's rotation
|
||||||
|
_desiredRotation = UserRotation ? intentionalRotation : relativeToPlayerRotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all torque, reset rotation input & store the rotation difference for next FixedUpdate call
|
||||||
|
_grabbedRigidbody.angularVelocity = _zeroVector3;
|
||||||
|
_rotationInput = _zeroVector2;
|
||||||
|
_rotationDifference = Quaternion.Inverse(PlayerTransform.rotation) * _desiredRotation;
|
||||||
|
|
||||||
|
// Calculate object's center position based on the offset we stored
|
||||||
|
// NOTE: We need to convert the local-space point back to world coordinates
|
||||||
|
// Get the destination point for the point on the object we grabbed
|
||||||
|
Vector3 holdPoint = ray.GetPoint(_currentGrabDistance) + _scrollWheelInput;
|
||||||
|
Vector3 centerDestination = holdPoint - CurrentGrabbedTransform.TransformVector(_hitOffsetLocal);
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
Debug.DrawLine(ray.origin, holdPoint, Color.blue, Time.fixedDeltaTime);
|
||||||
|
#endif
|
||||||
|
// Find vector from current position to destination
|
||||||
|
Vector3 toDestination = centerDestination - CurrentGrabbedTransform.position;
|
||||||
|
|
||||||
|
// Calculate force
|
||||||
|
Vector3 force = toDestination / Time.fixedDeltaTime * 0.3f/* / _grabbedRigidbody.mass*/;
|
||||||
|
|
||||||
|
//force += _scrollWheelInput;
|
||||||
|
// Remove any existing velocity and add force to move to final position
|
||||||
|
_grabbedRigidbody.velocity = _zeroVector3;
|
||||||
|
_grabbedRigidbody.AddForce(force, ForceMode.VelocityChange);
|
||||||
|
|
||||||
|
//Rotate object
|
||||||
|
RotateGrabbedObject();
|
||||||
|
|
||||||
|
//We need to recalculte the grabbed distance as the object distance from the player has been changed
|
||||||
|
if (_distanceChanged)
|
||||||
|
{
|
||||||
|
_distanceChanged = false;
|
||||||
|
_currentGrabDistance = Vector3.Distance(ray.origin, holdPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update public properties
|
||||||
|
StartPoint = _laserStartPoint.transform.position;
|
||||||
|
MidPoint = holdPoint;
|
||||||
|
EndPoint = CurrentGrabbedTransform.TransformPoint(_hitOffsetLocal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RotateGrabbedObject()
|
||||||
|
{
|
||||||
|
if (_grabbedRigidbody == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// lerp to desired rotation
|
||||||
|
_grabbedRigidbody.MoveRotation(Quaternion.Lerp(_grabbedRigidbody.rotation, _desiredRotation,
|
||||||
|
Time.fixedDeltaTime * _rotationSpeed));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update Rotation axis based on movement
|
||||||
|
private void UpdateRotationAxis()
|
||||||
|
{
|
||||||
|
if (!_snapRotation)
|
||||||
|
{
|
||||||
|
_forward = PlayerTransform.forward;
|
||||||
|
_right = PlayerTransform.right;
|
||||||
|
_up = PlayerTransform.up;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_rotationAxis)
|
||||||
|
{
|
||||||
|
_forward = CurrentGrabbedTransform.forward;
|
||||||
|
_right = CurrentGrabbedTransform.right;
|
||||||
|
_up = CurrentGrabbedTransform.up;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NearestTranformDirection(CurrentGrabbedTransform, PlayerTransform, ref _up, ref _forward, ref _right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NearestTranformDirection(Transform transformToCheck, Transform referenceTransform, ref Vector3 up,
|
||||||
|
ref Vector3 forward, ref Vector3 right)
|
||||||
|
{
|
||||||
|
var directions = new List<Vector3>
|
||||||
|
{
|
||||||
|
transformToCheck.forward,
|
||||||
|
-transformToCheck.forward,
|
||||||
|
transformToCheck.up,
|
||||||
|
-transformToCheck.up,
|
||||||
|
transformToCheck.right,
|
||||||
|
-transformToCheck.right
|
||||||
|
};
|
||||||
|
|
||||||
|
//Find the up Vector
|
||||||
|
up = GetDirectionVector(directions, referenceTransform.up);
|
||||||
|
//Remove Vectors from list to prevent duplicates and the opposite vector being found in case where the player is at around a 45 degree angle to the object
|
||||||
|
directions.Remove(up);
|
||||||
|
directions.Remove(-up);
|
||||||
|
//Find the Forward Vector
|
||||||
|
forward = GetDirectionVector(directions, referenceTransform.forward);
|
||||||
|
//Remove used directions
|
||||||
|
directions.Remove(forward);
|
||||||
|
directions.Remove(-forward);
|
||||||
|
|
||||||
|
right = GetDirectionVector(directions, referenceTransform.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3 GetDirectionVector(List<Vector3> directions, Vector3 direction)
|
||||||
|
{
|
||||||
|
var maxDot = -Mathf.Infinity;
|
||||||
|
Vector3 ret = Vector3.zero;
|
||||||
|
|
||||||
|
for (var i = 0; i < directions.Count; i++)
|
||||||
|
{
|
||||||
|
var dot = Vector3.Dot(direction, directions[i]);
|
||||||
|
|
||||||
|
if (dot > maxDot)
|
||||||
|
{
|
||||||
|
ret = directions[i];
|
||||||
|
maxDot = dot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <returns>Ray from center of the main camera's viewport forward</returns>
|
||||||
|
private Ray CenterRay()
|
||||||
|
{
|
||||||
|
return _camera.ViewportPointToRay(_oneVector3 * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check distance is within range when moving object with the scroll wheel
|
||||||
|
private bool CheckObjectDistance(float direction)
|
||||||
|
{
|
||||||
|
Vector3 pointA = PlayerTransform.position;
|
||||||
|
Vector3 pointB = _grabbedRigidbody.position;
|
||||||
|
|
||||||
|
var distance = Vector3.Distance(pointA, pointB);
|
||||||
|
|
||||||
|
if (direction > 0)
|
||||||
|
return distance <= _maxGrabDistance;
|
||||||
|
|
||||||
|
if (direction < 0)
|
||||||
|
return distance >= _minObjectDistance;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseObject()
|
||||||
|
{
|
||||||
|
if (OnObjectReleased != null)
|
||||||
|
OnObjectReleased(_grabbedRigidbody.gameObject);
|
||||||
|
|
||||||
|
if (_grabbedRigidbody)
|
||||||
|
{
|
||||||
|
//Move rotation to desired rotation in case the lerp hasn't finished
|
||||||
|
//_grabbedRigidbody.MoveRotation(_desiredRotation);
|
||||||
|
// Reset the rigidbody to how it was before we grabbed it
|
||||||
|
_grabbedRigidbody.isKinematic = _wasKinematic;
|
||||||
|
_grabbedRigidbody.interpolation = _initialInterpolationSetting;
|
||||||
|
_grabbedRigidbody.freezeRotation = false;
|
||||||
|
_grabbedRigidbody = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scrollWheelInput = _zeroVector3;
|
||||||
|
CurrentGrabbedTransform = null;
|
||||||
|
UserRotation = false;
|
||||||
|
_snapRotation = false;
|
||||||
|
StartPoint = _zeroVector3;
|
||||||
|
MidPoint = _zeroVector3;
|
||||||
|
EndPoint = _zeroVector3;
|
||||||
|
|
||||||
|
OnObjectGrabbed?.Invoke(null);
|
||||||
|
}
|
||||||
|
}
|
20
PhysicsGunMod/HarmonyPatches.cs
Normal file
20
PhysicsGunMod/HarmonyPatches.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
using ABI_RC.Systems.InputManagement;
|
||||||
|
using HarmonyLib;
|
||||||
|
using NAK.PhysicsGunMod.Components;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace NAK.PhysicsGunMod.HarmonyPatches;
|
||||||
|
|
||||||
|
internal static class CVRInputManagerPatches
|
||||||
|
{
|
||||||
|
[HarmonyPostfix]
|
||||||
|
[HarmonyPatch(typeof(CVRInputManager), nameof(CVRInputManager.Update))]
|
||||||
|
private static void Postfix_CVRInputManager_Update(ref CVRInputManager __instance)
|
||||||
|
{
|
||||||
|
if (PhysicsGunInteractionBehavior.Instance == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (PhysicsGunInteractionBehavior.Instance.UserRotation)
|
||||||
|
__instance.lookVector = Vector2.zero;
|
||||||
|
}
|
||||||
|
}
|
43
PhysicsGunMod/Main.cs
Normal file
43
PhysicsGunMod/Main.cs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
using ABI_RC.Core.Util.AssetFiltering;
|
||||||
|
using MelonLoader;
|
||||||
|
using NAK.PhysicsGunMod.Components;
|
||||||
|
using NAK.PhysicsGunMod.HarmonyPatches;
|
||||||
|
|
||||||
|
namespace NAK.PhysicsGunMod;
|
||||||
|
|
||||||
|
public class PhysicsGunMod : MelonMod
|
||||||
|
{
|
||||||
|
internal static MelonLogger.Instance Logger;
|
||||||
|
|
||||||
|
public override void OnInitializeMelon()
|
||||||
|
{
|
||||||
|
Logger = LoggerInstance;
|
||||||
|
|
||||||
|
// add to prop whitelist
|
||||||
|
SharedFilter._spawnableWhitelist.Add(typeof(PhysicsGunInteractionBehavior));
|
||||||
|
|
||||||
|
// add to event whitelist
|
||||||
|
SharedFilter._allowedEventComponents.Add(typeof(PhysicsGunInteractionBehavior));
|
||||||
|
SharedFilter._allowedEventFunctions.Add(typeof(PhysicsGunInteractionBehavior), new List<string>
|
||||||
|
{
|
||||||
|
"set_enabled",
|
||||||
|
// TODO: expose more methods like release ?
|
||||||
|
});
|
||||||
|
|
||||||
|
// apply patches
|
||||||
|
ApplyPatches(typeof(CVRInputManagerPatches));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyPatches(Type type)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HarmonyInstance.PatchAll(type);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LoggerInstance.Msg($"Failed while patching {type.Name}!");
|
||||||
|
LoggerInstance.Error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
PhysicsGunMod/ModSettings.cs
Normal file
12
PhysicsGunMod/ModSettings.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using MelonLoader;
|
||||||
|
|
||||||
|
namespace NAK.PhysicsGunMod;
|
||||||
|
|
||||||
|
internal static class ModSettings
|
||||||
|
{
|
||||||
|
internal const string ModName = nameof(PhysicsGunMod);
|
||||||
|
internal const string ASM_SettingsCategory = "Physics Gun Mod";
|
||||||
|
|
||||||
|
private static readonly MelonPreferences_Category Category =
|
||||||
|
MelonPreferences.CreateCategory(ModName);
|
||||||
|
}
|
37
PhysicsGunMod/PhysicsGunMod.csproj
Normal file
37
PhysicsGunMod/PhysicsGunMod.csproj
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
<NoWarn />
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||||
|
<NoWarn />
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="resources\menu.js" />
|
||||||
|
<None Remove="resources\nak_menu.css" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="resources\Icon_AvatarHeightConfig.png" />
|
||||||
|
<None Remove="resources\ASM_Icon_AvatarHeightCopy.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="BTKUILib">
|
||||||
|
<HintPath>$(MsBuildThisFileDirectory)\..\.ManagedLibs\BTKUILib.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="ActionMenu">
|
||||||
|
<HintPath>$(MsBuildThisFileDirectory)\..\.ManagedLibs\ActionMenu.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
32
PhysicsGunMod/Properties/AssemblyInfo.cs
Normal file
32
PhysicsGunMod/Properties/AssemblyInfo.cs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
using MelonLoader;
|
||||||
|
using NAK.PhysicsGunMod.Properties;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
|
||||||
|
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
|
||||||
|
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
|
||||||
|
[assembly: AssemblyTitle(nameof(NAK.PhysicsGunMod))]
|
||||||
|
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
|
||||||
|
[assembly: AssemblyProduct(nameof(NAK.PhysicsGunMod))]
|
||||||
|
|
||||||
|
[assembly: MelonInfo(
|
||||||
|
typeof(NAK.PhysicsGunMod.PhysicsGunMod),
|
||||||
|
nameof(NAK.PhysicsGunMod),
|
||||||
|
AssemblyInfoParams.Version,
|
||||||
|
AssemblyInfoParams.Author,
|
||||||
|
downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/PhysicsGunMod"
|
||||||
|
)]
|
||||||
|
|
||||||
|
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
|
||||||
|
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
|
||||||
|
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
|
||||||
|
[assembly: MelonColor(255, 241, 200, 82)]
|
||||||
|
[assembly: MelonAuthorColor(255, 114, 17, 25)]
|
||||||
|
[assembly: HarmonyDontPatchAll]
|
||||||
|
|
||||||
|
namespace NAK.PhysicsGunMod.Properties;
|
||||||
|
internal static class AssemblyInfoParams
|
||||||
|
{
|
||||||
|
public const string Version = "1.0.0";
|
||||||
|
public const string Author = "NotAKidoS";
|
||||||
|
}
|
28
PhysicsGunMod/README.md
Normal file
28
PhysicsGunMod/README.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# AvatarScaleMod
|
||||||
|
|
||||||
|
Proof of concept mod to add Avatar Scaling to any avatar. This is local-only, but I may toy with using Mod Network.
|
||||||
|
|
||||||
|
Legit threw this together in three hours. ChilloutVR handles all the hard stuff already with its existing animation-clip-based Avatar Scaling.
|
||||||
|
|
||||||
|
https://github.com/NotAKidOnSteam/NAK_CVR_Mods/assets/37721153/7405cef5-fd68-4103-8c18-b3164029eab1
|
||||||
|
|
||||||
|
## Notes:
|
||||||
|
* Constraint scaling partially conflicts with avatars run through my [Avatar Scale Tool](https://github.com/NotAKidOnSteam/AvatarScaleTool).
|
||||||
|
* This is local-only, at least unless I bother with Mod Network.
|
||||||
|
* The entire thing is pretty messy and I am unsure of the performance impact, especially with scaling all lights, audio, & constraints.
|
||||||
|
|
||||||
|
## Relevant Feedback Posts:
|
||||||
|
https://feedback.abinteractive.net/p/built-in-avatar-scaling-system
|
||||||
|
|
||||||
|
This mod is me creating the system I wanted when I wrote the above feedback post.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
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
PhysicsGunMod/format.json
Normal file
23
PhysicsGunMod/format.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"_id": 126,
|
||||||
|
"name": "BetterCalibration",
|
||||||
|
"modversion": "1.0.0",
|
||||||
|
"gameversion": "2022r173",
|
||||||
|
"loaderversion": "0.6.1",
|
||||||
|
"modtype": "Mod",
|
||||||
|
"author": "NotAKidoS",
|
||||||
|
"description": "Mod to improve the calibration process for FBT.",
|
||||||
|
"searchtags": [
|
||||||
|
"IK",
|
||||||
|
"FBT",
|
||||||
|
"VRIK",
|
||||||
|
"calibration",
|
||||||
|
],
|
||||||
|
"requirements": [
|
||||||
|
"None"
|
||||||
|
],
|
||||||
|
"downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r3/BetterCalibration.dll",
|
||||||
|
"sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/BetterCalibration/",
|
||||||
|
"changelog": "",
|
||||||
|
"embedcolor": "9b59b6"
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue