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,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
}
}