further cleanup of repo

This commit is contained in:
NotAKidoS 2025-04-03 03:03:24 -05:00
parent 4f8dcb0cd0
commit 323eb92f2e
140 changed files with 1 additions and 2430 deletions

View file

@ -0,0 +1,229 @@
using ABI.CCK.Components;
using UnityEngine;
namespace NAK.AvatarCloneTest;
public partial class AvatarClone
{
#region Exclusions
private FPRExclusion[] _exclusions;
private void AddExclusionToHeadIfNeeded()
{
if (!TryGetComponent(out Animator animator)
|| !animator.isHuman
|| !animator.avatar
|| !animator.avatar.isValid)
return;
Transform head = animator.GetBoneTransform(HumanBodyBones.Head);
if (!head)
return;
GameObject headGo = head.gameObject;
if (headGo.TryGetComponent(out FPRExclusion exclusion))
return;
exclusion = headGo.AddComponent<FPRExclusion>();
exclusion.target = head;
exclusion.isShown = false;
}
private void InitializeExclusions()
{
_exclusions = GetComponentsInChildren<FPRExclusion>(true);
var exclusionRoots = new Dictionary<Transform, AvatarCloneExclusion>(_exclusions.Length);
// **1. Precompute Exclusions**
foreach (FPRExclusion exclusion in _exclusions)
{
Transform target = exclusion.target ??= exclusion.transform;
if (exclusionRoots.ContainsKey(target) || !target.gameObject.scene.IsValid())
continue;
AvatarCloneExclusion behaviour = new AvatarCloneExclusion(this, target);
exclusion.behaviour = behaviour;
exclusionRoots.Add(target, behaviour);
}
// Process Exclusion Transforms
Renderer ourRenderer;
void ProcessTransformHierarchy(Transform current, Transform root, AvatarCloneExclusion behaviour)
{
if (exclusionRoots.ContainsKey(current) && current != root) return;
behaviour.affectedTransforms.Add(current);
if (current.TryGetComponent(out ourRenderer))
behaviour.affectedRenderers.Add(ourRenderer);
for (int i = 0; i < current.childCount; i++)
{
Transform child = current.GetChild(i);
if (!exclusionRoots.ContainsKey(child))
ProcessTransformHierarchy(child, root, behaviour);
}
}
foreach (var entry in exclusionRoots)
{
Transform rootTransform = entry.Key;
AvatarCloneExclusion behaviour = entry.Value;
ProcessTransformHierarchy(rootTransform, rootTransform, behaviour);
behaviour.affectedTransformSet = new HashSet<Transform>(behaviour.affectedTransforms);
}
// ------------------------------
// **OPTIMIZED EXCLUSION BONE MAPPING**
// ------------------------------
Dictionary<Transform, AvatarCloneExclusion>.ValueCollection exclusionBehaviours = exclusionRoots.Values;
int skinnedCount = _skinnedClones.Count;
// **2. Precompute Bone-to-Exclusion Mapping**
int estimatedBoneCount = skinnedCount * 20; // Estimated bones per skinned mesh
var boneToExclusion = new Dictionary<Transform, List<AvatarCloneExclusion>>(estimatedBoneCount);
foreach (AvatarCloneExclusion behaviour in exclusionBehaviours)
{
foreach (Transform bone in behaviour.affectedTransformSet)
{
if (!boneToExclusion.TryGetValue(bone, out var list))
{
list = new List<AvatarCloneExclusion>(2);
boneToExclusion[bone] = list;
}
list.Add(behaviour);
}
}
// **3. Process Skinned Mesh Renderers**
for (int s = 0; s < skinnedCount; s++)
{
SkinnedMeshRenderer source = _skinnedRenderers[s];
var bones = source.bones; // Cache bones array
SkinnedMeshRenderer smr = _skinnedClones[s];
int boneCount = bones.Length;
for (int i = 0; i < boneCount; i++)
{
Transform bone = bones[i];
// **Skip if the bone isn't mapped to exclusions**
if (!bone // Skip null bones
|| !boneToExclusion.TryGetValue(bone, out var behaviours))
continue;
// **Avoid redundant dictionary lookups**
for (int j = 0; j < behaviours.Count; j++)
{
AvatarCloneExclusion behaviour = behaviours[j];
if (!behaviour.skinnedToBoneIndex.TryGetValue(smr, out var indices))
{
indices = new List<int>(4);
behaviour.skinnedToBoneIndex[smr] = indices;
}
indices.Add(i);
}
}
}
ApplyInitialExclusionState();
}
public void ApplyInitialExclusionState()
{
foreach (FPRExclusion exclusion in _exclusions)
{
exclusion._wasShown = exclusion.isShown;
if (!exclusion.isShown) exclusion.UpdateExclusions();
}
}
public void HandleExclusionUpdate(AvatarCloneExclusion exclusion, bool isShown)
{
#if ENABLE_PROFILER
s_UpdateExclusions.Begin();
#endif
// **1. Update Renderer Visibility**
foreach (Renderer renderer in exclusion.affectedRenderers)
{
if (renderer is SkinnedMeshRenderer skinned)
{
int index = _skinnedRenderers.IndexOf(skinned);
if (index >= 0) _skinnedClones[index].gameObject.SetActive(isShown);
}
else if (renderer is MeshRenderer mesh)
{
int index = _meshRenderers.IndexOf(mesh);
if (index >= 0)
{
if (Setting_CloneMeshRenderers)
{
_meshClones[index].gameObject.SetActive(isShown);
}
else
{
// Other renderer (never cloned) - update shadow casting state
_sourceShouldBeHiddenFromFPR[index] = !isShown; // When hidden, use for shadows
}
}
}
else if (renderer)
{
int index = _otherRenderers.IndexOf(renderer);
if (index >= 0)
{
int shadowIndex = index + (Setting_CloneMeshRenderers ? _meshRenderers.Count : 0);
_sourceShouldBeHiddenFromFPR[shadowIndex] = !isShown; // When hidden, use for shadows
}
}
}
// **2. Update Bone References in Skinned Mesh Renderers**
UpdateSkinnedMeshBones(exclusion, exclusion._shrinkBone, isShown);
#if ENABLE_PROFILER
s_UpdateExclusions.End();
#endif
}
private void UpdateSkinnedMeshBones(AvatarCloneExclusion exclusion, Transform shrinkBone, bool isShown)
{
#if ENABLE_PROFILER
s_HandleBoneUpdates.Begin();
#endif
foreach (var smrEntry in exclusion.skinnedToBoneIndex)
{
SkinnedMeshRenderer smr = smrEntry.Key;
var indices = smrEntry.Value;
bool needsUpdate = false;
var parentBones = smr.transform.parent.GetComponent<SkinnedMeshRenderer>().bones;
var cloneBones = smr.bones;
Array.Resize(ref cloneBones, parentBones.Length);
// Only modify our bones, other exclusions may have modified others
for (int i = 0; i < indices.Count; i++)
{
int index = indices[i];
if (!isShown) cloneBones[index] = shrinkBone;
else cloneBones[index] = parentBones[index];
needsUpdate = true;
}
if (needsUpdate) smr.bones = cloneBones;
}
#if ENABLE_PROFILER
s_HandleBoneUpdates.End();
#endif
}
#endregion Exclusions
}

View file

@ -0,0 +1,304 @@
using ABI_RC.Core.Player.ShadowClone;
using UnityEngine;
using UnityEngine.Rendering;
namespace NAK.AvatarCloneTest;
public partial class AvatarClone
{
#region Initialization
private void InitializeCollections()
{
#if ENABLE_PROFILER
s_InitializeData.Begin();
#endif
// Initialize source collections
_skinnedRenderers = new List<SkinnedMeshRenderer>();
_blendShapeWeights = new List<List<float>>();
_meshRenderers = new List<MeshRenderer>();
_meshFilters = new List<MeshFilter>();
_otherRenderers = new List<Renderer>();
// Initialize clone collections
_skinnedClones = new List<SkinnedMeshRenderer>();
_skinnedCloneMaterials = new List<Material[]>();
_skinnedCloneCullingMaterials = new List<Material[]>();
if (Setting_CloneMeshRenderers)
{
_meshClones = new List<MeshRenderer>();
_meshCloneFilters = new List<MeshFilter>();
_meshCloneMaterials = new List<Material[]>();
_meshCloneCullingMaterials = new List<Material[]>();
}
// Initialize shared resources
_materialWorkingList = new List<Material>();
_propertyBlock = new MaterialPropertyBlock();
#if ENABLE_PROFILER
s_InitializeData.End();
#endif
}
private void CollectRenderers()
{
#if ENABLE_PROFILER
s_InitializeData.Begin();
#endif
var renderers = GetComponentsInChildren<Renderer>(true);
var currentIndex = 0;
var nonCloned = 0;
// Single pass: directly categorize renderers
foreach (Renderer renderer in renderers)
{
switch (renderer)
{
case SkinnedMeshRenderer skinned when skinned.sharedMesh != null:
AddSkinnedRenderer(skinned);
currentIndex++;
break;
case MeshRenderer mesh:
MeshFilter filter = mesh.GetComponent<MeshFilter>();
if (filter != null && filter.sharedMesh != null)
{
if (Setting_CloneMeshRenderers)
{
AddMeshRenderer(mesh, filter);
}
else
{
AddMeshRenderer(mesh, filter);
nonCloned++;
}
currentIndex++;
}
break;
default:
AddOtherRenderer(renderer);
currentIndex++;
nonCloned++;
break;
}
}
_rendererActiveStates = new bool[currentIndex];
_originalShadowCastingMode = new ShadowCastingMode[currentIndex];
_sourceShouldBeHiddenFromFPR = new bool[nonCloned];
#if ENABLE_PROFILER
s_InitializeData.End();
#endif
}
private void AddSkinnedRenderer(SkinnedMeshRenderer renderer)
{
#if ENABLE_PROFILER
s_AddRenderer.Begin();
#endif
_skinnedRenderers.Add(renderer);
// Clone materials array for clone renderer
var materials = renderer.sharedMaterials;
var cloneMaterials = new Material[materials.Length];
for (int i = 0; i < materials.Length; i++) cloneMaterials[i] = materials[i];
_skinnedCloneMaterials.Add(cloneMaterials);
// Cache culling materials
var cullingMaterialArray = new Material[materials.Length];
#if !UNITY_EDITOR
for (int i = 0; i < materials.Length; i++) cullingMaterialArray[i] = ShadowCloneUtils.cullingMaterial;
#else
for (int i = 0; i < materials.Length; i++) cullingMaterialArray[i] = cullingMaterial;
#endif
_skinnedCloneCullingMaterials.Add(cullingMaterialArray);
// Cache blend shape weights
var weights = new List<float>(renderer.sharedMesh.blendShapeCount);
for (int i = 0; i < renderer.sharedMesh.blendShapeCount; i++) weights.Add(0f);
_blendShapeWeights.Add(weights);
#if ENABLE_PROFILER
s_AddRenderer.End();
#endif
}
private void AddMeshRenderer(MeshRenderer renderer, MeshFilter filter)
{
#if ENABLE_PROFILER
s_AddRenderer.Begin();
#endif
_meshRenderers.Add(renderer);
_meshFilters.Add(filter);
if (!Setting_CloneMeshRenderers) return;
// Clone materials array for clone renderer
var materials = renderer.sharedMaterials;
var cloneMaterials = new Material[materials.Length];
for (int i = 0; i < materials.Length; i++) cloneMaterials[i] = materials[i];
_meshCloneMaterials.Add(cloneMaterials);
// Cache culling materials
var cullingMaterialArray = new Material[materials.Length];
#if !UNITY_EDITOR
for (int i = 0; i < materials.Length; i++) cullingMaterialArray[i] = ShadowCloneUtils.cullingMaterial;
#else
for (int i = 0; i < materials.Length; i++) cullingMaterialArray[i] = cullingMaterial;
#endif
_meshCloneCullingMaterials.Add(cullingMaterialArray);
#if ENABLE_PROFILER
s_AddRenderer.End();
#endif
}
private void AddOtherRenderer(Renderer renderer)
{
#if ENABLE_PROFILER
s_AddRenderer.Begin();
#endif
_otherRenderers.Add(renderer);
#if ENABLE_PROFILER
s_AddRenderer.End();
#endif
}
private void CreateClones()
{
#if ENABLE_PROFILER
s_InitializeData.Begin();
#endif
// Always create skinned mesh clones
int skinnedCount = _skinnedRenderers.Count;
for (int i = 0; i < skinnedCount; i++)
{
CreateSkinnedClone(i);
}
// Optionally create mesh clones
if (Setting_CloneMeshRenderers)
{
int meshCount = _meshRenderers.Count;
for (int i = 0; i < meshCount; i++)
{
CreateMeshClone(i);
}
}
#if ENABLE_PROFILER
s_InitializeData.End();
#endif
}
private void CreateSkinnedClone(int index)
{
#if ENABLE_PROFILER
s_CreateClone.Begin();
#endif
SkinnedMeshRenderer source = _skinnedRenderers[index];
GameObject clone = new(source.name + "_Clone")
{
layer = CLONE_LAYER
};
clone.transform.SetParent(source.transform, false);
SkinnedMeshRenderer cloneRenderer = clone.AddComponent<SkinnedMeshRenderer>();
// Basic setup
cloneRenderer.sharedMaterials = _skinnedCloneMaterials[index];
cloneRenderer.shadowCastingMode = ShadowCastingMode.Off;
cloneRenderer.probeAnchor = source.probeAnchor;
cloneRenderer.sharedMesh = source.sharedMesh;
cloneRenderer.rootBone = source.rootBone;
cloneRenderer.bones = source.bones;
#if !UNITY_EDITOR
cloneRenderer.localBounds = new Bounds(source.localBounds.center, source.localBounds.size * 2f);
#endif
// Quality settings
cloneRenderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
cloneRenderer.allowOcclusionWhenDynamic = false;
cloneRenderer.updateWhenOffscreen = false;
cloneRenderer.skinnedMotionVectors = false;
cloneRenderer.forceMatrixRecalculationPerRender = false;
cloneRenderer.quality = SkinQuality.Bone4;
source.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
source.allowOcclusionWhenDynamic = false;
source.updateWhenOffscreen = false;
source.skinnedMotionVectors = false;
source.forceMatrixRecalculationPerRender = false;
source.quality = SkinQuality.Bone4;
// Add to clone list
_skinnedClones.Add(cloneRenderer);
#if ENABLE_PROFILER
s_CreateClone.End();
#endif
}
private void CreateMeshClone(int index)
{
#if ENABLE_PROFILER
s_CreateClone.Begin();
#endif
MeshRenderer source = _meshRenderers[index];
MeshFilter sourceFilter = _meshFilters[index];
GameObject clone = new(source.name + "_Clone")
{
layer = CLONE_LAYER
};
clone.transform.SetParent(source.transform, false);
MeshRenderer cloneRenderer = clone.AddComponent<MeshRenderer>();
MeshFilter cloneFilter = clone.AddComponent<MeshFilter>();
// Basic setup
cloneRenderer.sharedMaterials = _meshCloneMaterials[index];
cloneRenderer.shadowCastingMode = ShadowCastingMode.Off;
cloneRenderer.probeAnchor = source.probeAnchor;
#if !UNITY_EDITOR
cloneRenderer.localBounds = new Bounds(source.localBounds.center, source.localBounds.size * 2f);
#endif
cloneFilter.sharedMesh = sourceFilter.sharedMesh;
// Quality settings
cloneRenderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
cloneRenderer.allowOcclusionWhenDynamic = false;
source.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
source.allowOcclusionWhenDynamic = false;
// Add to clone lists
_meshClones.Add(cloneRenderer);
_meshCloneFilters.Add(cloneFilter);
#if ENABLE_PROFILER
s_CreateClone.End();
#endif
}
#endregion Initialization
}

View file

@ -0,0 +1,212 @@
using UnityEngine;
using UnityEngine.Rendering;
namespace NAK.AvatarCloneTest;
public partial class AvatarClone
{
#region Render State Management
private void MyOnPreCull(Camera cam)
{
#if UNITY_EDITOR
// Scene & Preview cameras are not needed
if (cam.cameraType != CameraType.Game)
return;
#endif
#if ENABLE_PROFILER
s_PreCullUpdate.Begin();
#endif
bool isOurUiCamera = IsUIInternalCamera(cam);
bool rendersOurPlayerLayer = CameraRendersPlayerLocalLayer(cam);
bool rendersOurCloneLayer = CameraRendersPlayerCloneLayer(cam);
bool rendersBothPlayerLayers = rendersOurPlayerLayer && rendersOurCloneLayer;
// Handle shadow casting when camera renders both layers
if (!_sourcesSetForShadowCasting
&& rendersBothPlayerLayers)
{
ConfigureSourceShadowCasting(true);
_sourcesSetForShadowCasting = true;
}
else if (_sourcesSetForShadowCasting && !rendersBothPlayerLayers)
{
ConfigureSourceShadowCasting(false);
_sourcesSetForShadowCasting = false;
}
// Handle UI culling for clone layer
if (!_clonesSetForUiCulling
&& isOurUiCamera && rendersOurCloneLayer)
{
ConfigureCloneUICulling(true);
_clonesSetForUiCulling = true;
}
else if (_clonesSetForUiCulling)
{
ConfigureCloneUICulling(false);
_clonesSetForUiCulling = false;
}
#if ENABLE_PROFILER
s_PreCullUpdate.End();
#endif
}
private void ConfigureSourceShadowCasting(bool setSourcesToShadowCast)
{
#if ENABLE_PROFILER
s_ConfigureShadowCasting.Begin();
#endif
int currentIndex = 0;
int shadowArrayIndex = 0;
// Handle skinned mesh renderers (always have clones)
int skinnedCount = _skinnedRenderers.Count;
for (int i = 0; i < skinnedCount; i++, currentIndex++)
{
if (!_rendererActiveStates[currentIndex]) continue;
SkinnedMeshRenderer source = _skinnedRenderers[i];
if (setSourcesToShadowCast)
{
ShadowCastingMode originalMode = _originalShadowCastingMode[currentIndex] = source.shadowCastingMode;
if (originalMode == ShadowCastingMode.Off)
source.forceRenderingOff = true;
else
source.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
}
else
{
source.shadowCastingMode = _originalShadowCastingMode[currentIndex];
source.forceRenderingOff = false;
}
}
// Handle mesh renderers based on clone setting
if (Setting_CloneMeshRenderers)
{
int meshCount = _meshRenderers.Count;
for (int i = 0; i < meshCount; i++, currentIndex++)
{
if (!_rendererActiveStates[currentIndex]) continue;
MeshRenderer source = _meshRenderers[i];
if (setSourcesToShadowCast)
{
ShadowCastingMode originalMode = _originalShadowCastingMode[currentIndex] = source.shadowCastingMode;
if (originalMode == ShadowCastingMode.Off)
source.forceRenderingOff = true;
else
source.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
}
else
{
source.shadowCastingMode = _originalShadowCastingMode[currentIndex];
source.forceRenderingOff = false;
}
}
}
else
{
// When not cloned, mesh renderers use the shadow casting array
int meshCount = _meshRenderers.Count;
for (int i = 0; i < meshCount; i++, shadowArrayIndex++, currentIndex++)
{
if (!_rendererActiveStates[currentIndex]) continue;
if (!_sourceShouldBeHiddenFromFPR[shadowArrayIndex]) continue;
MeshRenderer source = _meshRenderers[i];
if (setSourcesToShadowCast)
{
ShadowCastingMode originalMode = _originalShadowCastingMode[currentIndex] = source.shadowCastingMode;
if (originalMode == ShadowCastingMode.Off)
source.forceRenderingOff = true;
else
source.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
}
else
{
source.shadowCastingMode = _originalShadowCastingMode[currentIndex];
source.forceRenderingOff = false;
}
}
}
// Handle other renderers (never cloned)
int otherCount = _otherRenderers.Count;
for (int i = 0; i < otherCount; i++, shadowArrayIndex++, currentIndex++)
{
if (!_rendererActiveStates[currentIndex]) continue;
if (!_sourceShouldBeHiddenFromFPR[shadowArrayIndex]) continue;
Renderer source = _otherRenderers[i];
if (setSourcesToShadowCast)
{
ShadowCastingMode originalMode = _originalShadowCastingMode[currentIndex] = source.shadowCastingMode;
if (originalMode == ShadowCastingMode.Off)
source.forceRenderingOff = true;
else
source.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
}
else
{
source.shadowCastingMode = _originalShadowCastingMode[currentIndex];
source.forceRenderingOff = false;
}
}
#if ENABLE_PROFILER
s_ConfigureShadowCasting.End();
#endif
}
private void ConfigureCloneUICulling(bool enableCulling)
{
#if ENABLE_PROFILER
s_ConfigureUICulling.Begin();
#endif
// Set the materials to our culling materials
int currentIndex = 0;
int skinnedCount = _skinnedRenderers.Count;
for (int i = 0; i < skinnedCount; i++, currentIndex++)
{
if (!_rendererActiveStates[currentIndex])
continue;
_skinnedClones[i].sharedMaterials = enableCulling ?
_skinnedCloneCullingMaterials[i] :
_skinnedCloneMaterials[i];
}
if (Setting_CloneMeshRenderers)
{
int meshCount = _meshRenderers.Count;
for (int i = 0; i < meshCount; i++, currentIndex++)
{
if (!_rendererActiveStates[currentIndex])
continue;
_meshClones[i].sharedMaterials = enableCulling ?
_meshCloneCullingMaterials[i] :
_meshCloneMaterials[i];
}
}
#if ENABLE_PROFILER
s_ConfigureUICulling.End();
#endif
}
#endregion Render State Management
}

View file

@ -0,0 +1,156 @@
using UnityEngine;
namespace NAK.AvatarCloneTest;
public partial class AvatarClone
{
#region State Syncing
private void SyncEnabledState()
{
#if ENABLE_PROFILER
s_CopyEnabledState.Begin();
#endif
int currentIndex = 0;
// Update skinned mesh renderers
int skinnedCount = _skinnedRenderers.Count;
for (int i = 0; i < skinnedCount; i++, currentIndex++)
{
SkinnedMeshRenderer source = _skinnedRenderers[i];
_skinnedClones[i].enabled = _rendererActiveStates[currentIndex] = IsRendererActive(source);
}
// Update mesh renderers
int meshCount = _meshRenderers.Count;
for (int i = 0; i < meshCount; i++, currentIndex++)
{
MeshRenderer source = _meshRenderers[i];
if (Setting_CloneMeshRenderers) _meshClones[i].enabled = _rendererActiveStates[currentIndex] = IsRendererActive(source);
else _rendererActiveStates[currentIndex] = IsRendererActive(source);
}
// Update other renderers
int otherCount = _otherRenderers.Count;
for (int i = 0; i < otherCount; i++, currentIndex++)
{
Renderer source = _otherRenderers[i];
_rendererActiveStates[currentIndex] = IsRendererActive(source);
}
#if ENABLE_PROFILER
s_CopyEnabledState.End();
#endif
}
private void SyncMaterials()
{
#if ENABLE_PROFILER
s_CopyMaterials.Begin();
#endif
int currentIndex = 0;
// Sync skinned mesh materials
int skinnedCount = _skinnedRenderers.Count;
for (int i = 0; i < skinnedCount; i++, currentIndex++)
{
if (!_rendererActiveStates[currentIndex])
continue;
CopyMaterialsAndProperties(
_skinnedRenderers[i],
_skinnedClones[i],
_propertyBlock,
_materialWorkingList,
_skinnedCloneMaterials[i]);
}
// Sync mesh materials if enabled
if (Setting_CloneMeshRenderers)
{
int meshCount = _meshRenderers.Count;
for (int i = 0; i < meshCount; i++, currentIndex++)
{
if (!_rendererActiveStates[currentIndex])
continue;
CopyMaterialsAndProperties(
_meshRenderers[i],
_meshClones[i],
_propertyBlock,
_materialWorkingList,
_meshCloneMaterials[i]);
}
}
#if ENABLE_PROFILER
s_CopyMaterials.End();
#endif
}
private void SyncBlendShapes()
{
#if ENABLE_PROFILER
s_CopyBlendShapes.Begin();
#endif
int skinnedCount = _skinnedRenderers.Count;
for (int i = 0; i < skinnedCount; i++)
{
SkinnedMeshRenderer source = _skinnedRenderers[i];
if (!_rendererActiveStates[i])
continue;
CopyBlendShapes(
source,
_skinnedClones[i],
_blendShapeWeights[i]);
}
#if ENABLE_PROFILER
s_CopyBlendShapes.End();
#endif
}
private static void CopyMaterialsAndProperties(
Renderer source,
Renderer clone,
MaterialPropertyBlock propertyBlock,
List<Material> workingList,
Material[] cloneMaterials)
{
source.GetSharedMaterials(workingList);
int matCount = workingList.Count;
bool hasChanged = false;
for (int i = 0; i < matCount; i++)
{
if (ReferenceEquals(workingList[i], cloneMaterials[i])) continue;
cloneMaterials[i] = workingList[i];
hasChanged = true;
}
if (hasChanged) clone.sharedMaterials = cloneMaterials;
source.GetPropertyBlock(propertyBlock);
clone.SetPropertyBlock(propertyBlock);
}
private static void CopyBlendShapes(
SkinnedMeshRenderer source,
SkinnedMeshRenderer clone,
List<float> weights)
{
int weightCount = weights.Count;
for (int i = 0; i < weightCount; i++)
{
float weight = source.GetBlendShapeWeight(i);
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (weight == weights[i]) continue; // Halves the work
clone.SetBlendShapeWeight(i, weights[i] = weight);
}
}
#endregion State Syncing
}

View file

@ -0,0 +1,81 @@
using ABI_RC.Core.Player;
using MagicaCloth;
using MagicaCloth2;
using UnityEngine;
namespace NAK.AvatarCloneTest;
public partial class AvatarClone
{
#region Utilities
private static bool IsRendererActive(Renderer renderer)
=> renderer && renderer.enabled && renderer.gameObject.activeInHierarchy;
private static bool CameraRendersPlayerLocalLayer(Camera cam)
=> (cam.cullingMask & (1 << LOCAL_LAYER)) != 0;
private static bool CameraRendersPlayerCloneLayer(Camera cam)
=> (cam.cullingMask & (1 << CLONE_LAYER)) != 0;
private static bool IsUIInternalCamera(Camera cam)
#if !UNITY_EDITOR
=> cam == PlayerSetup.Instance.activeUiCam;
#else
=> cam.gameObject.layer == 15;
#endif
#endregion Utilities
#region Magica Cloth Support
private void SetupMagicaClothSupport()
{
var magicaCloths1 = GetComponentsInChildren<BaseCloth>(true);
foreach (BaseCloth magicaCloth1 in magicaCloths1)
magicaCloth1.SetCullingMode(PhysicsTeam.TeamCullingMode.Off);
var magicaCloths2 = base.GetComponentsInChildren<MagicaCloth2.MagicaCloth>(true);
foreach (MagicaCloth2.MagicaCloth magicaCloth2 in magicaCloths2)
magicaCloth2.serializeData.cullingSettings.cameraCullingMode = CullingSettings.CameraCullingMode.AnimatorLinkage;
}
public void OnMagicaClothMeshSwapped(Renderer render, Mesh newMesh)
{
switch (render)
{
case MeshRenderer mesh:
{
int index = _meshRenderers.IndexOf(mesh);
if (index != -1) _meshCloneFilters[index].sharedMesh = newMesh;
break;
}
case SkinnedMeshRenderer skinned:
{
int index = _skinnedRenderers.IndexOf(skinned);
if (index != -1)
{
// Copy the mesh
_skinnedClones[index].sharedMesh = newMesh;
// Copy appended bones if count is different
var cloneBones = _skinnedClones[index].bones; // alloc
var sourceBones = skinned.bones; // alloc
int cloneBoneCount = cloneBones.Length;
int sourceBoneCount = sourceBones.Length;
if (cloneBoneCount != sourceBoneCount)
{
// Copy the new bones only
Array.Resize(ref cloneBones, sourceBoneCount);
for (int i = cloneBoneCount; i < sourceBoneCount; i++) cloneBones[i] = sourceBones[i];
_skinnedClones[index].bones = cloneBones;
}
}
break;
}
}
}
#endregion Magica Cloth Support
}

View file

@ -0,0 +1,147 @@
using NAK.AvatarCloneTest;
using UnityEngine;
using UnityEngine.Rendering;
namespace NAK.AvatarCloneTest;
public partial class AvatarClone : MonoBehaviour
{
#region Constants
private const int LOCAL_LAYER = 8;
private const int CLONE_LAYER = 9;
#endregion Constants
#region Profiler Markers
#if ENABLE_PROFILER
private static readonly ProfilerMarker s_CopyEnabledState = new($"{nameof(AvatarClone)}.{nameof(SyncEnabledState)}");
private static readonly ProfilerMarker s_CopyMaterials = new($"{nameof(AvatarClone)}.{nameof(CopyMaterialsAndProperties)}");
private static readonly ProfilerMarker s_CopyBlendShapes = new($"{nameof(AvatarClone)}.{nameof(CopyBlendShapes)}");
private static readonly ProfilerMarker s_InitializeData = new($"{nameof(AvatarClone)}.Initialize");
private static readonly ProfilerMarker s_UpdateExclusions = new($"{nameof(AvatarClone)}.{nameof(HandleExclusionUpdate)}");
private static readonly ProfilerMarker s_CollectExclusionData = new($"{nameof(AvatarClone)}.{nameof(CollectExclusionData)}");
private static readonly ProfilerMarker s_HandleBoneUpdates = new($"{nameof(AvatarClone)}.{nameof(UpdateSkinnedMeshBones)}");
private static readonly ProfilerMarker s_PreCullUpdate = new($"{nameof(AvatarClone)}.{nameof(MyOnPreCull)}");
private static readonly ProfilerMarker s_ConfigureShadowCasting = new($"{nameof(AvatarClone)}.{nameof(ConfigureSourceShadowCasting)}");
private static readonly ProfilerMarker s_ConfigureUICulling = new($"{nameof(AvatarClone)}.{nameof(ConfigureCloneUICulling)}");
private static readonly ProfilerMarker s_AddRenderer = new($"{nameof(AvatarClone)}.AddRenderer");
private static readonly ProfilerMarker s_CreateClone = new($"{nameof(AvatarClone)}.CreateClone");
#endif
#endregion Profiler Markers
#region Settings
public bool Setting_CloneMeshRenderers;
public bool Setting_CopyMaterials = true;
public bool Setting_CopyBlendShapes = true;
#endregion Settings
#region Source Collections - Cloned Renderers
// Skinned mesh renderers (always cloned)
private List<SkinnedMeshRenderer> _skinnedRenderers;
private List<List<float>> _blendShapeWeights;
// Mesh renderers (optionally cloned)
private List<MeshRenderer> _meshRenderers;
private List<MeshFilter> _meshFilters;
#endregion Source Collections - Cloned Renderers
#region Source Collections - Non-Cloned Renderers
// All other renderers (never cloned)
private List<Renderer> _otherRenderers;
// True if source renderer should hide. False if source renderer should show.
// Only used for non-cloned renderers (MeshRenderers and other Renderers).
private bool[] _sourceShouldBeHiddenFromFPR;
// Three states: On, ShadowsOnly, Off
private ShadowCastingMode[] _originalShadowCastingMode;
#endregion Source Collections - Non-Cloned Renderers
#region Clone Collections
// Skinned mesh clones
private List<SkinnedMeshRenderer> _skinnedClones;
private List<Material[]> _skinnedCloneMaterials;
private List<Material[]> _skinnedCloneCullingMaterials;
// Mesh clones (optional)
private List<MeshRenderer> _meshClones;
private List<MeshFilter> _meshCloneFilters;
private List<Material[]> _meshCloneMaterials;
private List<Material[]> _meshCloneCullingMaterials;
#endregion Clone Collections
#region Shared Resources
private List<Material> _materialWorkingList; // Used for GetSharedMaterials
private MaterialPropertyBlock _propertyBlock;
#endregion Shared Resources
#region State
private bool _sourcesSetForShadowCasting;
private bool _clonesSetForUiCulling;
private bool[] _rendererActiveStates;
#endregion State
#region Unity Events
private void Start()
{
Setting_CloneMeshRenderers = AvatarCloneTestMod.EntryCloneMeshRenderers.Value;
InitializeCollections();
CollectRenderers();
CreateClones();
AddExclusionToHeadIfNeeded();
InitializeExclusions();
SetupMagicaClothSupport();
// bool animatesClone = transform.Find("[ExplicitlyAnimatesVisualClones]") != null;
// Setting_CopyMaterials = !animatesClone;
// Setting_CopyBlendShapes = !animatesClone;
// Animator animator = GetComponent<Animator>();
// if (animator && animatesClone) animator.Rebind();
// Likely a Unity bug with where we can touch shadowCastingMode & forceRenderingOff
#if !UNITY_EDITOR
Camera.onPreCull += MyOnPreCull;
#else
Camera.onPreRender += MyOnPreCull;
#endif
}
private void LateUpdate()
{
SyncEnabledState();
if (Setting_CopyMaterials && AvatarCloneTestMod.EntryCopyMaterials.Value)
SyncMaterials();
if (Setting_CopyBlendShapes && AvatarCloneTestMod.EntryCopyBlendShapes.Value)
SyncBlendShapes();
}
private void OnDestroy()
{
// Likely a Unity bug with where we can touch shadowCastingMode & forceRenderingOff
#if !UNITY_EDITOR
Camera.onPreCull -= MyOnPreCull;
#else
Camera.onPreRender -= MyOnPreCull;
#endif
}
#endregion Unity Events
}

View file

@ -0,0 +1,39 @@
using ABI.CCK.Components;
using UnityEngine;
namespace NAK.AvatarCloneTest;
public class AvatarCloneExclusion : IExclusionBehaviour
{
public readonly Dictionary<SkinnedMeshRenderer, List<int>> skinnedToBoneIndex = new();
public readonly List<Transform> affectedTransforms = new();
public readonly List<Renderer> affectedRenderers = new();
public HashSet<Transform> affectedTransformSet = new();
private readonly AvatarClone _cloneSystem;
private readonly Transform _target;
internal Transform _shrinkBone;
public bool isImmuneToGlobalState { get; set; }
public AvatarCloneExclusion(AvatarClone cloneSystem, Transform target)
{
_cloneSystem = cloneSystem;
_target = target;
}
public void UpdateExclusions(bool isShown, bool shrinkToZero)
{
if (_shrinkBone == null)
{
// Create shrink bone parented directly to target
_shrinkBone = new GameObject($"{_target.name}_Shrink").transform;
_shrinkBone.SetParent(_target, false);
}
// Set scale based on shrink mode
_shrinkBone.localScale = shrinkToZero ? Vector3.zero : Vector3.positiveInfinity;
// Replace the bone references with the shrink bone for the indicies we modify
_cloneSystem.HandleExclusionUpdate(this, isShown);
}
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>LocalCloneFix</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>TRACE;TRACE;</DefineConstants>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,81 @@
using ABI_RC.Core;
using ABI_RC.Core.EventSystem;
using ABI_RC.Core.Savior;
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<bool> 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<bool> EntryCloneMeshRenderers =
Category.CreateEntry("clone_mesh_renderers", false,
"Clone Mesh Renderers", description: "Clones the mesh renderers from the original avatar to the clone.");
internal static readonly MelonPreferences_Entry<bool> 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<bool> EntryCopyMaterials =
Category.CreateEntry("copy_materials", true,
"Copy Materials", description: "Copies the materials 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<UnityEngine.Camera>())
{
if (camera.tag != "MainCamera")
{
camera.cullingMask &= ~(1 << CVRLayers.PlayerClone);
}
}
}
// if pressing ctrl + r, reload avatar
if (Input.GetKey(KeyCode.LeftControl) && Input.GetKeyDown(KeyCode.R))
{
var player = MetaPort.Instance.currentAvatarGuid;
AssetManagement.Instance.LoadLocalAvatar(player);
}
}
#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
}

View file

@ -0,0 +1,22 @@
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<AvatarClone>();
return false;
}
}

View file

@ -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.3";
public const string Author = "NotAKidoS";
}

View file

@ -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.

View file

@ -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"
}

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Mute SDraw's funny warning, because he only compilled for x64 while most people compile for both -->
<NoWarn>$(NoWarn);MSB3270</NoWarn>
</PropertyGroup>
<!-- Didn't put in the Directory.Build.props because it spams funny warnings... -->
<ItemGroup>
<Reference Include="ECM2">
<HintPath>..\.ManagedLibs\ECM2.dll</HintPath>
</Reference>
<Reference Include="ml_prm">
<HintPath>$(MsBuildThisFileDirectory)\..\.ManagedLibs\ml_prm.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ChatBox">
<HintPath>$(MsBuildThisFileDirectory)\..\.ManagedLibs\ChatBox.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,14 @@
using ABI_RC.Systems.InputManagement;
using HarmonyLib;
namespace NAK.ChatBoxExtensions.HarmonyPatches;
public class CVRInputManagerPatches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(CVRInputManager), nameof(CVRInputManager.Start))]
private static void Postfix_CVRInputManager_Start(ref CVRInputManager __instance)
{
__instance.AddInputModule(ChatBoxExtensions.InputModule);
}
}

View file

@ -0,0 +1,25 @@
using ABI_RC.Systems.InputManagement;
using UnityEngine;
namespace NAK.ChatBoxExtensions.InputModules;
internal class InputModuleChatBoxExtensions : CVRInputModule
{
public bool jump = false;
public float emote = -1;
public Vector2 lookVector = Vector2.zero;
public Vector3 movementVector = Vector3.zero;
public override void UpdateInput()
{
CVRInputManager.Instance.jump |= jump;
CVRInputManager.Instance.movementVector += movementVector;
CVRInputManager.Instance.lookVector += lookVector;
if (emote > 0) CVRInputManager.Instance.emote = emote;
jump = false;
emote = -1;
lookVector = Vector3.zero;
movementVector = Vector3.zero;
}
}

View file

@ -0,0 +1,56 @@
using ABI_RC.Core.Networking;
using ABI_RC.Core.Player;
namespace NAK.ChatBoxExtensions.Integrations;
public class CommandBase
{
internal static bool IsCommandForAll(string argument)
{
if (String.IsNullOrWhiteSpace(argument)) return false;
return argument == "*" || argument.StartsWith("@a") || argument.StartsWith("@e");
}
internal static bool IsCommandForLocalPlayer(string argument)
{
if (String.IsNullOrWhiteSpace(argument)) return false;
if (argument.Contains("*"))
{
string partialName = argument.Replace("*", "").Trim();
if (String.IsNullOrWhiteSpace(partialName)) return false;
return AuthManager.Username.Contains(partialName);
}
return AuthManager.Username == argument;
}
internal static void LocalCommandIgnoreOthers(string argument, Action<string[]> callback)
{
string[] args = argument.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).ToArray();
// will fail if arguments are specified which arent local player
if (args.Length == 0 || IsCommandForAll(args[0]) || IsCommandForLocalPlayer(args[0])) callback(args);
}
//remote must specify exact player, wont respawn to all
internal static void RemoteCommandListenForSelf(string argument, Action<string[]> callback)
{
string[] args = argument.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).ToArray();
if (args.Length == 0) return;
if (IsCommandForLocalPlayer(args[0])) callback(args);
}
// remote must specify player or all, ignore commands without arguments
internal static void RemoteCommandListenForAll(string argument, Action<string[]> callback)
{
string[] args = argument.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).ToArray();
if (args.Length == 0) return;
if (IsCommandForAll(args[0]) || IsCommandForLocalPlayer(args[0])) callback(args);
}
internal static string GetPlayerUsername(string guid)
{
return CVRPlayerManager.Instance.TryGetPlayerName(guid);
}
}

View file

@ -0,0 +1,91 @@
using Kafe.ChatBox;
namespace NAK.ChatBoxExtensions.Integrations;
internal class ChatBoxCommands : CommandBase
{
public static void RegisterCommands()
{
bool awaitingPing = false;
DateTime pingTime = DateTime.MinValue; // store the time when "ping" command was sent
Commands.RegisterCommand("ping",
onCommandSent: (message, sound, displayMsg) =>
{
pingTime = DateTime.Now;
awaitingPing = true;
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForSelf(message, args =>
{
API.SendMessage("/pong " + GetPlayerUsername(sender), false, true, true);
});
});
Commands.RegisterCommand("pong",
onCommandSent: null,
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForSelf(message, args =>
{
if (awaitingPing)
{
awaitingPing = false;
TimeSpan timeSincePing = DateTime.Now - pingTime; // calculate the time difference
API.SendMessage($"Time since ping: {timeSincePing.TotalMilliseconds}ms", false, true, true);
return;
}
API.SendMessage($"You have to ping first, {GetPlayerUsername(sender)}!", false, true, true);
});
});
Commands.RegisterCommand("sudo",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, args =>
{
if (args.Length > 1)
{
string command = string.Join(" ", args.Skip(1));
API.SendMessage($"/{command}", false, true, true);
}
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, args =>
{
if (args.Length > 1)
{
string command = string.Join(" ", args.Skip(1));
API.SendMessage($"/{command}", false, true, true);
}
});
});
Commands.RegisterCommand("say",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, args =>
{
if (args.Length > 0)
{
string text = string.Join(" ", args);
API.SendMessage(text, false, true, true);
}
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, args =>
{
if (args.Length > 0)
{
string text = string.Join(" ", args);
API.SendMessage(text, false, true, true);
}
});
});
}
}

View file

@ -0,0 +1,32 @@
using ABI_RC.Core.Player;
namespace NAK.ChatBoxExtensions.Integrations;
internal class ChilloutVRAASCommands : CommandBase
{
public static void RegisterCommands()
{
// /aas [target player] [name] [value]
Commands.RegisterCommand("aas",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, args =>
{
if (args.Length > 2 && float.TryParse(args[2], out float value))
{
PlayerSetup.Instance.ChangeAnimatorParam(args[1], value);
}
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, args =>
{
if (args.Length > 2 && float.TryParse(args[2], out float value))
{
PlayerSetup.Instance.ChangeAnimatorParam(args[1], value);
}
});
});
}
}

View file

@ -0,0 +1,118 @@
using ABI_RC.Core;
using ABI_RC.Core.Base;
using ABI_RC.Core.Player;
using ABI_RC.Systems.Movement;
using UnityEngine;
namespace NAK.ChatBoxExtensions.Integrations;
internal class ChilloutVRBaseCommands : CommandBase
{
public static void RegisterCommands()
{
Commands.RegisterCommand("respawn",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, args =>
{
RootLogic.Instance.Respawn();
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, (args) =>
{
RootLogic.Instance.Respawn();
});
});
Commands.RegisterCommand("mute",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, args =>
{
AudioManagement.SetMicrophoneActive(false);
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, args =>
{
AudioManagement.SetMicrophoneActive(false);
});
});
// teleport [x] [y] [z]
Commands.RegisterCommand("teleport",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, args =>
{
if (args.Length > 2 && float.TryParse(args[0], out float x) && float.TryParse(args[1], out float y) && float.TryParse(args[2], out float z))
{
BetterBetterCharacterController.Instance.TeleportPlayerTo(new Vector3(x, y, z), false, false);
}
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, args =>
{
if (args.Length > 2 && float.TryParse(args[0], out float x) && float.TryParse(args[1], out float y) && float.TryParse(args[2], out float z))
{
BetterBetterCharacterController.Instance.TeleportPlayerTo(new Vector3(x, y, z), false, false);
}
});
});
// tp [x] [y] [z]
Commands.RegisterCommand("tp",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, args =>
{
if (args.Length > 2 && float.TryParse(args[0], out float x) && float.TryParse(args[1], out float y) && float.TryParse(args[2], out float z))
{
BetterBetterCharacterController.Instance.TeleportPlayerTo(new Vector3(x, y, z), false, false);
}
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, args =>
{
if (args.Length > 2 && float.TryParse(args[0], out float x) && float.TryParse(args[1], out float y) && float.TryParse(args[2], out float z))
{
BetterBetterCharacterController.Instance.TeleportPlayerTo(new Vector3(x, y, z), false, false);
}
});
});
// teleport [player]
Commands.RegisterCommand("teleport",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, args =>
{
if (args.Length > 0)
{
string player = args[0];
CVRPlayerEntity playerEnt = CVRPlayerManager.Instance.NetworkPlayers.FirstOrDefault(x => x.Username == player);
if (playerEnt != null) BetterBetterCharacterController.Instance.TeleportPlayerTo(playerEnt.PuppetMaster.transform.position, false, false);
}
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, args =>
{
if (args.Length > 0)
{
string player = args[0];
CVRPlayerEntity playerEnt = CVRPlayerManager.Instance.NetworkPlayers.FirstOrDefault(x => x.Username == player);
if (playerEnt != null) BetterBetterCharacterController.Instance.TeleportPlayerTo(playerEnt.PuppetMaster.transform.position, false, false);
}
});
});
}
}

View file

@ -0,0 +1,53 @@
namespace NAK.ChatBoxExtensions.Integrations;
internal class ChilloutVRInputCommands : CommandBase
{
public static void RegisterCommands()
{
Commands.RegisterCommand("emote",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, args =>
{
if (args.Length > 0 && int.TryParse(args[0], out int emote))
{
ChatBoxExtensions.InputModule.emote = (float)emote;
}
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, args =>
{
if (args.Length > 1 && int.TryParse(args[1], out int emote))
{
ChatBoxExtensions.InputModule.emote = (float)emote;
}
});
});
Commands.RegisterCommand("jump",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, args =>
{
if (args.Length > 0 && bool.TryParse(args[0], out bool jump))
{
ChatBoxExtensions.InputModule.jump = jump;
return;
}
ChatBoxExtensions.InputModule.jump = true;
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, args =>
{
if (bool.TryParse(args[0], out bool jump))
{
ChatBoxExtensions.InputModule.jump = jump;
}
});
});
}
}

View file

@ -0,0 +1,54 @@
namespace NAK.ChatBoxExtensions.Integrations;
public static class Commands
{
private const string Character = "/";
private static readonly List<Command> CommandList = new();
internal static void InitializeCommandHandlers()
{
Kafe.ChatBox.API.OnMessageSent += msg => HandleSentCommand(msg.Message, msg.TriggerNotification, msg.DisplayOnChatBox);
Kafe.ChatBox.API.OnMessageReceived += msg => HandleReceivedCommand(msg.SenderGuid, msg.Message, msg.TriggerNotification, msg.DisplayOnChatBox);
}
internal static void RegisterCommand(string prefix, Action<string, bool, bool> onCommandSent = null, Action<string, string, bool, bool> onCommandReceived = null)
{
var cmd = new Command { Prefix = prefix, OnCommandSent = onCommandSent, OnCommandReceived = onCommandReceived };
CommandList.Add(cmd);
}
internal static void UnregisterCommand(string prefix)
{
CommandList.RemoveAll(cmd => cmd.Prefix == prefix);
}
private class Command
{
internal string Prefix;
// Command Sent (message)
internal Action<string, bool, bool> OnCommandSent;
// Command Sent (sender guid, message)
internal Action<string, string, bool, bool> OnCommandReceived;
}
private static void HandleSentCommand(string message, bool notification, bool displayMsg)
{
if (!message.StartsWith(Character)) return;
foreach (var command in CommandList.Where(command => message.StartsWith(Character + command.Prefix)))
{
command.OnCommandSent?.Invoke(message, notification, displayMsg);
}
}
private static void HandleReceivedCommand(string sender, string message, bool notification, bool displayMsg)
{
if (!message.StartsWith(Character)) return;
foreach (var command in CommandList.Where(command => message.StartsWith(Character + command.Prefix)))
{
command.OnCommandReceived?.Invoke(sender, message, notification, displayMsg);
}
}
}

View file

@ -0,0 +1,89 @@
using ml_prm;
namespace NAK.ChatBoxExtensions.Integrations;
internal class PlayerRagdollModCommands : CommandBase
{
public static void RegisterCommands()
{
Commands.RegisterCommand("unragdoll",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, (args) =>
{
if (RagdollController.Instance.IsRagdolled())
{
RagdollController.Instance.SwitchRagdoll();
}
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, (args) =>
{
if (RagdollController.Instance.IsRagdolled())
{
RagdollController.Instance.SwitchRagdoll();
}
});
});
Commands.RegisterCommand("ragdoll",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, (args) =>
{
bool switchRagdoll = true;
if (args.Length > 0 && bool.TryParse(args[0], out bool state))
{
switchRagdoll = state != RagdollController.Instance.IsRagdolled();
}
if (switchRagdoll)
{
RagdollController.Instance.SwitchRagdoll();
}
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, (args) =>
{
bool switchRagdoll = true;
if (args.Length > 1 && bool.TryParse(args[1], out bool state))
{
switchRagdoll = state != RagdollController.Instance.IsRagdolled();
}
if (switchRagdoll)
{
RagdollController.Instance.SwitchRagdoll();
}
});
});
Commands.RegisterCommand("kill",
onCommandSent: (message, sound, displayMsg) =>
{
LocalCommandIgnoreOthers(message, (args) =>
{
if (!RagdollController.Instance.IsRagdolled())
{
RagdollController.Instance.SwitchRagdoll();
}
});
},
onCommandReceived: (sender, message, sound, displayMsg) =>
{
RemoteCommandListenForAll(message, (args) =>
{
if (!RagdollController.Instance.IsRagdolled())
{
RagdollController.Instance.SwitchRagdoll();
}
});
});
}
}

View file

@ -0,0 +1,54 @@
using MelonLoader;
using NAK.ChatBoxExtensions.InputModules;
namespace NAK.ChatBoxExtensions;
public class ChatBoxExtensions : MelonMod
{
internal static MelonLogger.Instance Logger;
internal static InputModuleChatBoxExtensions InputModule = new();
public override void OnInitializeMelon()
{
Logger = LoggerInstance;
if (RegisteredMelons.All(it => it.Info.Name != "ChatBox"))
{
Logger.Error("ChatBox was not found!");
return;
}
ApplyIntegrations();
}
private void ApplyIntegrations()
{
Integrations.Commands.InitializeCommandHandlers();
Integrations.ChatBoxCommands.RegisterCommands();
Integrations.ChilloutVRBaseCommands.RegisterCommands();
Integrations.ChilloutVRAASCommands.RegisterCommands();
Integrations.ChilloutVRInputCommands.RegisterCommands();
ApplyPatches(typeof(HarmonyPatches.CVRInputManagerPatches));
if (RegisteredMelons.Any(it => it.Info.Name == "PlayerRagdollMod"))
{
Integrations.PlayerRagdollModCommands.RegisterCommands();
}
}
private void ApplyPatches(Type type)
{
try
{
HarmonyInstance.PatchAll(type);
}
catch (Exception e)
{
Logger.Msg($"Failed while patching {type.Name}!");
Logger.Error(e);
}
}
}

View file

@ -0,0 +1,33 @@
using ChatBoxExtensions.Properties;
using MelonLoader;
using System.Reflection;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.ChatBoxExtensions))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.ChatBoxExtensions))]
[assembly: MelonInfo(
typeof(NAK.ChatBoxExtensions.ChatBoxExtensions),
nameof(NAK.ChatBoxExtensions),
AssemblyInfoParams.Version,
AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ChatBoxExtensions"
)]
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
[assembly: MelonAdditionalDependencies("ChatBox")]
[assembly: MelonOptionalDependencies("PlayerRagdollMod")]
[assembly: HarmonyDontPatchAll]
namespace ChatBoxExtensions.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.3";
public const string Author = "NotAKidoS";
}

View file

@ -0,0 +1,23 @@
{
"_id": 129,
"name": "ChatBoxExtensions",
"modversion": "1.0.1",
"gameversion": "2022r170p1",
"loaderversion": "0.6.1",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "Prevents VRIK from using toe bones in VR & optionaly FBT.\n\nVRIK calculates weird center of mass when toes are mapped, so it is sometimes desired to unmap toes to prevent an avatars feet from resting far back.\n\nPlease see the README for relevant imagery detailing the problem.",
"searchtags": [
"toes",
"vrik",
"ik",
"feet"
],
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r3/ChatBoxExtensions.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ChatBoxExtensions/",
"changelog": "",
"embedcolor": "#ffc700"
}