diff --git a/VisualCloneFix/Main.cs b/VisualCloneFix/Main.cs new file mode 100644 index 0000000..04c0e73 --- /dev/null +++ b/VisualCloneFix/Main.cs @@ -0,0 +1,174 @@ +using System.Reflection; +using ABI_RC.Core.Player.LocalClone; +using ABI_RC.Core.Player.TransformHider; +using ABI.CCK.Components; +using HarmonyLib; +using MelonLoader; +using UnityEngine; + +namespace NAK.VisualCloneFix; + +public class VisualCloneFixMod : MelonMod +{ + #region Melon Preferences + + private static readonly MelonPreferences_Category Category = + MelonPreferences.CreateCategory(nameof(VisualCloneFix)); + + private static readonly MelonPreferences_Entry EntryUseVisualClone = + Category.CreateEntry("use_visual_clone", true, + "Use Visual Clone", description: "Uses the potentially faster Visual Clone setup for the local avatar."); + + #endregion Melon Preferences + + #region Melon Events + + public override void OnInitializeMelon() + { + HarmonyInstance.Patch( // add option back as melonpref + typeof(TransformHiderUtils).GetMethod(nameof(TransformHiderUtils.SetupAvatar), + BindingFlags.Public | BindingFlags.Static), + prefix: new HarmonyMethod(typeof(VisualCloneFixMod).GetMethod(nameof(OnSetupAvatar), + BindingFlags.NonPublic | BindingFlags.Static)) + ); + + HarmonyInstance.Patch( // fix binding of ids to exclusions + typeof(LocalCloneHelper).GetMethod(nameof(LocalCloneHelper.CollectTransformToExclusionMap), + BindingFlags.NonPublic | BindingFlags.Static), + prefix: new HarmonyMethod(typeof(VisualCloneFixMod).GetMethod(nameof(CollectTransformToExclusionMap), + BindingFlags.NonPublic | BindingFlags.Static)) + ); + + HarmonyInstance.Patch( // fix binding of exclusion ids to compute buffer + typeof(SkinnedLocalClone).GetMethod(nameof(SkinnedLocalClone.FindExclusionVertList), + BindingFlags.NonPublic | BindingFlags.Static), + prefix: new HarmonyMethod(typeof(VisualCloneFixMod).GetMethod(nameof(FindExclusionVertList), + BindingFlags.NonPublic | BindingFlags.Static)) + ); + } + + #endregion Melon Events + + #region Transform Hider Setup + + private static bool OnSetupAvatar(GameObject avatar) + { + if (!EntryUseVisualClone.Value) + return true; + + LocalCloneHelper.SetupAvatar(avatar); + return false; + } + + #endregion Transform Hider Setup + + #region FPR Exclusion Processing + + private static bool CollectTransformToExclusionMap( + Component root, Transform headBone, + ref Dictionary __result) + { + // add an fpr exclusion to the head bone + FPRExclusion headExclusion = headBone.gameObject.AddComponent(); + MeshHiderExclusion headExclusionBehaviour = new(); + headExclusion.target = headBone; + headExclusion.behaviour = headExclusionBehaviour; + headExclusionBehaviour.id = 1; // head bone is always 1 + + // body is 0, ensure it is not masked away + //LocalCloneManager.cullingMask &= ~(1 << 0); + + // get all FPRExclusions + var fprExclusions = root.GetComponentsInChildren(true).ToList(); + + // get all valid exclusion targets, and destroy invalid exclusions + Dictionary exclusionTargets = new(); + + int nextId = 2; + for (int i = fprExclusions.Count - 1; i >= 0; i--) + { + FPRExclusion exclusion = fprExclusions[i]; + if (exclusion.target == null + || exclusionTargets.ContainsKey(exclusion.target) + || !exclusion.target.gameObject.scene.IsValid()) + { + UnityEngine.Object.Destroy(exclusion); + fprExclusions.RemoveAt(i); + continue; + } + + if (exclusion.behaviour == null) // head exclusion is already created + { + MeshHiderExclusion meshHiderExclusion = new(); + exclusion.behaviour = meshHiderExclusion; + meshHiderExclusion.id = nextId++; + } + + // first to add wins + exclusionTargets.TryAdd(exclusion.target, exclusion); + } + + // process each FPRExclusion (recursive) + foreach (FPRExclusion exclusion in fprExclusions) + { + ProcessExclusion(exclusion, exclusion.target); + exclusion.UpdateExclusions(); // initial state + } + + // log totals + //LocalCloneMod.Logger.Msg($"Exclusions: {fprExclusions.Count}"); + __result = exclusionTargets; + return false; + + void ProcessExclusion(FPRExclusion exclusion, Transform transform) + { + if (exclusionTargets.ContainsKey(transform) + && exclusionTargets[transform] != exclusion) return; // found other exclusion root + + //exclusion.affectedChildren.Add(transform); // associate with the exclusion + exclusionTargets.TryAdd(transform, exclusion); // add to the dictionary (yes its wasteful) + + foreach (Transform child in transform) + ProcessExclusion(exclusion, child); // process children + } + } + + #endregion + + #region Head Hiding Methods + + public static bool FindExclusionVertList( + SkinnedMeshRenderer renderer, IReadOnlyDictionary exclusions, + ref int[] __result) + { + Mesh sharedMesh = renderer.sharedMesh; + var boneWeights = sharedMesh.boneWeights; + int[] vertexIndices = new int[sharedMesh.vertexCount]; + + // Pre-map bone transforms to their exclusion ids if applicable + int[] boneIndexToExclusionId = new int[renderer.bones.Length]; + for (int i = 0; i < renderer.bones.Length; i++) + if (exclusions.TryGetValue(renderer.bones[i], out FPRExclusion exclusion)) + boneIndexToExclusionId[i] = ((MeshHiderExclusion)exclusion.behaviour).id; + + const float minWeightThreshold = 0.2f; + for (int i = 0; i < boneWeights.Length; i++) + { + BoneWeight weight = boneWeights[i]; + + if (weight.weight0 > minWeightThreshold) + vertexIndices[i] = boneIndexToExclusionId[weight.boneIndex0]; + else if (weight.weight1 > minWeightThreshold) + vertexIndices[i] = boneIndexToExclusionId[weight.boneIndex1]; + else if (weight.weight2 > minWeightThreshold) + vertexIndices[i] = boneIndexToExclusionId[weight.boneIndex2]; + else if (weight.weight3 > minWeightThreshold) + vertexIndices[i] = boneIndexToExclusionId[weight.boneIndex3]; + } + + __result = vertexIndices; + return false; + } + + #endregion Head Hiding Methods +} \ No newline at end of file diff --git a/VisualCloneFix/Properties/AssemblyInfo.cs b/VisualCloneFix/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ef699bd --- /dev/null +++ b/VisualCloneFix/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using MelonLoader; +using NAK.VisualCloneFix.Properties; +using System.Reflection; + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.VisualCloneFix))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.VisualCloneFix))] + +[assembly: MelonInfo( + typeof(NAK.VisualCloneFix.VisualCloneFixMod), + nameof(NAK.VisualCloneFix), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/VisualCloneFix" +)] + +[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.VisualCloneFix.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/VisualCloneFix/README.md b/VisualCloneFix/README.md new file mode 100644 index 0000000..6588797 --- /dev/null +++ b/VisualCloneFix/README.md @@ -0,0 +1,16 @@ +# 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. + +--- + +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/VisualCloneFix/VisualCloneFix.csproj b/VisualCloneFix/VisualCloneFix.csproj new file mode 100644 index 0000000..8dc4bcd --- /dev/null +++ b/VisualCloneFix/VisualCloneFix.csproj @@ -0,0 +1,6 @@ + + + + LocalCloneFix + + diff --git a/VisualCloneFix/format.json b/VisualCloneFix/format.json new file mode 100644 index 0000000..fcd6381 --- /dev/null +++ b/VisualCloneFix/format.json @@ -0,0 +1,23 @@ +{ + "_id": -1, + "name": "VisualCloneFix", + "modversion": "1.0.0", + "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.", + "searchtags": [ + "visual", + "clone", + "head", + "hiding" + ], + "requirements": [ + "None" + ], + "downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r34/VisualCloneFix.dll", + "sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/VisualCloneFix/", + "changelog": "- Initial release", + "embedcolor": "#f61963" +} \ No newline at end of file