Move many mods to Deprecated folder, fix spelling

This commit is contained in:
NotAKidoS 2025-04-03 02:57:35 -05:00
parent 5e822cec8d
commit 0042590aa6
539 changed files with 7475 additions and 3120 deletions

View file

@ -0,0 +1,42 @@
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.InteractionSystem.Base;
using ABI_RC.Systems.InputManagement;
using UnityEngine;
using UnityEngine.Serialization;
namespace ABI_RC.Core.Player.Interaction
{
public class CVRPlayerHand : MonoBehaviour
{
#region Fields
[SerializeField]
private CVRHand _hand;
// Pickup rig
[SerializeField] private Transform rayDirection;
[SerializeField] private Transform _attachmentPoint;
[SerializeField] private Transform _pivotPoint;
[SerializeField] private VelocityTracker _velocityTracker;
// Pickup state
private bool _isHoldingObject;
private Pickupable _heldPickupable;
private Pickupable _proximityPickupable;
#endregion Fields
#region Unity Events
#endregion Unity Events
#region Private Methods
#endregion Private Methods
#region Public Methods
#endregion Public Methods
}
}

View file

@ -0,0 +1,214 @@
using ABI_RC.Core.Player.Interaction.RaycastImpl;
using ABI_RC.Core.Savior;
using ABI_RC.Systems.InputManagement;
using UnityEngine;
namespace ABI_RC.Core.Player.Interaction
{
public class CVRPlayerInteractionManager : MonoBehaviour
{
#region Singleton
public static CVRPlayerInteractionManager Instance { get; private set; }
#endregion Singleton
#region Serialized Fields
[Header("Hand Components")]
[SerializeField] private CVRPlayerHand handVrLeft;
[SerializeField] private CVRPlayerHand handVrRight;
[SerializeField] private CVRPlayerHand handDesktopRight; // Desktop does not have a left hand
[Header("Raycast Transforms")]
[SerializeField] private Transform raycastTransformVrRight;
[SerializeField] private Transform raycastTransformVrLeft;
[SerializeField] private Transform raycastTransformDesktopRight;
[Header("Settings")]
[SerializeField] private bool interactionEnabled = true;
[SerializeField] private LayerMask interactionLayerMask = -1; // Default to all layers, will be filtered
#endregion Serialized Fields
#region Properties
private CVRPlayerHand _rightHand;
private CVRPlayerHand _leftHand;
private CVRPlayerRaycaster _rightRaycaster;
private CVRPlayerRaycaster _leftRaycaster;
private CVRRaycastResult _rightRaycastResult;
private CVRRaycastResult _leftRaycastResult;
// Input handler
private CVRPlayerInputHandler _inputHandler;
// Interaction flags
public bool InteractionEnabled
{
get => interactionEnabled;
set => interactionEnabled = value;
}
#endregion Properties
#region Unity Events
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
// Create the input handler
_inputHandler = gameObject.AddComponent<CVRPlayerInputHandler>();
}
private void Start()
{
// Setup interaction for current device mode
SetupInteractionForDeviceMode();
// Listen for VR mode changes
MetaPort.Instance.onVRModeSwitch.AddListener(SetupInteractionForDeviceMode);
}
private void Update()
{
if (!interactionEnabled)
return;
// Process right hand
if (_rightRaycaster != null)
{
// Determine raycast flags based on current mode
CVRPlayerRaycaster.RaycastFlags flags = DetermineRaycastFlags(_rightHand);
// Get raycast results
_rightRaycastResult = _rightRaycaster.GetRaycastResults(flags);
// Process input based on raycast results
_inputHandler.ProcessInput(CVRHand.Right, _rightRaycastResult);
}
// Process left hand (if available)
if (_leftRaycaster != null)
{
// Determine raycast flags based on current mode
CVRPlayerRaycaster.RaycastFlags flags = DetermineRaycastFlags(_leftHand);
// Get raycast results
_leftRaycastResult = _leftRaycaster.GetRaycastResults(flags);
// Process input based on raycast results
_inputHandler.ProcessInput(CVRHand.Left, _leftRaycastResult);
}
}
private void OnDestroy()
{
// Clean up event listener
if (MetaPort.Instance != null)
MetaPort.Instance.onVRModeSwitch.RemoveListener(SetupInteractionForDeviceMode);
}
#endregion Unity Events
#region Public Methods
/// <summary>
/// Register a custom tool mode
/// </summary>
public void RegisterCustomToolMode(System.Action<CVRHand, CVRRaycastResult, InputState> callback)
{
_inputHandler.RegisterCustomTool(callback);
}
/// <summary>
/// Unregister the current custom tool mode
/// </summary>
public void UnregisterCustomToolMode()
{
_inputHandler.UnregisterCustomTool();
}
/// <summary>
/// Set the interaction mode
/// </summary>
public void SetInteractionMode(CVRPlayerInputHandler.InteractionMode mode)
{
_inputHandler.SetInteractionMode(mode);
}
/// <summary>
/// Get the raycast result for a specific hand
/// </summary>
public CVRRaycastResult GetRaycastResult(CVRHand hand)
{
return hand == CVRHand.Left ? _leftRaycastResult : _rightRaycastResult;
}
#endregion Public Methods
#region Private Methods
private void SetupInteractionForDeviceMode()
{
bool isVr = MetaPort.Instance.isUsingVr;
if (isVr)
{
// VR mode
_rightHand = handVrRight;
_leftHand = handVrLeft;
// VR uses the controller transform for raycasting
_rightRaycaster = new CVRPlayerRaycasterTransform(raycastTransformVrRight);
_leftRaycaster = new CVRPlayerRaycasterTransform(raycastTransformVrLeft);
}
else
{
// Desktop mode
_rightHand = handDesktopRight;
_leftHand = null;
// Desktop uses the mouse position for raycasting when unlocked
Camera desktopCamera = PlayerSetup.Instance.desktopCam;
_rightRaycaster = new CVRPlayerRaycasterMouse(raycastTransformDesktopRight, desktopCamera);
_leftRaycaster = null;
}
// Set the layer mask for raycasters
if (_rightRaycaster != null)
_rightRaycaster.SetLayerMask(interactionLayerMask);
if (_leftRaycaster != null)
_leftRaycaster.SetLayerMask(interactionLayerMask);
}
private static CVRPlayerRaycaster.RaycastFlags DetermineRaycastFlags(CVRPlayerHand hand)
{
// Default to all flags
CVRPlayerRaycaster.RaycastFlags flags = CVRPlayerRaycaster.RaycastFlags.All;
// Check if hand is holding a pickup
if (hand != null && hand.IsHoldingObject)
{
// When holding an object, only check for COHTML interaction
flags = CVRPlayerRaycaster.RaycastFlags.CohtmlInteract;
}
// Could add more conditional flag adjustments here based on the current mode
// For example, in a teleport tool mode, you might only want world hits
return flags;
}
#endregion Private Methods
}
}

View file

@ -0,0 +1,121 @@
using ABI_RC.Core.Base;
using ABI_RC.Core.Player;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace NAK.SuperAwesomeMod.Components
{
public class CVRCanvasWrapper : MonoBehaviour
{
public bool IsInteractable = true;
public float MaxInteractDistance = 10f;
private Canvas _canvas;
private GraphicRaycaster _graphicsRaycaster;
private static readonly List<RaycastResult> _raycastResults = new();
private static readonly PointerEventData _pointerEventData = new(EventSystem.current);
private static Selectable _workingSelectable;
private Camera _camera;
private RectTransform _rectTransform;
#region Unity Events
private void Awake()
{
if (!TryGetComponent(out _canvas)
|| _canvas.renderMode != RenderMode.WorldSpace)
{
IsInteractable = false;
return;
}
_rectTransform = _canvas.GetComponent<RectTransform>();
}
private void Start()
{
_graphicsRaycaster = _canvas.gameObject.AddComponent<GraphicRaycaster>();
_camera = PlayerSetup.Instance.activeCam;
_canvas.worldCamera = _camera;
}
#endregion Unity Events
#region Public Methods
public bool GetGraphicsHit(Ray worldRay, out RaycastResult result)
{
result = default;
if (!IsInteractable || _camera == null) return false;
// Get the plane of the canvas
Plane canvasPlane = new(transform.forward, transform.position);
// Find where the ray intersects the canvas plane
if (!canvasPlane.Raycast(worldRay, out float distance))
return false;
// Get the world point of intersection
Vector3 worldHitPoint = worldRay.origin + worldRay.direction * distance;
// Check if hit point is within max interaction distance
if (Vector3.Distance(worldRay.origin, worldHitPoint) > MaxInteractDistance)
return false;
// Check if hit point is within canvas bounds
Vector3 localHitPoint = transform.InverseTransformPoint(worldHitPoint);
Rect canvasRect = _rectTransform.rect;
if (!canvasRect.Contains(new Vector2(localHitPoint.x, localHitPoint.y)))
return false;
// Convert world hit point to screen space
Vector2 screenPoint = _camera.WorldToScreenPoint(worldHitPoint);
// Update pointer event data
_pointerEventData.position = screenPoint;
_pointerEventData.delta = Vector2.zero;
// Clear previous results and perform raycast
_raycastResults.Clear();
_graphicsRaycaster.Raycast(_pointerEventData, _raycastResults);
// Early out if no hits
if (_raycastResults.Count == 0)
{
//Debug.Log($"No hits on canvas {_canvas.name}");
return false;
}
// Find first valid interactive UI element
foreach (RaycastResult hit in _raycastResults)
{
if (!hit.isValid)
{
//Debug.Log($"Invalid hit on canvas {_canvas.name}");
continue;
}
// Check if the hit object has a Selectable component and is interactable
GameObject hitObject = hit.gameObject;
if (!hitObject.TryGetComponent(out _workingSelectable)
|| !_workingSelectable.interactable)
{
//Debug.Log($"Non-interactable hit on canvas {_canvas.name} - {hitObject.name}");
continue;
}
//Debug.Log($"Hit on canvas {_canvas.name} with {hitObject.name}");
result = hit;
return true;
}
return false;
}
#endregion Public Methods
}
}

View file

@ -0,0 +1,385 @@
using ABI_RC.Core.InteractionSystem.Base;
using ABI_RC.Core.UI;
using ABI.CCK.Components;
using NAK.SuperAwesomeMod.Components;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace ABI_RC.Core.Player.Interaction.RaycastImpl
{
public abstract class CVRPlayerRaycaster
{
#region Enums
[Flags]
public enum RaycastFlags
{
None = 0,
TelepathicCandidate = 1 << 0,
ProximityInteract = 1 << 1,
RayInteract = 1 << 2,
CohtmlInteract = 1 << 3,
All = ~0
}
#endregion Enums
#region Constants
private const float MAX_RAYCAST_DISTANCE = 100f; // Max distance you can raycast
private const float RAYCAST_SPHERE_RADIUS = 0.1f; // Radius of the proximity sphere
private const float TELEPATHIC_SPHERE_RADIUS = 0.3f; // Radius of the telepathic sphere
private const float MAX_TELEPATHIC_DISTANCE = 20f; // Max distance for telepathic grab
private const int MAX_RAYCAST_HITS = 100; // Hit buffer size, high due to triggers, which we use lots in CCK
// Global setting is Collide, but better to be explicit about what we need
private const QueryTriggerInteraction _triggerInteraction = QueryTriggerInteraction.Collide;
// Layers that are reserved for other purposes or illegal to interact with
private const int RESERVED_OR_ILLEGAL_LAYERS = (1 << CVRLayers.IgnoreRaycast)
| (1 << CVRLayers.MirrorReflection)
| (1 << CVRLayers.PlayerLocal);
#endregion Constants
#region Static Fields
private static readonly RaycastHit[] _hits = new RaycastHit[MAX_RAYCAST_HITS];
private static readonly Comparer<RaycastHit> _hitsComparer = Comparer<RaycastHit>.Create((hit1, hit2) =>
{
bool isUI1 = hit1.collider.gameObject.layer == CVRLayers.UIInternal;
bool isUI2 = hit2.collider.gameObject.layer == CVRLayers.UIInternal;
// Prioritize UIInternal hits
if (isUI1 && !isUI2) return -1; // UIInternal comes first
if (!isUI1 && isUI2) return 1; // Non-UIInternal comes after
// If both are UIInternal or both are not, sort by distance
return hit1.distance.CompareTo(hit2.distance);
});
private static readonly LayerMask _telepathicLayerMask = 1 << CVRLayers.MirrorReflection;
// Working variables to avoid repeated allocations
private static Collider _workingCollider;
private static GameObject _workingGameObject;
private static Pickupable _workingPickupable;
private static Interactable _workingInteractable;
private static Selectable _workingSelectable;
private static ICanvasElement _workingCanvasElement;
#endregion Static Fields
#region Private Fields
private LayerMask _layerMask; // Default to no layers so we know if we fucked up
#endregion Private Fields
#region Constructor
protected CVRPlayerRaycaster(Transform rayOrigin) => _rayOrigin = rayOrigin;
protected readonly Transform _rayOrigin;
#endregion Constructor
#region Public Methods
public void SetLayerMask(LayerMask layerMask)
{
layerMask &= ~RESERVED_OR_ILLEGAL_LAYERS;
_layerMask = layerMask;
}
public CVRRaycastResult GetRaycastResults(RaycastFlags flags = RaycastFlags.All)
{
// Early out if we don't want to do anything
if (flags == RaycastFlags.None) return default;
Ray ray = GetRayFromImpl();
CVRRaycastResult result = new();
// Always check COHTML first
if ((flags & RaycastFlags.CohtmlInteract) != 0
&& TryProcessCohtmlHit(ray, ref result))
return result;
// Check if there are pickups or interactables in immediate proximity
if ((flags & RaycastFlags.ProximityInteract) != 0)
{
ProcessProximityHits(ray, ref result); // TODO: Offset origin to center of palm based on hand type
if (result.isProximityHit)
return result;
}
// Check for regular raycast hits
if ((flags & RaycastFlags.RayInteract) != 0)
ProcessRaycastHits(ray, ref result);
// If we hit something, check for telepathic grab candidates at the hit point
if ((flags & RaycastFlags.TelepathicCandidate) != 0 && result.hit.collider)
ProcessTelepathicGrabCandidate(result.hit.point, ref result);
return result;
}
#endregion Public Methods
#region Private Methods
private static bool TryProcessCohtmlHit(Ray ray, ref CVRRaycastResult result)
{
CohtmlControlledView hitView = CohtmlViewInputHandler.Instance.RayToView(ray,
out float _, out Vector2 hitCoords);
if (hitView == null) return false;
result.hitCohtml = true;
result.hitCohtmlView = hitView;
result.hitCohtmlCoords = hitCoords;
// Manually check for pickups & interactables on the hit view (future-proofing for menu grabbing)
if (hitView.TryGetComponent(out _workingInteractable)) result.hitInteractable = _workingInteractable;
if (hitView.TryGetComponent(out _workingPickupable)) result.hitPickupable = _workingPickupable;
return true;
}
private void ProcessProximityHits(Ray ray, ref CVRRaycastResult result)
{
int proximityHits = Physics.SphereCastNonAlloc(
ray.origin,
RAYCAST_SPHERE_RADIUS,
Vector3.up,
_hits,
0.001f,
_layerMask,
_triggerInteraction
);
if (proximityHits <= 0) return;
Array.Sort(_hits, 0, proximityHits, _hitsComparer);
for (int i = 0; i < proximityHits; i++)
{
RaycastHit hit = _hits[i];
_workingCollider = hit.collider;
_workingGameObject = _workingCollider.gameObject;
// Skip things behind the ray origin
if (Vector3.Dot(ray.direction, hit.point - ray.origin) < 0)
continue;
// Check for interactables & pickupables in proximity
if (!TryProcessInteractables(hit, ref result))
continue;
result.isProximityHit = true;
break;
}
}
private void ProcessRaycastHits(Ray ray, ref CVRRaycastResult result)
{
// Get all hits including triggers, sorted by UI Internal layer & distance
int hitCount = Physics.RaycastNonAlloc(ray,
_hits,
MAX_RAYCAST_DISTANCE,
_layerMask,
_triggerInteraction);
if (hitCount <= 0) return;
Array.Sort(_hits, 0, hitCount, _hitsComparer);
for (int i = 0; i < hitCount; i++)
{
RaycastHit hit = _hits[i];
_workingCollider = hit.collider;
_workingGameObject = _workingCollider.gameObject;
// Special case where we only get the closest water hit position.
// As the array is sorted by distance, we only need to check if we didn't hit water yet.
if (!result.hitWater) TryProcessFluidVolume(hit, ref result);
// Check for hits in order of priority
if (TryProcessSelectable(hit, ref result))
break; // Hit a Unity UI Selectable (Button, Slider, etc.)
if (TryProcessCanvasElement(hit, ref result))
break; // Hit a Unity UI Canvas Element (ScrollRect, idk what else yet)
if (TryProcessInteractables(hit, ref result))
break; // Hit an in-range Interactable or Pickup
if (TryProcessWorldHit(hit, ref result))
break; // Hit a non-trigger collider (world, end of ray)
}
}
private void ProcessTelepathicGrabCandidate(Vector3 hitPoint, ref CVRRaycastResult result)
{
// If we already hit a pickupable, we don't need to check for telepathic grab candidates
if (result.hitPickupable)
{
result.hasTelepathicGrabCandidate = true;
result.telepathicPickupable = result.hitPickupable;
result.telepathicGrabPoint = hitPoint;
return;
}
// If the hit distance is too far, don't bother checking for telepathic grab candidates
if (Vector3.Distance(hitPoint, _rayOrigin.position) > MAX_TELEPATHIC_DISTANCE)
return;
// Check for mirror reflection triggers in a sphere around the hit point
int telepathicHits = Physics.SphereCastNonAlloc(
hitPoint,
TELEPATHIC_SPHERE_RADIUS,
Vector3.up,
_hits,
0.001f,
_telepathicLayerMask,
QueryTriggerInteraction.Collide
);
if (telepathicHits <= 0) return;
// Look for pickupable objects near our hit point
var nearestDistance = float.MaxValue;
for (int i = 0; i < telepathicHits; i++)
{
RaycastHit hit = _hits[i];
_workingCollider = hit.collider;
// _workingGameObject = _workingCollider.gameObject;
Transform parentTransform = _workingCollider.transform.parent;
if (!parentTransform
|| !parentTransform.TryGetComponent(out _workingPickupable)
|| !_workingPickupable.CanPickup)
continue;
var distance = Vector3.Distance(hitPoint, hit.point);
if (!(distance < nearestDistance))
continue;
result.hasTelepathicGrabCandidate = true;
result.telepathicPickupable = _workingPickupable;
result.telepathicGrabPoint = hitPoint;
nearestDistance = distance;
}
}
private static bool TryProcessSelectable(RaycastHit hit, ref CVRRaycastResult result)
{
if (!_workingGameObject.TryGetComponent(out _workingSelectable))
return false;
result.hitUnityUi = true;
result.hitSelectable = _workingSelectable;
result.hit = hit;
return true;
}
private static bool TryProcessCanvasElement(RaycastHit hit, ref CVRRaycastResult result)
{
if (!_workingGameObject.TryGetComponent(out _workingCanvasElement))
return false;
result.hitUnityUi = true;
result.hitCanvasElement = _workingCanvasElement;
result.hit = hit;
return true;
}
private static void TryProcessFluidVolume(RaycastHit hit, ref CVRRaycastResult result)
{
if (_workingGameObject.layer != CVRLayers.Water) return;
result.hitWater = true;
result.waterHit = hit;
}
private static bool TryProcessInteractables(RaycastHit hit, ref CVRRaycastResult result)
{
bool hitValidComponent = false;
if (_workingGameObject.TryGetComponent(out _workingInteractable)
&& _workingInteractable.CanInteract
&& IsCVRInteractableWithinRange(_workingInteractable, hit))
{
result.hitInteractable = _workingInteractable;
hitValidComponent = true;
}
if (_workingGameObject.TryGetComponent(out _workingPickupable)
&& _workingPickupable.CanPickup
&& IsCVRPickupableWithinRange(_workingPickupable, hit))
{
result.hitPickupable = _workingPickupable;
hitValidComponent = true;
}
if (!hitValidComponent)
return false;
result.hit = hit;
return true;
}
private static bool TryProcessWorldHit(RaycastHit hit, ref CVRRaycastResult result)
{
if (_workingCollider.isTrigger)
return false;
result.hitWorld = true;
result.hit = hit;
return true;
}
#endregion Private Methods
#region Protected Methods
protected abstract Ray GetRayFromImpl();
#endregion Protected Methods
#region Utility Because Original Methods Are Broken
private static bool IsCVRInteractableWithinRange(Interactable interactable, RaycastHit hit)
{
if (interactable is not CVRInteractable cvrInteractable)
return true;
foreach (CVRInteractableAction action in cvrInteractable.actions)
{
if (action.actionType
is not (CVRInteractableAction.ActionRegister.OnInteractDown
or CVRInteractableAction.ActionRegister.OnInteractUp
or CVRInteractableAction.ActionRegister.OnInputDown
or CVRInteractableAction.ActionRegister.OnInputUp))
continue;
float maxDistance = action.floatVal;
if (Mathf.Approximately(maxDistance, 0f)
|| hit.distance <= maxDistance)
return true; // Interactable is within range
}
return false;
}
private static bool IsCVRPickupableWithinRange(Pickupable pickupable, RaycastHit hit)
{
return hit.distance <= pickupable.MaxGrabDistance;
}
private static bool IsCVRCanvasWrapperWithinRange(CVRCanvasWrapper canvasWrapper, RaycastHit hit)
{
return hit.distance <= canvasWrapper.MaxInteractDistance;
}
#endregion Utility Because Original Methods Are Broken
}
}

View file

@ -0,0 +1,13 @@
using UnityEngine;
namespace ABI_RC.Core.Player.Interaction.RaycastImpl
{
public class CVRPlayerRaycasterMouse : CVRPlayerRaycaster
{
private readonly Camera _camera;
public CVRPlayerRaycasterMouse(Transform rayOrigin, Camera camera) : base(rayOrigin) { _camera = camera; }
protected override Ray GetRayFromImpl() => Cursor.lockState == CursorLockMode.Locked
? new Ray(_camera.transform.position, _camera.transform.forward)
: _camera.ScreenPointToRay(Input.mousePosition);
}
}

View file

@ -0,0 +1,10 @@
using UnityEngine;
namespace ABI_RC.Core.Player.Interaction.RaycastImpl
{
public class CVRPlayerRaycasterTransform : CVRPlayerRaycaster
{
public CVRPlayerRaycasterTransform(Transform rayOrigin) : base(rayOrigin) { }
protected override Ray GetRayFromImpl() => new(_rayOrigin.position, _rayOrigin.forward);
}
}

View file

@ -0,0 +1,38 @@
using ABI_RC.Core.InteractionSystem.Base;
using ABI_RC.Core.UI;
using NAK.SuperAwesomeMod.Components;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace ABI_RC.Core.Player.Interaction.RaycastImpl
{
public struct CVRRaycastResult
{
// Hit flags
public bool hitWorld; // Any non-specific collision
public bool hitWater; // Hit a fluid volume
public bool hitCohtml; // Specifically hit a COHTML view (Main/Quick Menu)
public bool isProximityHit; // Hit was from proximity sphere check
public bool hitUnityUi; // Hit a canvas
// Main raycast hit info
public RaycastHit hit;
public RaycastHit? waterHit; // Only valid if hitWater is true
// Specific hit components
public Pickupable hitPickupable;
public Interactable hitInteractable;
public Selectable hitSelectable;
public ICanvasElement hitCanvasElement;
// COHTML specific results
public CohtmlControlledView hitCohtmlView;
public Vector2 hitCohtmlCoords;
// Telepathic pickup
public bool hasTelepathicGrabCandidate;
public Pickupable telepathicPickupable;
public Vector3 telepathicGrabPoint;
}
}

View file

@ -0,0 +1,312 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace ABI_RC.Core.Player.Interaction.RaycastImpl
{
public class CVRRaycastDebugManager : MonoBehaviour
{
#region Singleton
private static CVRRaycastDebugManager _instance;
public static CVRRaycastDebugManager Instance => _instance;
public static void Initialize(Camera camera)
{
if (_instance != null) return;
var go = new GameObject("RaycastDebugManager");
_instance = go.AddComponent<CVRRaycastDebugManager>();
DontDestroyOnLoad(go);
_instance.Setup(camera);
}
#endregion
#region Private Fields
private CVRPlayerRaycasterMouse _raycaster;
private CVRRaycastResult _lastResult;
private System.Diagnostics.Stopwatch _stopwatch;
// Performance tracking
private const int ROLLING_AVERAGE_SAMPLES = 60; // 1 second at 60fps
private readonly float[] _timeHistory = new float[ROLLING_AVERAGE_SAMPLES];
private int _currentSampleIndex;
private float _lastRaycastTime;
private float _minRaycastTime = float.MaxValue;
private float _maxRaycastTime;
private float _rollingAverageTime;
private bool _historyFilled;
private const int DEBUG_PANEL_WIDTH = 300;
private const int DEBUG_PANEL_MARGIN = 10;
private const float MOUSE_CURSOR_SIZE = 24f;
private const float CURSOR_OFFSET = MOUSE_CURSOR_SIZE / 2f;
private GUIStyle _labelStyle;
private GUIStyle _headerStyle;
private GUIStyle _boxStyle;
private static readonly Color32 TIMING_COLOR = new(255, 255, 150, 255); // Yellow
private static readonly Color32 COHTML_COLOR = new(150, 255, 150, 255); // Green
private static readonly Color32 UI_COLOR = new(150, 150, 255, 255); // Blue
private static readonly Color32 UNITY_UI_COLOR = new(255, 200, 150, 255); // Orange
private static readonly Color32 INTERACT_COLOR = new(255, 150, 150, 255); // Red
private static readonly Color32 WATER_COLOR = new(150, 255, 255, 255); // Cyan
private static readonly Color32 TELEPATHIC_COLOR = new(255, 150, 255, 255);// Purple
private static readonly Color32 SELECTABLE_COLOR = new(200, 150, 255, 255);// Light Purple
#endregion
#region Setup
private void Setup(Camera camera)
{
_raycaster = new CVRPlayerRaycasterMouse(transform, camera);
_raycaster.SetLayerMask(Physics.DefaultRaycastLayers);
_stopwatch = new System.Diagnostics.Stopwatch();
}
#endregion
#region MonoBehaviour
private void Update()
{
_stopwatch.Restart();
_lastResult = _raycaster.GetRaycastResults();
_stopwatch.Stop();
UpdatePerformanceMetrics();
}
private void UpdatePerformanceMetrics()
{
// Calculate current frame time
_lastRaycastTime = _stopwatch.ElapsedTicks / (float)System.TimeSpan.TicksPerMillisecond;
// Update min/max
_minRaycastTime = Mathf.Min(_minRaycastTime, _lastRaycastTime);
_maxRaycastTime = Mathf.Max(_maxRaycastTime, _lastRaycastTime);
// Update rolling average
_timeHistory[_currentSampleIndex] = _lastRaycastTime;
// Calculate rolling average based on filled samples
float sum = 0f;
int sampleCount = _historyFilled ? ROLLING_AVERAGE_SAMPLES : _currentSampleIndex + 1;
for (int i = 0; i < sampleCount; i++)
sum += _timeHistory[i];
_rollingAverageTime = sum / sampleCount;
// Update index for next frame
_currentSampleIndex = (_currentSampleIndex + 1) % ROLLING_AVERAGE_SAMPLES;
if (_currentSampleIndex == 0)
_historyFilled = true;
}
private void OnGUI()
{
InitializeStyles();
DrawDebugPanel();
}
#endregion
#region Drawing Methods
private void InitializeStyles()
{
if (_labelStyle != null) return;
_labelStyle = new GUIStyle
{
normal = { textColor = Color.white },
fontSize = 12,
padding = new RectOffset(5, 5, 2, 2),
margin = new RectOffset(5, 5, 0, 0)
};
_headerStyle = new GUIStyle
{
normal = { textColor = Color.white },
fontSize = 14,
fontStyle = FontStyle.Bold,
padding = new RectOffset(5, 5, 5, 5),
margin = new RectOffset(5, 5, 5, 5)
};
_boxStyle = new GUIStyle(GUI.skin.box)
{
padding = new RectOffset(10, 10, 5, 5),
margin = new RectOffset(5, 5, 5, 5)
};
}
private void DrawDebugPanel()
{
var rect = new Rect(
Screen.width - DEBUG_PANEL_WIDTH - DEBUG_PANEL_MARGIN,
DEBUG_PANEL_MARGIN,
DEBUG_PANEL_WIDTH,
Screen.height - (DEBUG_PANEL_MARGIN * 2)
);
GUI.Box(rect, "");
GUILayout.BeginArea(rect);
GUI.backgroundColor = Color.black;
GUILayout.Label("Raycast Debug Info", _headerStyle);
DrawPerformanceSection();
DrawCohtmlSection();
DrawUnityUISection();
DrawSelectableSection();
DrawInteractionSection();
DrawWaterSection();
DrawTelepathicSection();
DrawWorldHitSection();
GUILayout.EndArea();
}
private void DrawPerformanceSection()
{
GUI.backgroundColor = TIMING_COLOR;
GUILayout.BeginVertical(_boxStyle);
GUILayout.Label("Performance", _headerStyle);
DrawLabel("Last Raycast", $"{_lastRaycastTime:F3} ms");
DrawLabel("Average (1s)", $"{_rollingAverageTime:F3} ms");
DrawLabel("Min", $"{_minRaycastTime:F3} ms");
DrawLabel("Max", $"{_maxRaycastTime:F3} ms");
GUILayout.EndVertical();
}
private void DrawCohtmlSection()
{
if (!_lastResult.hitCohtml) return;
GUI.backgroundColor = COHTML_COLOR;
GUILayout.BeginVertical(_boxStyle);
GUILayout.Label("COHTML Hit", _headerStyle);
DrawLabel("View", _lastResult.hitCohtmlView.name);
DrawLabel("Coords", _lastResult.hitCohtmlCoords.ToString());
GUILayout.EndVertical();
}
private void DrawUnityUISection()
{
if (!_lastResult.hitUnityUi || _lastResult.hitCanvasElement == null) return;
GUI.backgroundColor = UNITY_UI_COLOR;
GUILayout.BeginVertical(_boxStyle);
GUILayout.Label("Unity UI Hit", _headerStyle);
var canvasElement = _lastResult.hitCanvasElement;
var gameObject = canvasElement as MonoBehaviour;
DrawLabel("Canvas Element", gameObject != null ? gameObject.name : "Unknown");
DrawLabel("Element Type", canvasElement.GetType().Name);
if (gameObject != null)
{
DrawLabel("GameObject", gameObject.gameObject.name);
if (gameObject.transform.parent != null)
DrawLabel("Parent", gameObject.transform.parent.name);
}
GUILayout.EndVertical();
}
private void DrawSelectableSection()
{
if (_lastResult.hitSelectable == null) return;
GUI.backgroundColor = SELECTABLE_COLOR;
GUILayout.BeginVertical(_boxStyle);
GUILayout.Label("UI Selectable", _headerStyle);
DrawLabel("Selectable", _lastResult.hitSelectable.name);
DrawLabel("Selectable Type", _lastResult.hitSelectable.GetType().Name);
DrawLabel("Is Interactable", _lastResult.hitSelectable.interactable.ToString());
DrawLabel("Navigation Mode", _lastResult.hitSelectable.navigation.mode.ToString());
if (_lastResult.hitSelectable is Toggle toggle)
DrawLabel("Toggle State", toggle.isOn.ToString());
else if (_lastResult.hitSelectable is Slider slider)
DrawLabel("Slider Value", slider.value.ToString("F2"));
else if (_lastResult.hitSelectable is Scrollbar scrollbar)
DrawLabel("Scrollbar Value", scrollbar.value.ToString("F2"));
GUILayout.EndVertical();
}
private void DrawInteractionSection()
{
if (!_lastResult.hitPickupable && !_lastResult.hitInteractable) return;
GUI.backgroundColor = INTERACT_COLOR;
GUILayout.BeginVertical(_boxStyle);
GUILayout.Label("Interaction", _headerStyle);
if (_lastResult.hitPickupable)
DrawLabel("Pickupable", _lastResult.hitPickupable.name);
if (_lastResult.hitInteractable)
DrawLabel("Interactable", _lastResult.hitInteractable.name);
DrawLabel("Is Proximity", _lastResult.isProximityHit.ToString());
GUILayout.EndVertical();
}
private void DrawWaterSection()
{
if (!_lastResult.hitWater || !_lastResult.waterHit.HasValue) return;
GUI.backgroundColor = WATER_COLOR;
GUILayout.BeginVertical(_boxStyle);
GUILayout.Label("Water Surface", _headerStyle);
DrawLabel("Hit Point", _lastResult.waterHit.Value.point.ToString("F2"));
DrawLabel("Surface Normal", _lastResult.waterHit.Value.normal.ToString("F2"));
GUILayout.EndVertical();
}
private void DrawTelepathicSection()
{
if (!_lastResult.hasTelepathicGrabCandidate) return;
GUI.backgroundColor = TELEPATHIC_COLOR;
GUILayout.BeginVertical(_boxStyle);
GUILayout.Label("Telepathic Grab", _headerStyle);
DrawLabel("Target", _lastResult.telepathicPickupable.name);
DrawLabel("Grab Point", _lastResult.telepathicGrabPoint.ToString("F2"));
GUILayout.EndVertical();
}
private void DrawWorldHitSection()
{
if (_lastResult.hitCohtml ||
_lastResult.hitPickupable ||
_lastResult.hitInteractable ||
_lastResult.hitUnityUi ||
!_lastResult.hitWorld ||
_lastResult.hit.collider == null) return;
GUI.backgroundColor = Color.grey;
GUILayout.BeginVertical(_boxStyle);
GUILayout.Label("World Hit", _headerStyle);
DrawLabel("Object", _lastResult.hit.collider.name);
DrawLabel("Distance", _lastResult.hit.distance.ToString("F2"));
DrawLabel("Point", _lastResult.hit.point.ToString("F2"));
GUILayout.EndVertical();
}
private void DrawLabel(string label, string value)
{
GUILayout.Label($"{label}: {value}", _labelStyle);
}
#endregion
}
}

View file

@ -0,0 +1,81 @@
using System.Reflection;
using ABI_RC.Core.Base.Jobs;
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.Player;
using ABI_RC.Core.Player.Interaction.RaycastImpl;
using ABI_RC.Core.Util.AssetFiltering;
using ABI.CCK.Components;
using HarmonyLib;
using MelonLoader;
using NAK.SuperAwesomeMod.Components;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace NAK.SuperAwesomeMod;
public class SuperAwesomeModMod : MelonMod
{
#region Melon Events
public override void OnInitializeMelon()
{
HarmonyInstance.Patch(
typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.Start),
BindingFlags.NonPublic | BindingFlags.Instance),
postfix: new HarmonyMethod(typeof(SuperAwesomeModMod).GetMethod(nameof(OnPlayerSetupStart),
BindingFlags.NonPublic | BindingFlags.Static))
);
HarmonyInstance.Patch(
typeof(SceneLoaded).GetMethod(nameof(SceneLoaded.FilterWorldComponent),
BindingFlags.NonPublic | BindingFlags.Static),
postfix: new HarmonyMethod(typeof(SuperAwesomeModMod).GetMethod(nameof(OnShitLoaded),
BindingFlags.NonPublic | BindingFlags.Static))
);
// patch SharedFilter.ProcessCanvas
HarmonyInstance.Patch(
typeof(SharedFilter).GetMethod(nameof(SharedFilter.ProcessCanvas),
BindingFlags.Public | BindingFlags.Static),
postfix: new HarmonyMethod(typeof(SuperAwesomeModMod).GetMethod(nameof(OnProcessCanvas),
BindingFlags.NonPublic | BindingFlags.Static))
);
LoggerInstance.Msg("SuperAwesomeModMod! OnInitializeMelon! :D");
}
public override void OnApplicationQuit()
{
LoggerInstance.Msg("SuperAwesomeModMod! OnApplicationQuit! D:");
}
#endregion Melon Events
private static void OnPlayerSetupStart()
{
CVRRaycastDebugManager.Initialize(PlayerSetup.Instance.desktopCam);
}
private static void OnShitLoaded(Component c, List<Task> asyncTasks = null, Scene? scene = null)
{
if (c == null)
return;
if (c.gameObject == null)
return;
if (c.gameObject.scene.buildIndex > 0)
return;
if ((scene != null)
&& (c.gameObject.scene != scene))
return;
if (c is Canvas canvas) canvas.gameObject.AddComponent<CVRCanvasWrapper>();
}
private static void OnProcessCanvas(string collectionId, Canvas canvas)
{
canvas.gameObject.AddComponent<CVRCanvasWrapper>();
}
}

View file

@ -0,0 +1,32 @@
using MelonLoader;
using NAK.SuperAwesomeMod.Properties;
using System.Reflection;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.SuperAwesomeMod))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.SuperAwesomeMod))]
[assembly: MelonInfo(
typeof(NAK.SuperAwesomeMod.SuperAwesomeModMod),
nameof(NAK.SuperAwesomeMod),
AssemblyInfoParams.Version,
AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/SuperAwesomeMod"
)]
[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.SuperAwesomeMod.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.0";
public const string Author = "NotAKidoS";
}

View file

@ -0,0 +1,54 @@
# ASTExtension
Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool):
- VR Gesture to scale
- Persistent height
- Copy height from others
Best used with Avatar Scale Tool, but will attempt to work with found scaling setups.
Requires already having Avatar Scaling on the avatar. This is **not** Universal Scaling.
## Supported Setups
ASTExtension will attempt to work with the following setups:
**Parameter Names:**
- AvatarScale
- Scale
- Scaler
- Scale/Scale
- Height
- LoliModifier
- AvatarSize
- Size
- SizeScale
- Scaling
These parameter names are not case sensitive and have been gathered from polling the community for common parameter names.
Assuming the parameter is a float, ASTExtension will attempt to use it as the height parameter. Will automatically calibrate to the height range of the found parameter, assuming the scaling animation is in a blend tree / state using motion time & is linear. The scaling animation state **must be active** at time of avatar load.
The max value ASTExtension will drive the parameter to is 100. As the mod is having to guess the max height, it may not be accurate if the max height is not capped at a multiple of 10.
Examples:
- `AvatarScale` - 0 to 1 (slider)
- This is the default setup for Avatar Scale Tool and will work perfectly.
- `Scale` - 0 to 100 (input single)
- This will also work perfectly as the max height is a multiple of 10.
- `Height` - 0 to 2 (input single)
- This will not work properly. The max value to drive the parameter to is not a multiple of 10, and as such ASTExtension will believe the parameter range is 0 to 1.
- `BurntToast` - 0 to 10 (input single)
- This will not work properly. The parameter name is not recognized by ASTExtension.
If your setup is theoretically supported but not working, it is likely the scaling animation is not linear or has loop enabled if using Motion Time, making the first and last frame identical height. In this case, you will need to fix your animation clip curves / blend tree to be linear &|| not loop, or use Avatar Scale Tool to generate a new scaling animation.
---
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.

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>ASTExtension</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="BTKUILib">
<HintPath>..\.ManagedLibs\BTKUILib.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,24 @@
{
"_id": 223,
"name": "ASTExtension",
"modversion": "1.0.2",
"gameversion": "2025r178",
"loaderversion": "0.6.1",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool):\n- VR Gesture to scale\n- Persistent height\n- Copy height from others\n\nBest used with Avatar Scale Tool, but will attempt to work with found scaling setups.\nRequires already having Avatar Scaling on the avatar. This is **not** Universal Scaling.",
"searchtags": [
"tool",
"scaling",
"height",
"extension",
"avatar"
],
"requirements": [
"BTKUILib"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r45/ASTExtension.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ASTExtension/",
"changelog": "- Fixes for 2025r178",
"embedcolor": "#f61963"
}