From 3660b8f68349f7f7cd4beff1148c80c2fd017791 Mon Sep 17 00:00:00 2001
From: NotAKidoS <37721153+NotAKidoS@users.noreply.github.com>
Date: Wed, 29 Jan 2025 16:37:15 -0600
Subject: [PATCH] AvatarCloneTest: push so i can reference in an email
---
.../AvatarClone/AvatarClone.API.cs | 68 +++++++
.../AvatarClone/AvatarClone.Clones.cs | 103 ++++++++++
.../AvatarClone/AvatarClone.Exclusion.cs | 141 ++++++++++++++
.../AvatarClone/AvatarClone.Fields.cs | 67 +++++++
.../AvatarClone/AvatarClone.Init.cs | 128 +++++++++++++
.../AvatarClone/AvatarClone.MagicaSupport.cs | 34 ++++
.../AvatarClone/AvatarClone.Update.cs | 181 ++++++++++++++++++
.../AvatarClone/AvatarClone.Util.cs | 20 ++
AvatarCloneTest/AvatarClone/AvatarClone.cs | 134 +++++++++++++
.../FPRExclusion/AvatarCloneExclusion.cs | 38 ++++
AvatarCloneTest/AvatarCloneTest.csproj | 9 +
AvatarCloneTest/Main.cs | 72 +++++++
AvatarCloneTest/Patches.cs | 87 +++++++++
AvatarCloneTest/Properties/AssemblyInfo.cs | 32 ++++
AvatarCloneTest/README.md | 18 ++
AvatarCloneTest/format.json | 23 +++
16 files changed, 1155 insertions(+)
create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.API.cs
create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Clones.cs
create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Exclusion.cs
create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Fields.cs
create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Init.cs
create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.MagicaSupport.cs
create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Update.cs
create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Util.cs
create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.cs
create mode 100644 AvatarCloneTest/AvatarClone/FPRExclusion/AvatarCloneExclusion.cs
create mode 100644 AvatarCloneTest/AvatarCloneTest.csproj
create mode 100644 AvatarCloneTest/Main.cs
create mode 100644 AvatarCloneTest/Patches.cs
create mode 100644 AvatarCloneTest/Properties/AssemblyInfo.cs
create mode 100644 AvatarCloneTest/README.md
create mode 100644 AvatarCloneTest/format.json
diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.API.cs b/AvatarCloneTest/AvatarClone/AvatarClone.API.cs
new file mode 100644
index 0000000..89835bd
--- /dev/null
+++ b/AvatarCloneTest/AvatarClone/AvatarClone.API.cs
@@ -0,0 +1,68 @@
+using UnityEngine;
+
+namespace NAK.AvatarCloneTest;
+
+public partial class AvatarClone
+{
+ #region Public API Methods
+
+ ///
+ /// Sets whether the specific renderer requires additional runtime checks when copying to the clone.
+ /// For example, Magica Cloth modifies the sharedMesh & bones of the renderer at runtime. This is not needed
+ /// for most renderers, so copying for all renderers would be inefficient.
+ ///
+ public void SetRendererNeedsAdditionalChecks(Renderer rend, bool needsChecks)
+ {
+ switch (rend)
+ {
+ case MeshRenderer meshRenderer:
+ {
+ int index = _standardRenderers.IndexOf(meshRenderer);
+ if (index == -1) return;
+
+ if (needsChecks && !_standardRenderersNeedingChecks.Contains(index))
+ {
+ int insertIndex = _standardRenderersNeedingChecks.Count;
+ _standardRenderersNeedingChecks.Add(index);
+ _cachedSharedMeshes.Insert(insertIndex, null);
+ }
+ else if (!needsChecks)
+ {
+ int removeIndex = _standardRenderersNeedingChecks.IndexOf(index);
+ if (removeIndex != -1)
+ {
+ _standardRenderersNeedingChecks.RemoveAt(removeIndex);
+ _cachedSharedMeshes.RemoveAt(removeIndex);
+ }
+ }
+ return;
+ }
+ case SkinnedMeshRenderer skinnedRenderer:
+ {
+ int index = _skinnedRenderers.IndexOf(skinnedRenderer);
+ if (index == -1) return;
+
+ if (needsChecks && !_skinnedRenderersNeedingChecks.Contains(index))
+ {
+ int insertIndex = _skinnedRenderersNeedingChecks.Count;
+ _skinnedRenderersNeedingChecks.Add(index);
+ _cachedSharedMeshes.Insert(_standardRenderersNeedingChecks.Count + insertIndex, null);
+ _cachedSkinnedBoneCounts.Add(0);
+ }
+ else if (!needsChecks)
+ {
+ int removeIndex = _skinnedRenderersNeedingChecks.IndexOf(index);
+ if (removeIndex != -1)
+ {
+ _skinnedRenderersNeedingChecks.RemoveAt(removeIndex);
+ _cachedSharedMeshes.RemoveAt(_standardRenderersNeedingChecks.Count + removeIndex);
+ _cachedSkinnedBoneCounts.RemoveAt(removeIndex);
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ #endregion Public API Methods
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Clones.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Clones.cs
new file mode 100644
index 0000000..8fb54ec
--- /dev/null
+++ b/AvatarCloneTest/AvatarClone/AvatarClone.Clones.cs
@@ -0,0 +1,103 @@
+using ABI_RC.Core;
+using UnityEngine;
+using UnityEngine.Rendering;
+
+namespace NAK.AvatarCloneTest;
+
+public partial class AvatarClone
+{
+ #region Clone Creation
+
+ private void CreateClones()
+ {
+ int standardCount = _standardRenderers.Count;
+ _standardClones = new List(standardCount);
+ _standardCloneFilters = new List(standardCount);
+ for (int i = 0; i < standardCount; i++) CreateStandardClone(i);
+
+ int skinnedCount = _skinnedRenderers.Count;
+ _skinnedClones = new List(skinnedCount);
+ for (int i = 0; i < skinnedCount; i++) CreateSkinnedClone(i);
+ }
+
+ private void CreateStandardClone(int index)
+ {
+ MeshRenderer sourceRenderer = _standardRenderers[index];
+ MeshFilter sourceFilter = _standardFilters[index];
+
+ GameObject go = new(sourceRenderer.name + "_VisualClone")
+ {
+ layer = CVRLayers.PlayerClone
+ };
+
+ go.transform.SetParent(sourceRenderer.transform, false);
+ //go.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
+
+ MeshRenderer cloneRenderer = go.AddComponent();
+ MeshFilter cloneFilter = go.AddComponent();
+
+ // Initial setup
+ cloneRenderer.sharedMaterials = sourceRenderer.sharedMaterials;
+ cloneRenderer.shadowCastingMode = ShadowCastingMode.Off;
+ cloneRenderer.probeAnchor = sourceRenderer.probeAnchor;
+ cloneRenderer.localBounds = new Bounds(Vector3.zero, Vector3.positiveInfinity);
+ cloneFilter.sharedMesh = sourceFilter.sharedMesh;
+
+ // Optimizations to enforce
+ cloneRenderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
+ cloneRenderer.allowOcclusionWhenDynamic = false;
+
+ // Optimizations to enforce
+ sourceRenderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
+ sourceRenderer.allowOcclusionWhenDynamic = false;
+
+ _standardClones.Add(cloneRenderer);
+ _standardCloneFilters.Add(cloneFilter);
+ }
+
+ private void CreateSkinnedClone(int index)
+ {
+ SkinnedMeshRenderer source = _skinnedRenderers[index];
+
+ GameObject go = new(source.name + "_VisualClone")
+ {
+ layer = CVRLayers.PlayerClone
+ };
+
+ go.transform.SetParent(source.transform, false);
+ //go.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
+
+ SkinnedMeshRenderer clone = go.AddComponent();
+
+ // Initial setup
+ clone.sharedMaterials = source.sharedMaterials;
+ clone.shadowCastingMode = ShadowCastingMode.Off;
+ clone.probeAnchor = source.probeAnchor;
+ clone.localBounds = new Bounds(Vector3.zero, Vector3.positiveInfinity);
+ clone.sharedMesh = source.sharedMesh;
+ clone.rootBone = source.rootBone;
+ clone.bones = source.bones;
+ clone.quality = source.quality;
+ clone.updateWhenOffscreen = source.updateWhenOffscreen;
+
+ // Optimizations to enforce
+ clone.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
+ clone.allowOcclusionWhenDynamic = false;
+ clone.updateWhenOffscreen = false;
+ clone.skinnedMotionVectors = false;
+ clone.forceMatrixRecalculationPerRender = false;
+ clone.quality = SkinQuality.Bone4;
+
+ // Optimizations to enforce
+ source.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
+ source.allowOcclusionWhenDynamic = false;
+ source.updateWhenOffscreen = false;
+ source.skinnedMotionVectors = false;
+ source.forceMatrixRecalculationPerRender = false;
+ source.quality = SkinQuality.Bone4;
+
+ _skinnedClones.Add(clone);
+ }
+
+ #endregion Clone Creation
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Exclusion.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Exclusion.cs
new file mode 100644
index 0000000..d5136e3
--- /dev/null
+++ b/AvatarCloneTest/AvatarClone/AvatarClone.Exclusion.cs
@@ -0,0 +1,141 @@
+using ABI.CCK.Components;
+using UnityEngine;
+
+namespace NAK.AvatarCloneTest;
+
+public partial class AvatarClone
+{
+ private readonly Dictionary> _exclusionDirectRenderers = new();
+ private readonly Dictionary> _exclusionControlledBones = new();
+
+ private void InitializeExclusions()
+ {
+ // Add head exclusion for humanoid avatars if not present
+ var animator = GetComponent();
+ if (animator != null && animator.isHuman)
+ {
+ var headBone = animator.GetBoneTransform(HumanBodyBones.Head);
+ if (headBone != null && headBone.GetComponent() == null)
+ {
+ var exclusion = headBone.gameObject.AddComponent();
+ exclusion.isShown = false;
+ exclusion.target = headBone;
+ exclusion.shrinkToZero = true;
+ }
+ }
+
+ // Process existing exclusions bottom-up
+ var exclusions = GetComponentsInChildren(true);
+
+ for (int i = exclusions.Length - 1; i >= 0; i--)
+ {
+ var exclusion = exclusions[i];
+ if (exclusion.target == null)
+ exclusion.target = exclusion.transform;
+
+ // Skip invalid exclusions or already processed targets
+ if (exclusion.target == null || _exclusionDirectRenderers.ContainsKey(exclusion.target))
+ {
+ Destroy(exclusion);
+ continue;
+ }
+
+ // Initialize data for this exclusion
+ _exclusionDirectRenderers[exclusion.target] = new HashSet();
+ _exclusionControlledBones[exclusion.target] = new HashSet();
+
+ // Set up our behaviour
+ exclusion.behaviour = new AvatarCloneExclusion(this, exclusion.target);
+
+ // Collect affected renderers and bones
+ CollectExclusionData(exclusion.target);
+
+ // Initial update
+ exclusion.UpdateExclusions();
+ }
+ }
+
+ private void CollectExclusionData(Transform target)
+ {
+ var stack = new Stack();
+ stack.Push(target);
+
+ while (stack.Count > 0)
+ {
+ var current = stack.Pop();
+
+ // Skip if this transform belongs to another exclusion
+ if (current != target && current.GetComponent() != null)
+ continue;
+
+ _exclusionControlledBones[target].Add(current);
+
+ // Add renderers that will need their clone visibility toggled
+ foreach (var renderer in current.GetComponents())
+ {
+ // Find corresponding clone renderer
+ if (renderer is MeshRenderer meshRenderer)
+ {
+ int index = _standardRenderers.IndexOf(meshRenderer);
+ if (index != -1)
+ _exclusionDirectRenderers[target].Add(_standardClones[index]);
+ }
+ else if (renderer is SkinnedMeshRenderer skinnedRenderer)
+ {
+ int index = _skinnedRenderers.IndexOf(skinnedRenderer);
+ if (index != -1)
+ _exclusionDirectRenderers[target].Add(_skinnedClones[index]);
+ }
+ }
+
+ // Add children to stack
+ foreach (Transform child in current)
+ {
+ stack.Push(child);
+ }
+ }
+ }
+
+ public void HandleExclusionUpdate(Transform target, Transform shrinkBone, bool isShown)
+ {
+ if (!_exclusionDirectRenderers.TryGetValue(target, out var directCloneRenderers) ||
+ !_exclusionControlledBones.TryGetValue(target, out var controlledBones))
+ return;
+
+ // Handle direct clone renderers
+ foreach (var cloneRenderer in directCloneRenderers)
+ {
+ cloneRenderer.enabled = isShown;
+ }
+
+ // Update bone references in clone renderers
+ int cloneCount = _skinnedClones.Count;
+ var cloneRenderers = _skinnedClones;
+ var sourceRenderers = _skinnedRenderers;
+
+ for (int i = 0; i < cloneCount; i++)
+ {
+ var clone = cloneRenderers[i];
+ var source = sourceRenderers[i];
+ var sourceBones = source.bones;
+ var cloneBones = clone.bones;
+ int boneCount = cloneBones.Length;
+ bool needsUpdate = false;
+
+ for (int j = 0; j < boneCount; j++)
+ {
+ // Check if this bone is in our controlled set
+ if (controlledBones.Contains(sourceBones[j]))
+ {
+ cloneBones[j] = isShown ? sourceBones[j] : shrinkBone;
+ needsUpdate = true;
+ }
+ }
+
+ if (needsUpdate)
+ {
+ clone.bones = cloneBones;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Fields.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Fields.cs
new file mode 100644
index 0000000..0ba69b0
--- /dev/null
+++ b/AvatarCloneTest/AvatarClone/AvatarClone.Fields.cs
@@ -0,0 +1,67 @@
+using UnityEngine;
+
+namespace NAK.AvatarCloneTest;
+
+public partial class AvatarClone
+{
+ #region Profile Markers
+//#if UNITY_EDITOR
+ private static readonly UnityEngine.Profiling.CustomSampler s_CopyMaterials =
+ UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.CopyMaterials");
+ private static readonly UnityEngine.Profiling.CustomSampler s_CopyBlendShapes =
+ UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.CopyBlendShapes");
+ private static readonly UnityEngine.Profiling.CustomSampler s_CopyMeshes =
+ UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.CopyMeshes");
+
+ private static readonly UnityEngine.Profiling.CustomSampler s_MyOnPreRender =
+ UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.MyOnPreRender");
+ private static readonly UnityEngine.Profiling.CustomSampler s_SetShadowsOnly =
+ UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.SetShadowsOnly");
+ private static readonly UnityEngine.Profiling.CustomSampler s_UndoShadowsOnly =
+ UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.UndoShadowsOnly");
+ private static readonly UnityEngine.Profiling.CustomSampler s_SetUiCulling =
+ UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.SetUiCulling");
+ private static readonly UnityEngine.Profiling.CustomSampler s_UndoUiCulling =
+ UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.UndoUiCulling");
+
+//#endif
+ #endregion Profile Markers
+
+ #region Source Renderers
+ private List _standardRenderers;
+ private List _standardFilters;
+ private List _skinnedRenderers;
+ private List _allSourceRenderers; // For shadow casting only
+ #endregion Source Renderers
+
+ #region Clone Renderers
+ private List _standardClones;
+ private List _standardCloneFilters;
+ private List _skinnedClones;
+ #endregion Clone Renderers
+
+ #region Dynamic Check Lists
+ private List _standardRenderersNeedingChecks; // Stores indices into _standardRenderers
+ private List _skinnedRenderersNeedingChecks; // Stores indices into _skinnedRenderers
+ private List _cachedSkinnedBoneCounts; // So we don't copy the bones unless they've changed
+ private List _cachedSharedMeshes; // So we don't copy the mesh unless it's changed
+ #endregion Dynamic Check Lists
+
+ #region Material Data
+ private List _localMaterials;
+ private List _cullingMaterials;
+ private List _mainMaterials;
+ private MaterialPropertyBlock _propertyBlock;
+ #endregion Material Data
+
+ #region Blend Shape Data
+ private List> _blendShapeWeights;
+ #endregion Blend Shape Data
+
+ #region Shadow and UI Culling Settings
+ private bool _uiCullingActive;
+ private bool _shadowsOnlyActive;
+ private bool[] _originallyHadShadows;
+ private bool[] _originallyWasEnabled;
+ #endregion Shadow and UI Culling Settings
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Init.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Init.cs
new file mode 100644
index 0000000..41bd84b
--- /dev/null
+++ b/AvatarCloneTest/AvatarClone/AvatarClone.Init.cs
@@ -0,0 +1,128 @@
+using ABI_RC.Core.Player.ShadowClone;
+using UnityEngine;
+
+namespace NAK.AvatarCloneTest;
+
+public partial class AvatarClone
+{
+ #region Initialization
+
+ private void InitializeCollections()
+ {
+ _standardRenderers = new List();
+ _standardFilters = new List();
+ _skinnedRenderers = new List();
+ _allSourceRenderers = new List();
+
+ _standardClones = new List();
+ _standardCloneFilters = new List();
+ _skinnedClones = new List();
+
+ _standardRenderersNeedingChecks = new List();
+ _skinnedRenderersNeedingChecks = new List();
+ _cachedSkinnedBoneCounts = new List();
+ _cachedSharedMeshes = new List();
+
+ _localMaterials = new List();
+ _cullingMaterials = new List();
+ _mainMaterials = new List();
+ _propertyBlock = new MaterialPropertyBlock();
+
+ _blendShapeWeights = new List>();
+ }
+
+ private void InitializeRenderers()
+ {
+ var renderers = GetComponentsInChildren(true);
+
+ // Pre-size lists based on found renderers
+ // _standardRenderers.Capacity = renderers.Length;
+ // _standardFilters.Capacity = renderers.Length;
+ // _skinnedRenderers.Capacity = renderers.Length;
+ // _allSourceRenderers.Capacity = renderers.Length;
+
+ // Sort renderers into their respective lists
+ foreach (Renderer render in renderers)
+ {
+ _allSourceRenderers.Add(render);
+
+ switch (render)
+ {
+ case MeshRenderer meshRenderer:
+ {
+ MeshFilter filter = meshRenderer.GetComponent();
+ if (filter != null && filter.sharedMesh != null)
+ {
+ _standardRenderers.Add(meshRenderer);
+ _standardFilters.Add(filter);
+ }
+ break;
+ }
+ case SkinnedMeshRenderer skinnedRenderer:
+ {
+ if (skinnedRenderer.sharedMesh != null) _skinnedRenderers.Add(skinnedRenderer);
+ break;
+ }
+ }
+ }
+ }
+
+ private void SetupMaterialsAndBlendShapes()
+ {
+ // Cache counts
+ int standardCount = _standardRenderers.Count;
+ int skinnedCount = _skinnedRenderers.Count;
+ var standardRenderers = _standardRenderers;
+ var skinnedRenderers = _skinnedRenderers;
+ var localMats = _localMaterials;
+ var cullingMats = _cullingMaterials;
+ var blendWeights = _blendShapeWeights;
+
+ // Setup standard renderer materials
+ for (int i = 0; i < standardCount; i++)
+ {
+ MeshRenderer render = standardRenderers[i];
+ int matCount = render.sharedMaterials.Length;
+
+ // Local materials array
+ var localMatArray = new Material[matCount];
+ for (int j = 0; j < matCount; j++) localMatArray[j] = render.sharedMaterials[j];
+ localMats.Add(localMatArray);
+
+ // Culling materials array
+ var cullingMatArray = new Material[matCount];
+ for (int j = 0; j < matCount; j++) cullingMatArray[j] = ShadowCloneUtils.cullingMaterial;
+ cullingMats.Add(cullingMatArray);
+ }
+
+ // Setup skinned renderer materials and blend shapes
+ for (int i = 0; i < skinnedCount; i++)
+ {
+ SkinnedMeshRenderer render = skinnedRenderers[i];
+ int matCount = render.sharedMaterials.Length;
+
+ // Local materials array
+ var localMatArray = new Material[matCount];
+ for (int j = 0; j < matCount; j++) localMatArray[j] = render.sharedMaterials[j];
+ localMats.Add(localMatArray);
+
+ // Culling materials array
+ var cullingMatArray = new Material[matCount];
+ for (int j = 0; j < matCount; j++) cullingMatArray[j] = ShadowCloneUtils.cullingMaterial;
+ cullingMats.Add(cullingMatArray);
+
+ // Blend shape weights
+ int blendShapeCount = render.sharedMesh.blendShapeCount;
+ var weights = new List(blendShapeCount);
+ for (int j = 0; j < blendShapeCount; j++) weights.Add(0f);
+ blendWeights.Add(weights);
+ }
+
+ // Initialize renderer state arrays
+ int totalRenderers = _allSourceRenderers.Count;
+ _originallyHadShadows = new bool[totalRenderers];
+ _originallyWasEnabled = new bool[totalRenderers];
+ }
+
+ #endregion Initialization
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.MagicaSupport.cs b/AvatarCloneTest/AvatarClone/AvatarClone.MagicaSupport.cs
new file mode 100644
index 0000000..7ae5855
--- /dev/null
+++ b/AvatarCloneTest/AvatarClone/AvatarClone.MagicaSupport.cs
@@ -0,0 +1,34 @@
+using MagicaCloth;
+using MagicaCloth2;
+using UnityEngine;
+
+namespace NAK.AvatarCloneTest;
+
+public partial class AvatarClone
+{
+ #region Magica Cloth Support
+
+ private void SetupMagicaClothSupport()
+ {
+ var magicaCloths1 = GetComponentsInChildren(true);
+ foreach (MagicaRenderDeformer magicaCloth in magicaCloths1)
+ {
+ // Get the renderer on the same object
+ Renderer renderer = magicaCloth.gameObject.GetComponent();
+ SetRendererNeedsAdditionalChecks(renderer, true);
+ }
+
+ var magicaCloths2 = GetComponentsInChildren(true);
+ foreach (MagicaCloth2.MagicaCloth magicaCloth in magicaCloths2)
+ {
+ if (magicaCloth.serializeData.clothType != ClothProcess.ClothType.MeshCloth)
+ continue; // Only matters for cloth physics
+
+ // Set the affected renderers as requiring extra checks
+ var renderers = magicaCloth.serializeData.sourceRenderers;
+ foreach (Renderer renderer in renderers) SetRendererNeedsAdditionalChecks(renderer, true);
+ }
+ }
+
+ #endregion Magica Cloth Support
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Update.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Update.cs
new file mode 100644
index 0000000..42cef73
--- /dev/null
+++ b/AvatarCloneTest/AvatarClone/AvatarClone.Update.cs
@@ -0,0 +1,181 @@
+using UnityEngine;
+
+namespace NAK.AvatarCloneTest;
+
+public partial class AvatarClone
+{
+ #region Update Methods
+
+ private void UpdateStandardRenderers()
+ {
+ int count = _standardRenderers.Count;
+ var sourceRenderers = _standardRenderers;
+ var cloneRenderers = _standardClones;
+ var localMats = _localMaterials;
+
+ for (int i = 0; i < count; i++)
+ {
+ if (!IsRendererValid(sourceRenderers[i])) continue;
+ CopyMaterialsAndProperties(
+ sourceRenderers[i],
+ cloneRenderers[i],
+ _propertyBlock,
+ _mainMaterials,
+ localMats[i]);
+ }
+ }
+
+ private void UpdateSkinnedRenderers()
+ {
+ int standardCount = _standardRenderers.Count;
+ int count = _skinnedRenderers.Count;
+ var sourceRenderers = _skinnedRenderers;
+ var cloneRenderers = _skinnedClones;
+ var localMats = _localMaterials;
+ var blendWeights = _blendShapeWeights;
+
+ for (int i = 0; i < count; i++)
+ {
+ SkinnedMeshRenderer source = sourceRenderers[i];
+ if (!IsRendererValid(source)) continue;
+
+ SkinnedMeshRenderer clone = cloneRenderers[i];
+ CopyMaterialsAndProperties(
+ source,
+ clone,
+ _propertyBlock,
+ _mainMaterials,
+ localMats[i + standardCount]);
+
+ CopyBlendShapes(source, clone, blendWeights[i]);
+ }
+ }
+
+ private void UpdateStandardRenderersWithChecks()
+ {
+ s_CopyMeshes.Begin();
+
+ var cloneFilters = _standardCloneFilters;
+ var sourceFilters = _standardFilters;
+ var cachedMeshes = _cachedSharedMeshes;
+ var checkIndices = _standardRenderersNeedingChecks;
+ int checkCount = checkIndices.Count;
+
+ while (cachedMeshes.Count < checkCount) cachedMeshes.Add(null);
+
+ for (int i = 0; i < checkCount; i++)
+ {
+ int rendererIndex = checkIndices[i];
+ Mesh newMesh = sourceFilters[rendererIndex].sharedMesh;
+ if (ReferenceEquals(newMesh, cachedMeshes[i])) continue;
+ cloneFilters[rendererIndex].sharedMesh = newMesh; // expensive & allocates
+ cachedMeshes[i] = newMesh;
+ }
+
+ s_CopyMeshes.End();
+ }
+
+ private void UpdateSkinnedRenderersWithChecks()
+ {
+ s_CopyMeshes.Begin();
+
+ var sourceRenderers = _skinnedRenderers;
+ var cloneRenderers = _skinnedClones;
+ var cachedMeshes = _cachedSharedMeshes;
+ var cachedBoneCounts = _cachedSkinnedBoneCounts;
+ var checkIndices = _skinnedRenderersNeedingChecks;
+ int checkCount = checkIndices.Count;
+ int meshOffset = _standardRenderersNeedingChecks.Count;
+
+ // Ensure cache lists are properly sized
+ while (cachedMeshes.Count < meshOffset + checkCount) cachedMeshes.Add(null);
+ while (cachedBoneCounts.Count < checkCount) cachedBoneCounts.Add(0);
+
+ for (int i = 0; i < checkCount; i++)
+ {
+ int rendererIndex = checkIndices[i];
+ SkinnedMeshRenderer source = sourceRenderers[rendererIndex];
+ SkinnedMeshRenderer clone = cloneRenderers[rendererIndex];
+
+ // Check mesh changes
+ Mesh newMesh = source.sharedMesh; // expensive & allocates
+ if (!ReferenceEquals(newMesh, cachedMeshes[meshOffset + i]))
+ {
+ clone.sharedMesh = newMesh;
+ cachedMeshes[meshOffset + i] = newMesh;
+ }
+
+ // Check bone changes
+ var sourceBones = source.bones;
+ int newBoneCount = sourceBones.Length;
+ int oldBoneCount = cachedBoneCounts[i];
+ if (newBoneCount == oldBoneCount)
+ continue;
+
+ var cloneBones = clone.bones; // expensive & allocates
+ if (newBoneCount > oldBoneCount)
+ {
+ // Resize array and copy only the new bones (Magica Cloth appends bones when enabling Mesh Cloth)
+ Array.Resize(ref cloneBones, newBoneCount);
+ for (int boneIndex = oldBoneCount; boneIndex < newBoneCount; boneIndex++)
+ cloneBones[boneIndex] = sourceBones[boneIndex];
+ clone.bones = cloneBones;
+ }
+ else
+ {
+ // If shrinking, just set the whole array
+ clone.bones = sourceBones;
+ }
+
+ cachedBoneCounts[i] = newBoneCount;
+ }
+
+ s_CopyMeshes.End();
+ }
+
+ private static void CopyMaterialsAndProperties(
+ Renderer source, Renderer clone,
+ MaterialPropertyBlock propertyBlock,
+ List mainMaterials,
+ Material[] localMaterials)
+ {
+ s_CopyMaterials.Begin();
+
+ source.GetSharedMaterials(mainMaterials);
+
+ int matCount = mainMaterials.Count;
+ bool hasChanged = false;
+ for (var i = 0; i < matCount; i++)
+ {
+ if (ReferenceEquals(mainMaterials[i], localMaterials[i])) continue;
+ localMaterials[i] = mainMaterials[i];
+ hasChanged = true;
+ }
+ if (hasChanged) clone.sharedMaterials = localMaterials;
+
+ source.GetPropertyBlock(propertyBlock);
+ clone.SetPropertyBlock(propertyBlock);
+
+ s_CopyMaterials.End();
+ }
+
+ private static void CopyBlendShapes(
+ SkinnedMeshRenderer source,
+ SkinnedMeshRenderer target,
+ List blendShapeWeights)
+ {
+ s_CopyBlendShapes.Begin();
+
+ int weightCount = blendShapeWeights.Count;
+ for (var i = 0; i < weightCount; i++)
+ {
+ var weight = source.GetBlendShapeWeight(i);
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if (weight == blendShapeWeights[i]) continue; // Halves the work
+ target.SetBlendShapeWeight(i, blendShapeWeights[i] = weight);
+ }
+
+ s_CopyBlendShapes.End();
+ }
+ #endregion Update Methods
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Util.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Util.cs
new file mode 100644
index 0000000..c6fb033
--- /dev/null
+++ b/AvatarCloneTest/AvatarClone/AvatarClone.Util.cs
@@ -0,0 +1,20 @@
+using ABI_RC.Core;
+using ABI_RC.Core.Player;
+using UnityEngine;
+
+namespace NAK.AvatarCloneTest;
+
+public partial class AvatarClone
+{
+ private static bool CameraRendersPlayerLocalLayer(Camera cam)
+ => (cam.cullingMask & (1 << CVRLayers.PlayerLocal)) != 0;
+
+ private static bool CameraRendersPlayerCloneLayer(Camera cam)
+ => (cam.cullingMask & (1 << CVRLayers.PlayerClone)) != 0;
+
+ private static bool IsUIInternalCamera(Camera cam)
+ => cam == PlayerSetup.Instance.activeUiCam;
+
+ private static bool IsRendererValid(Renderer renderer)
+ => renderer && renderer.gameObject.activeInHierarchy;
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.cs b/AvatarCloneTest/AvatarClone/AvatarClone.cs
new file mode 100644
index 0000000..88c8112
--- /dev/null
+++ b/AvatarCloneTest/AvatarClone/AvatarClone.cs
@@ -0,0 +1,134 @@
+using UnityEngine;
+using UnityEngine.Rendering;
+
+namespace NAK.AvatarCloneTest;
+
+public partial class AvatarClone : MonoBehaviour
+{
+ #region Unity Events
+
+ private void Start()
+ {
+ InitializeCollections();
+ InitializeRenderers();
+ SetupMaterialsAndBlendShapes();
+ CreateClones();
+
+ InitializeExclusions();
+ SetupMagicaClothSupport();
+
+ Camera.onPreCull += MyOnPreRender;
+ }
+
+ private void OnDestroy()
+ {
+ Camera.onPreCull -= MyOnPreRender;
+ }
+
+ private void LateUpdate()
+ {
+ // Update all renderers with basic properties (Materials & BlendShapes)
+ UpdateStandardRenderers();
+ UpdateSkinnedRenderers();
+
+ // Additional pass for renderers needing extra checks (Shared Mesh & Bone Changes)
+ UpdateStandardRenderersWithChecks();
+ UpdateSkinnedRenderersWithChecks();
+ }
+
+ private void MyOnPreRender(Camera cam)
+ {
+ s_MyOnPreRender.Begin();
+
+ bool isOurUiCamera = IsUIInternalCamera(cam);
+ bool rendersOurPlayerLayer = CameraRendersPlayerLocalLayer(cam);
+ bool rendersOurCloneLayer = CameraRendersPlayerCloneLayer(cam);
+
+ // Renders both player layers.
+ // PlayerLocal will now act as a shadow caster, while PlayerClone will act as the actual head-hidden renderer.
+ bool rendersBothPlayerLayers = rendersOurPlayerLayer && rendersOurCloneLayer;
+ if (!_shadowsOnlyActive && rendersBothPlayerLayers)
+ {
+ s_SetShadowsOnly.Begin();
+
+ int sourceCount = _allSourceRenderers.Count;
+ var sourceRenderers = _allSourceRenderers;
+ for (int i = 0; i < sourceCount; i++)
+ {
+ Renderer renderer = sourceRenderers[i];
+ if (!IsRendererValid(renderer)) continue;
+
+ bool shouldRender = renderer.shadowCastingMode != ShadowCastingMode.Off;
+ _originallyWasEnabled[i] = renderer.enabled;
+ _originallyHadShadows[i] = shouldRender;
+ renderer.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
+ if (renderer.forceRenderingOff == shouldRender) renderer.forceRenderingOff = !shouldRender; // TODO: Eval if check is needed
+ }
+ _shadowsOnlyActive = true;
+
+ s_SetShadowsOnly.End();
+ }
+ else if (_shadowsOnlyActive && !rendersBothPlayerLayers)
+ {
+ s_UndoShadowsOnly.Begin();
+
+ int sourceCount = _allSourceRenderers.Count;
+ var sourceRenderers = _allSourceRenderers;
+ for (int i = 0; i < sourceCount; i++)
+ {
+ Renderer renderer = sourceRenderers[i];
+ if (!IsRendererValid(renderer)) continue;
+
+ renderer.shadowCastingMode = _originallyHadShadows[i] ? ShadowCastingMode.On : ShadowCastingMode.Off;
+ if (renderer.forceRenderingOff == _originallyWasEnabled[i]) renderer.forceRenderingOff = !_originallyWasEnabled[i]; // TODO: Eval if check is needed
+ }
+ _shadowsOnlyActive = false;
+
+ s_UndoShadowsOnly.End();
+ }
+
+ // Handle UI culling material changes
+ if (isOurUiCamera && !_uiCullingActive && rendersOurCloneLayer)
+ {
+ s_SetUiCulling.Begin();
+
+ int standardCount = _standardRenderers.Count;
+ var standardClones = _standardClones;
+ var cullingMaterials = _cullingMaterials;
+ for (int i = 0; i < standardCount; i++)
+ standardClones[i].sharedMaterials = cullingMaterials[i];
+
+ int skinnedCount = _skinnedRenderers.Count;
+ var skinnedClones = _skinnedClones;
+ for (int i = 0; i < skinnedCount; i++)
+ skinnedClones[i].sharedMaterials = cullingMaterials[i + standardCount];
+
+ _uiCullingActive = true;
+
+ s_SetUiCulling.End();
+ }
+ else if (!isOurUiCamera && _uiCullingActive)
+ {
+ s_UndoUiCulling.Begin();
+
+ int standardCount = _standardRenderers.Count;
+ var standardClones = _standardClones;
+ var localMaterials = _localMaterials;
+ for (int i = 0; i < standardCount; i++)
+ standardClones[i].sharedMaterials = localMaterials[i];
+
+ int skinnedCount = _skinnedRenderers.Count;
+ var skinnedClones = _skinnedClones;
+ for (int i = 0; i < skinnedCount; i++)
+ skinnedClones[i].sharedMaterials = localMaterials[i + standardCount];
+
+ _uiCullingActive = false;
+
+ s_UndoUiCulling.End();
+ }
+
+ s_MyOnPreRender.End();
+ }
+
+ #endregion Unity Events
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/AvatarClone/FPRExclusion/AvatarCloneExclusion.cs b/AvatarCloneTest/AvatarClone/FPRExclusion/AvatarCloneExclusion.cs
new file mode 100644
index 0000000..5681761
--- /dev/null
+++ b/AvatarCloneTest/AvatarClone/FPRExclusion/AvatarCloneExclusion.cs
@@ -0,0 +1,38 @@
+using ABI.CCK.Components;
+using UnityEngine;
+
+namespace NAK.AvatarCloneTest;
+
+public class AvatarCloneExclusion : IExclusionBehaviour
+{
+ private readonly AvatarClone _cloneSystem;
+ private readonly Transform _target;
+ private Transform _shrinkBone;
+
+ public bool isImmuneToGlobalState { get; set; }
+
+ public AvatarCloneExclusion(AvatarClone cloneSystem, Transform target)
+ {
+ _cloneSystem = cloneSystem;
+ _target = target;
+ }
+
+ public void UpdateExclusions(bool isShown, bool shrinkToZero)
+ {
+ Debug.Log($"[AvatarClone2] Updating exclusion for {_target.name}: isShown={isShown}, shrinkToZero={shrinkToZero}");
+
+ if (_shrinkBone == null)
+ {
+ // Create shrink bone parented directly to target
+ _shrinkBone = new GameObject($"{_target.name}_Shrink").transform;
+ _shrinkBone.SetParent(_target, false);
+ Debug.Log($"[AvatarClone2] Created shrink bone for {_target.name}");
+ }
+
+ // Set scale based on shrink mode
+ _shrinkBone.localScale = shrinkToZero ? Vector3.zero : Vector3.positiveInfinity;
+
+ // Let the clone system handle the update
+ _cloneSystem.HandleExclusionUpdate(_target, _shrinkBone, isShown);
+ }
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/AvatarCloneTest.csproj b/AvatarCloneTest/AvatarCloneTest.csproj
new file mode 100644
index 0000000..2e58669
--- /dev/null
+++ b/AvatarCloneTest/AvatarCloneTest.csproj
@@ -0,0 +1,9 @@
+
+
+
+ LocalCloneFix
+
+
+ TRACE;TRACE;
+
+
diff --git a/AvatarCloneTest/Main.cs b/AvatarCloneTest/Main.cs
new file mode 100644
index 0000000..21a34bf
--- /dev/null
+++ b/AvatarCloneTest/Main.cs
@@ -0,0 +1,72 @@
+using ABI_RC.Core;
+using MelonLoader;
+using UnityEngine;
+
+namespace NAK.AvatarCloneTest;
+
+public class AvatarCloneTestMod : MelonMod
+{
+ #region Melon Preferences
+
+ private static readonly MelonPreferences_Category Category =
+ MelonPreferences.CreateCategory(nameof(AvatarCloneTest));
+
+ internal static readonly MelonPreferences_Entry EntryUseAvatarCloneTest =
+ Category.CreateEntry("use_avatar_clone_test", true,
+ "Use Avatar Clone", description: "Uses the Avatar Clone setup for the local avatar.");
+
+ // internal static readonly MelonPreferences_Entry EntryCopyBlendShapes =
+ // Category.CreateEntry("copy_blend_shapes", true,
+ // "Copy Blend Shapes", description: "Copies the blend shapes from the original avatar to the clone.");
+ //
+ // internal static readonly MelonPreferences_Entry EntryCopyMaterials =
+ // Category.CreateEntry("copy_materials", true,
+ // "Copy Materials", description: "Copies the materials from the original avatar to the clone.");
+ //
+ // internal static readonly MelonPreferences_Entry EntryCopyMeshes =
+ // Category.CreateEntry("copy_meshes", true,
+ // "Copy Meshes", description: "Copies the meshes from the original avatar to the clone.");
+
+ #endregion Melon Preferences
+
+ #region Melon Events
+
+ public override void OnInitializeMelon()
+ {
+ ApplyPatches(typeof(Patches)); // slapped together a fix cause HarmonyInstance.Patch was null ref for no reason?
+ }
+
+ public override void OnUpdate()
+ {
+ // press f1 to find all cameras that arent tagged main and set them tno not render CVRLayers.PlayerClone
+ if (Input.GetKeyDown(KeyCode.F1))
+ {
+ foreach (var camera in UnityEngine.Object.FindObjectsOfType())
+ {
+ if (camera.tag != "MainCamera")
+ {
+ camera.cullingMask &= ~(1 << CVRLayers.PlayerClone);
+ }
+ }
+ }
+ }
+
+ #endregion Melon Events
+
+ #region Melon Mod Utilities
+
+ private void ApplyPatches(Type type)
+ {
+ try
+ {
+ HarmonyInstance.PatchAll(type);
+ }
+ catch (Exception e)
+ {
+ LoggerInstance.Msg($"Failed while patching {type.Name}!");
+ LoggerInstance.Error(e);
+ }
+ }
+
+ #endregion Melon Mod Utilities
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/Patches.cs b/AvatarCloneTest/Patches.cs
new file mode 100644
index 0000000..b4cec17
--- /dev/null
+++ b/AvatarCloneTest/Patches.cs
@@ -0,0 +1,87 @@
+using ABI_RC.Core;
+using ABI_RC.Core.Player;
+using ABI_RC.Core.Player.TransformHider;
+using ABI_RC.Core.Savior;
+using ABI_RC.Systems.Camera;
+using HarmonyLib;
+using UnityEngine;
+
+namespace NAK.AvatarCloneTest;
+
+public static class Patches
+{
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(TransformHiderUtils), nameof(TransformHiderUtils.SetupAvatar))]
+ private static bool OnSetupAvatar(GameObject avatar)
+ {
+ if (!AvatarCloneTestMod.EntryUseAvatarCloneTest.Value) return true;
+ avatar.AddComponent();
+ return false;
+ }
+
+ // [HarmonyPostfix]
+ // [HarmonyPatch(typeof(FPRExclusion), nameof(FPRExclusion.UpdateExclusions))]
+ // private static void OnUpdateExclusions(ref FPRExclusion __instance)
+ // {
+ // AvatarClone clone = PlayerSetup.Instance._avatar.GetComponent();
+ // if (clone == null) return;
+ // clone.SetBoneChainVisibility(__instance.target, !__instance.isShown, !__instance.shrinkToZero);
+ // }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(CVRMirror), nameof(CVRMirror.Start))]
+ private static void OnMirrorStart(CVRMirror __instance)
+ {
+ if (!AvatarCloneTestMod.EntryUseAvatarCloneTest.Value)
+ return;
+
+ // Don't reflect the player clone layer
+ __instance.m_ReflectLayers &= ~(1 << CVRLayers.PlayerClone);
+ }
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Update))]
+ private static void OnTransformHiderManagerUpdate(PlayerSetup __instance)
+ {
+ if (!AvatarCloneTestMod.EntryUseAvatarCloneTest.Value)
+ return;
+
+ if (MetaPort.Instance.settings.GetSettingsBool("ExperimentalAvatarOverrenderUI"))
+ __instance.activeUiCam.cullingMask |= 1 << CVRLayers.PlayerClone;
+ else
+ __instance.activeUiCam.cullingMask &= ~(1 << CVRLayers.PlayerClone);
+ }
+
+ private static bool _wasDebugInPortableCamera;
+
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(PortableCamera), nameof(PortableCamera.Update))]
+ private static void OnPortableCameraUpdate(ref PortableCamera __instance)
+ {
+ if (!AvatarCloneTestMod.EntryUseAvatarCloneTest.Value)
+ {
+ // Show both PlayerLocal and PlayerClone
+ __instance.cameraComponent.cullingMask |= 1 << CVRLayers.PlayerLocal;
+ __instance.cameraComponent.cullingMask |= 1 << CVRLayers.PlayerClone;
+ return;
+ }
+
+ if (TransformHiderManager.s_DebugInPortableCamera == _wasDebugInPortableCamera)
+ return;
+
+ if (TransformHiderManager.s_DebugInPortableCamera)
+ {
+ // Hide PlayerLocal, show PlayerClone
+ __instance.cameraComponent.cullingMask &= ~(1 << CVRLayers.PlayerLocal);
+ __instance.cameraComponent.cullingMask |= 1 << CVRLayers.PlayerClone;
+ }
+ else
+ {
+ // Show PlayerLocal, hide PlayerClone
+ __instance.cameraComponent.cullingMask |= 1 << CVRLayers.PlayerLocal;
+ __instance.cameraComponent.cullingMask &= ~(1 << CVRLayers.PlayerClone);
+ }
+
+ _wasDebugInPortableCamera = TransformHiderManager.s_DebugInPortableCamera;
+ }
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/Properties/AssemblyInfo.cs b/AvatarCloneTest/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..3334b28
--- /dev/null
+++ b/AvatarCloneTest/Properties/AssemblyInfo.cs
@@ -0,0 +1,32 @@
+using MelonLoader;
+using NAK.AvatarCloneTest.Properties;
+using System.Reflection;
+
+[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyTitle(nameof(NAK.AvatarCloneTest))]
+[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
+[assembly: AssemblyProduct(nameof(NAK.AvatarCloneTest))]
+
+[assembly: MelonInfo(
+ typeof(NAK.AvatarCloneTest.AvatarCloneTestMod),
+ nameof(NAK.AvatarCloneTest),
+ AssemblyInfoParams.Version,
+ AssemblyInfoParams.Author,
+ downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/AvatarCloneTest"
+)]
+
+[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.AvatarCloneTest.Properties;
+internal static class AssemblyInfoParams
+{
+ public const string Version = "1.0.0";
+ public const string Author = "NotAKidoS";
+}
\ No newline at end of file
diff --git a/AvatarCloneTest/README.md b/AvatarCloneTest/README.md
new file mode 100644
index 0000000..cc12a9c
--- /dev/null
+++ b/AvatarCloneTest/README.md
@@ -0,0 +1,18 @@
+# VisualCloneFix
+
+Fixes the Visual Clone system and allows you to use it again.
+
+Using the Visual Clone should be faster than the default Head Hiding & Shadow Clones, but will add a longer hitch on initial avatar load.
+
+**NOTE:** The Visual Clone is still an experimental feature that was temporarily removed in [ChilloutVR 2024r175 Hotfix 1](https://abinteractive.net/blog/chilloutvr_2024r175_hotfix_1), so there may be bugs or issues with it.
+
+---
+
+Here is the block of text where I tell you this mod is not affiliated with or endorsed by ABI.
+https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games
+
+> This mod is an independent creation not affiliated with, supported by, or approved by Alpha Blend Interactive.
+
+> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use.
+
+> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive.
diff --git a/AvatarCloneTest/format.json b/AvatarCloneTest/format.json
new file mode 100644
index 0000000..6634e9f
--- /dev/null
+++ b/AvatarCloneTest/format.json
@@ -0,0 +1,23 @@
+{
+ "_id": 221,
+ "name": "VisualCloneFix",
+ "modversion": "1.0.1",
+ "gameversion": "2024r175",
+ "loaderversion": "0.6.1",
+ "modtype": "Mod",
+ "author": "NotAKidoS",
+ "description": "Fixes the Visual Clone system and allows you to use it again.\n\nUsing the Visual Clone should be faster than the default Head Hiding & Shadow Clones, but will add a longer hitch on initial avatar load.\n\n**NOTE:** The Visual Clone is still an experimental feature that was temporarily removed in [ChilloutVR 2024r175 Hotfix 1](https://abinteractive.net/blog/chilloutvr_2024r175_hotfix_1), so there may be bugs or issues with it.",
+ "searchtags": [
+ "visual",
+ "clone",
+ "head",
+ "hiding"
+ ],
+ "requirements": [
+ "None"
+ ],
+ "downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r36/VisualCloneFix.dll",
+ "sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/VisualCloneFix/",
+ "changelog": "- Fixed FPRExclusions IsShown state being inverted when toggled.\n- Fixed head FPRExclusion generation not checking for existing exclusion.\n- Sped up FindExclusionVertList by 100x by not being an idiot. This heavily reduces avatar hitch with Visual Clone active.",
+ "embedcolor": "#f61963"
+}
\ No newline at end of file