From 0f5e1484d1645e2959e004a481339f101c091da7 Mon Sep 17 00:00:00 2001 From: SDraw Date: Thu, 4 May 2023 11:40:47 +0300 Subject: [PATCH] New mod: PlayerMovementCopycat Collider radius upon avatar scaling --- README.md | 7 +- ml_amt/Main.cs | 25 ++- ml_amt/Properties/AssemblyInfo.cs | 6 +- ml_mods_cvr.sln | 6 + ml_pmc/Main.cs | 130 ++++++++++++ ml_pmc/ModUi.cs | 136 +++++++++++++ ml_pmc/PoseCopycat.cs | 322 ++++++++++++++++++++++++++++++ ml_pmc/Properties/AssemblyInfo.cs | 12 ++ ml_pmc/PuppetParser.cs | 156 +++++++++++++++ ml_pmc/README.md | 21 ++ ml_pmc/Settings.cs | 120 +++++++++++ ml_pmc/Utils.cs | 81 ++++++++ ml_pmc/ml_pmc.csproj | 101 ++++++++++ ml_pmc/ml_pmc.csproj.user | 6 + ml_pmc/resources/dancing.png | Bin 0 -> 4168 bytes ml_pmc/resources/dancing_on.png | Bin 0 -> 4604 bytes 16 files changed, 1122 insertions(+), 7 deletions(-) create mode 100644 ml_pmc/Main.cs create mode 100644 ml_pmc/ModUi.cs create mode 100644 ml_pmc/PoseCopycat.cs create mode 100644 ml_pmc/Properties/AssemblyInfo.cs create mode 100644 ml_pmc/PuppetParser.cs create mode 100644 ml_pmc/README.md create mode 100644 ml_pmc/Settings.cs create mode 100644 ml_pmc/Utils.cs create mode 100644 ml_pmc/ml_pmc.csproj create mode 100644 ml_pmc/ml_pmc.csproj.user create mode 100644 ml_pmc/resources/dancing.png create mode 100644 ml_pmc/resources/dancing_on.png diff --git a/README.md b/README.md index 2b3f8e8..f8586f3 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ Merged set of MelonLoader mods for ChilloutVR. | Full name | Short name | Latest version | Available in [CVRMA](https://github.com/knah/CVRMelonAssistant) | Current Status | Notes | |-----------|------------|----------------|-----------------------------------------------------------------|----------------|-------| | Avatar Change Info | ml_aci | 1.0.3 | Retired | Retired | Superseded by `Extended Game Notifications` -| Avatar Motion Tweaker | ml_amt | 1.2.7 | Yes | Working | +| Avatar Motion Tweaker | ml_amt | 1.2.8 | Yes, update review | Working | | Desktop Head Tracking | ml_dht | 1.1.3 | Yes | Working | | Desktop Reticle Switch | ml_drs | 1.0.0 | Yes | Working | | Extended Game Notifications | ml_egn | 1.0.2 | Yes | Working | Four Point Tracking | ml_fpt | 1.0.9 | Retired | Deprecated | In-game feature since 2022r170 update -| Leap Motion Extension | ml_lme | 1.3.6 | Yes, update review | Working | +| Leap Motion Extension | ml_lme | 1.3.6 | Yes | Working | | Pickup Arm Movement | ml_pam | 1.0.4 | Yes | Working | -| Player Ragdoll Mod | ml_prm | 1.0.3 | Yes, update review | Working | +| Player Movement Copycat | ml_pmc | 1.0.0 | On review | Working | +| Player Ragdoll Mod | ml_prm | 1.0.4 | Yes, update review | Working | | Server Connection Info | ml_sci | 1.0.2 | Retired | Retired | Superseded by `Extended Game Notifications` diff --git a/ml_amt/Main.cs b/ml_amt/Main.cs index c6ccd70..6db0fdd 100644 --- a/ml_amt/Main.cs +++ b/ml_amt/Main.cs @@ -68,12 +68,17 @@ namespace ml_amt ); } - // Alternative collider height + // Alternative collider height and radius HarmonyInstance.Patch( typeof(MovementSystem).GetMethod("UpdateCollider", BindingFlags.NonPublic | BindingFlags.Instance), new HarmonyLib.HarmonyMethod(typeof(AvatarMotionTweaker).GetMethod(nameof(OnUpdateCollider_Prefix), BindingFlags.Static | BindingFlags.NonPublic)), null ); + HarmonyInstance.Patch( + typeof(PlayerSetup).GetMethod("SetupIKScaling", BindingFlags.NonPublic | BindingFlags.Instance), + null, + new HarmonyLib.HarmonyMethod(typeof(AvatarMotionTweaker).GetMethod(nameof(OnSetupIKScaling_Postfix), BindingFlags.Static | BindingFlags.NonPublic)) + ); // AAS overriding fix HarmonyInstance.Patch( @@ -252,7 +257,25 @@ namespace ml_amt return false; } + static void OnSetupIKScaling_Postfix( + ref PlayerSetup __instance, + float ____avatarHeight + ) + { + if(!Settings.CollisionScale) + return; + try + { + __instance._movementSystem.UpdateAvatarHeight(Mathf.Clamp(____avatarHeight, 0.05f, float.MaxValue), true); + } + catch(System.Exception l_exception) + { + MelonLoader.MelonLogger.Error(l_exception); + } + } + + // AnimatorOverrideController runtime animation replacement fix static void OnOverride_Prefix(ref CVRAnimatorManager __instance, out AnimatorAnalyzer __state) { __state = new AnimatorAnalyzer(); diff --git a/ml_amt/Properties/AssemblyInfo.cs b/ml_amt/Properties/AssemblyInfo.cs index 2a256a4..7100101 100644 --- a/ml_amt/Properties/AssemblyInfo.cs +++ b/ml_amt/Properties/AssemblyInfo.cs @@ -1,10 +1,10 @@ using System.Reflection; [assembly: AssemblyTitle("AvatarMotionTweaker")] -[assembly: AssemblyVersion("1.2.7")] -[assembly: AssemblyFileVersion("1.2.7")] +[assembly: AssemblyVersion("1.2.8")] +[assembly: AssemblyFileVersion("1.2.8")] -[assembly: MelonLoader.MelonInfo(typeof(ml_amt.AvatarMotionTweaker), "AvatarMotionTweaker", "1.2.7", "SDraw", "https://github.com/SDraw/ml_mods_cvr")] +[assembly: MelonLoader.MelonInfo(typeof(ml_amt.AvatarMotionTweaker), "AvatarMotionTweaker", "1.2.8", "SDraw", "https://github.com/SDraw/ml_mods_cvr")] [assembly: MelonLoader.MelonGame(null, "ChilloutVR")] [assembly: MelonLoader.MelonOptionalDependencies("ml_prm", "ml_pmc")] [assembly: MelonLoader.MelonPlatform(MelonLoader.MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] diff --git a/ml_mods_cvr.sln b/ml_mods_cvr.sln index 19f4b60..94d3cbf 100644 --- a/ml_mods_cvr.sln +++ b/ml_mods_cvr.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ml_pam", "ml_pam\ml_pam.csp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ml_prm", "ml_prm\ml_prm.csproj", "{ABD2A720-2DE8-4EB3-BFC2-8F1C3D2ADA15}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ml_pmc", "ml_pmc\ml_pmc.csproj", "{758514D3-6E1A-4E05-A156-B4E5C74AB5C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -51,6 +53,10 @@ Global {ABD2A720-2DE8-4EB3-BFC2-8F1C3D2ADA15}.Debug|x64.Build.0 = Debug|x64 {ABD2A720-2DE8-4EB3-BFC2-8F1C3D2ADA15}.Release|x64.ActiveCfg = Release|x64 {ABD2A720-2DE8-4EB3-BFC2-8F1C3D2ADA15}.Release|x64.Build.0 = Release|x64 + {758514D3-6E1A-4E05-A156-B4E5C74AB5C4}.Debug|x64.ActiveCfg = Debug|x64 + {758514D3-6E1A-4E05-A156-B4E5C74AB5C4}.Debug|x64.Build.0 = Debug|x64 + {758514D3-6E1A-4E05-A156-B4E5C74AB5C4}.Release|x64.ActiveCfg = Release|x64 + {758514D3-6E1A-4E05-A156-B4E5C74AB5C4}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ml_pmc/Main.cs b/ml_pmc/Main.cs new file mode 100644 index 0000000..bb3a3ea --- /dev/null +++ b/ml_pmc/Main.cs @@ -0,0 +1,130 @@ +using ABI_RC.Core.Networking.IO.Social; +using ABI_RC.Core.Player; +using ABI_RC.Systems.MovementSystem; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace ml_pmc +{ + public class PlayerMovementCopycat : MelonLoader.MelonMod + { + static PlayerMovementCopycat ms_instance = null; + + PoseCopycat m_localCopycat = null; + + public override void OnInitializeMelon() + { + if(ms_instance == null) + ms_instance = this; + + Settings.Init(); + ModUi.Init(); + + HarmonyInstance.Patch( + typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.ClearAvatar)), + null, + new HarmonyLib.HarmonyMethod(typeof(PlayerMovementCopycat).GetMethod(nameof(OnAvatarClear_Postfix), BindingFlags.NonPublic | BindingFlags.Static)) + ); + HarmonyInstance.Patch( + typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.SetupAvatar)), + null, + new HarmonyLib.HarmonyMethod(typeof(PlayerMovementCopycat).GetMethod(nameof(OnSetupAvatar_Postfix), BindingFlags.Static | BindingFlags.NonPublic)) + ); + + MelonLoader.MelonCoroutines.Start(WaitForLocalPlayer()); + } + + public override void OnDeinitializeMelon() + { + if(ms_instance == this) + ms_instance = null; + + m_localCopycat = null; + } + + System.Collections.IEnumerator WaitForLocalPlayer() + { + while(PlayerSetup.Instance == null) + yield return null; + + m_localCopycat = PlayerSetup.Instance.gameObject.AddComponent(); + ModUi.CopySwitch += this.OnTargetSelect; + } + + void OnTargetSelect(string p_id) + { + if(m_localCopycat != null) + { + if(m_localCopycat.IsActive()) + m_localCopycat.SetTarget(null); + else + { + if(Friends.FriendsWith(p_id)) + { + if(CVRPlayerManager.Instance.GetPlayerPuppetMaster(p_id, out PuppetMaster l_puppetMaster)) + { + if(IsInSight(MovementSystem.Instance.proxyCollider, l_puppetMaster.GetComponent(), Utils.GetWorldMovementLimit())) + m_localCopycat.SetTarget(l_puppetMaster.gameObject); + else + ModUi.ShowAlert("Selected player is too far away or obstructed"); + } + else + ModUi.ShowAlert("Selected player isn't connected or ready yet"); + } + else + ModUi.ShowAlert("Selected player isn't your friend"); + } + } + } + + static bool IsInSight(CapsuleCollider p_source, CapsuleCollider p_target, float p_limit) + { + bool l_result = false; + if((p_source != null) && (p_target != null)) + { + Ray l_ray = new Ray(); + l_ray.origin = (p_source.transform.position + p_source.transform.rotation * p_source.center); + l_ray.direction = (p_target.transform.position + p_target.transform.rotation * p_target.center) - l_ray.origin; + List l_hits = Physics.RaycastAll(l_ray, p_limit, LayerMask.NameToLayer("UI Internal")).ToList(); + if(l_hits.Count > 0) + { + l_hits.Sort((a, b) => ((a.distance < b.distance) ? -1 : 1)); + l_result = (l_hits.First().collider.gameObject.transform.root == p_target.transform.root); + } + } + return l_result; + } + + // Patches + static void OnAvatarClear_Postfix() => ms_instance?.OnAvatarClear(); + void OnAvatarClear() + { + try + { + if(m_localCopycat != null) + m_localCopycat.OnAvatarClear(); + } + catch(Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + } + + static void OnSetupAvatar_Postfix() => ms_instance?.OnSetupAvatar(); + void OnSetupAvatar() + { + try + { + if(m_localCopycat != null) + m_localCopycat.OnAvatarSetup(); + } + catch(Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + } + } +} diff --git a/ml_pmc/ModUi.cs b/ml_pmc/ModUi.cs new file mode 100644 index 0000000..c537cd2 --- /dev/null +++ b/ml_pmc/ModUi.cs @@ -0,0 +1,136 @@ +using BTKUILib.UIObjects; +using BTKUILib.UIObjects.Components; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace ml_pmc +{ + static class ModUi + { + enum UiIndex + { + Toggle, + Position, + Rotation, + Gestures, + LookAtMix, + MirrorPose, + MirrorPosition, + MirrorRotation, + Reset + } + + internal static Action CopySwitch; + + static List ms_uiElements = null; + static string ms_selectedPlayer; + + internal static void Init() + { + ms_uiElements = new List(); + + BTKUILib.QuickMenuAPI.PrepareIcon("PlayerMovementCopycat", "PMC-Dancing", GetIconStream("dancing.png")); + BTKUILib.QuickMenuAPI.PrepareIcon("PlayerMovementCopycat", "PMC-Dancing-On", GetIconStream("dancing_on.png")); + + var l_category = BTKUILib.QuickMenuAPI.PlayerSelectPage.AddCategory("Player Movement Copycat", "PlayerMovementCopycat"); + + ms_uiElements.Add(l_category.AddButton("Copy movement", "PMC-Dancing", "Start/stop copy of player's movement")); + (ms_uiElements[(int)UiIndex.Toggle] as Button).OnPress += OnCopySwitch; + + ms_uiElements.Add(l_category.AddToggle("Apply position", "Apply local position change of target player", Settings.Position)); + (ms_uiElements[(int)UiIndex.Position] as ToggleButton).OnValueUpdated += (value) => OnToggleUpdate(UiIndex.Position, value); + + ms_uiElements.Add(l_category.AddToggle("Apply rotation", "Apply local rotation change of target player", Settings.Rotation)); + (ms_uiElements[(int)UiIndex.Rotation] as ToggleButton).OnValueUpdated += (value) => OnToggleUpdate(UiIndex.Rotation, value); + + ms_uiElements.Add(l_category.AddToggle("Copy gestures", "Copy gestures of target player", Settings.Gestures)); + (ms_uiElements[(int)UiIndex.Gestures] as ToggleButton).OnValueUpdated += (value) => OnToggleUpdate(UiIndex.Gestures, value); + + ms_uiElements.Add(l_category.AddToggle("Apply LookAtIK", "Mix target player pose and camera view direction (desktop only)", Settings.LookAtMix)); + (ms_uiElements[(int)UiIndex.LookAtMix] as ToggleButton).OnValueUpdated += (value) => OnToggleUpdate(UiIndex.LookAtMix, value); + + ms_uiElements.Add(l_category.AddToggle("Mirror pose", "Mirror target player pose", Settings.MirrorPose)); + (ms_uiElements[(int)UiIndex.MirrorPose] as ToggleButton).OnValueUpdated += (value) => OnToggleUpdate(UiIndex.MirrorPose, value); + + ms_uiElements.Add(l_category.AddToggle("Mirror position", "Mirror target player movement against 0YZ plane", Settings.MirrorPosition)); + (ms_uiElements[(int)UiIndex.MirrorPosition] as ToggleButton).OnValueUpdated += (value) => OnToggleUpdate(UiIndex.MirrorPosition, value); + + ms_uiElements.Add(l_category.AddToggle("Mirror rotation", "Mirror target player rotation against 0YZ plane", Settings.MirrorRotation)); + (ms_uiElements[(int)UiIndex.MirrorRotation] as ToggleButton).OnValueUpdated += (value) => OnToggleUpdate(UiIndex.MirrorRotation, value); + + ms_uiElements.Add(l_category.AddButton("Reset settings", "", "Reset mod's settings to default")); + (ms_uiElements[(int)UiIndex.Reset] as Button).OnPress += Reset; + + BTKUILib.QuickMenuAPI.OnPlayerSelected += (_, id) => ms_selectedPlayer = id; + PoseCopycat.OnActivityChange += UpdateToggleColor; + } + + static void OnCopySwitch() => CopySwitch?.Invoke(ms_selectedPlayer); + + static void OnToggleUpdate(UiIndex p_index, bool p_value, bool p_force = false) + { + switch(p_index) + { + case UiIndex.Position: + Settings.SetSetting(Settings.ModSetting.Position, p_value); + break; + + case UiIndex.Rotation: + Settings.SetSetting(Settings.ModSetting.Rotation, p_value); + break; + + case UiIndex.Gestures: + Settings.SetSetting(Settings.ModSetting.Gestures, p_value); + break; + + case UiIndex.LookAtMix: + Settings.SetSetting(Settings.ModSetting.LookAtMix, p_value); + break; + + case UiIndex.MirrorPose: + Settings.SetSetting(Settings.ModSetting.MirrorPose, p_value); + break; + + case UiIndex.MirrorPosition: + Settings.SetSetting(Settings.ModSetting.MirrorPosition, p_value); + break; + + case UiIndex.MirrorRotation: + Settings.SetSetting(Settings.ModSetting.MirrorRotation, p_value); + break; + } + + if(p_force) + (ms_uiElements[(int)p_index] as ToggleButton).ToggleValue = p_value; + } + + static void Reset() + { + OnToggleUpdate(UiIndex.Position, true, true); + OnToggleUpdate(UiIndex.Rotation, true, true); + OnToggleUpdate(UiIndex.Gestures, true, true); + OnToggleUpdate(UiIndex.LookAtMix, true, true); + OnToggleUpdate(UiIndex.MirrorPose, false, true); + OnToggleUpdate(UiIndex.MirrorPosition, false, true); + OnToggleUpdate(UiIndex.MirrorRotation, false, true); + } + + internal static void ShowAlert(string p_text) => BTKUILib.QuickMenuAPI.ShowAlertToast(p_text, 2); + + // Currently broken in BTKUILib, waiting for fix + static void UpdateToggleColor(bool p_state) + { + //(ms_uiElements[(int)UiIndex.Toggle] as Button).ButtonIcon = (p_state ? "PMC-Dancing-On" : "PMC-Dancing"); + //(ms_uiElements[(int)UiIndex.Toggle] as Button).ButtonText = (p_state ? "PMC-Dancing-On" : "PMC-Dancing"); + } + + static Stream GetIconStream(string p_name) + { + Assembly l_assembly = Assembly.GetExecutingAssembly(); + string l_assemblyName = l_assembly.GetName().Name; + return l_assembly.GetManifestResourceStream(l_assemblyName + ".resources." + p_name); + } + } +} diff --git a/ml_pmc/PoseCopycat.cs b/ml_pmc/PoseCopycat.cs new file mode 100644 index 0000000..a331c89 --- /dev/null +++ b/ml_pmc/PoseCopycat.cs @@ -0,0 +1,322 @@ +using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; +using ABI_RC.Systems.IK; +using ABI_RC.Systems.IK.SubSystems; +using ABI_RC.Systems.MovementSystem; +using RootMotion.FinalIK; +using UnityEngine; + +namespace ml_pmc +{ + [DisallowMultipleComponent] + public class PoseCopycat : MonoBehaviour + { + static readonly Vector4 ms_pointVector = new Vector4(0f, 0f, 0f, 1f); + + static public PoseCopycat Instance { get; private set; } = null; + static internal System.Action OnActivityChange; + + Animator m_animator = null; + VRIK m_vrIk = null; + float m_ikWeight = 1f; + LookAtIK m_lookAtIk = null; + float m_lookIkWeight = 1f; + bool m_sitting = false; + bool m_inVr = false; + + bool m_active = false; + float m_distanceLimit = float.MaxValue; + bool m_fingerTracking = false; + + HumanPoseHandler m_poseHandler = null; + HumanPose m_pose; + PuppetParser m_puppetParser = null; + + internal PoseCopycat() + { + if(Instance == null) + Instance = this; + } + ~PoseCopycat() + { + if(Instance == this) + Instance = null; + } + + // Unity events + void Update() + { + m_sitting = (MovementSystem.Instance.lastSeat != null); + + if(m_active && (m_puppetParser != null)) + { + OverrideIK(); + + if(m_puppetParser.HasAnimator()) + { + bool l_mirror = Settings.MirrorPose; + + if(Settings.Gestures) + { + CVRInputManager.Instance.gestureLeft = (l_mirror ? m_puppetParser.GetRightGesture() : m_puppetParser.GetLeftGesture()); + CVRInputManager.Instance.gestureRight = (l_mirror ? m_puppetParser.GetLeftGesture() : m_puppetParser.GetRightGesture()); + } + + if(m_puppetParser.HasFingerTracking()) + { + m_fingerTracking = true; + + CVRInputManager.Instance.individualFingerTracking = true; + IKSystem.Instance.FingerSystem.controlActive = true; + + ref float[] l_curls = ref m_puppetParser.GetFingerCurls(); + + CVRInputManager.Instance.fingerCurlLeftThumb = l_curls[l_mirror ? 5 : 0]; + CVRInputManager.Instance.fingerCurlLeftIndex = l_curls[l_mirror ? 6 : 1]; + CVRInputManager.Instance.fingerCurlLeftMiddle = l_curls[l_mirror ? 7 : 2]; + CVRInputManager.Instance.fingerCurlLeftRing = l_curls[l_mirror ? 8 : 3]; + CVRInputManager.Instance.fingerCurlLeftPinky = l_curls[l_mirror ? 9 : 4]; + CVRInputManager.Instance.fingerCurlRightThumb = l_curls[l_mirror ? 0 : 5]; + CVRInputManager.Instance.fingerCurlRightIndex = l_curls[l_mirror ? 1 : 6]; + CVRInputManager.Instance.fingerCurlRightMiddle = l_curls[l_mirror ? 2 : 7]; + CVRInputManager.Instance.fingerCurlRightRing = l_curls[l_mirror ? 3 : 8]; + CVRInputManager.Instance.fingerCurlRightPinky = l_curls[l_mirror ? 4 : 9]; + + IKSystem.Instance.FingerSystem.leftThumbCurl = l_curls[l_mirror ? 5 : 0]; + IKSystem.Instance.FingerSystem.leftIndexCurl = l_curls[l_mirror ? 6 : 1]; + IKSystem.Instance.FingerSystem.leftMiddleCurl = l_curls[l_mirror ? 7 : 2]; + IKSystem.Instance.FingerSystem.leftRingCurl = l_curls[l_mirror ? 8 : 3]; + IKSystem.Instance.FingerSystem.leftPinkyCurl = l_curls[l_mirror ? 9 : 4]; + IKSystem.Instance.FingerSystem.rightThumbCurl = l_curls[l_mirror ? 0 : 5]; + IKSystem.Instance.FingerSystem.rightIndexCurl = l_curls[l_mirror ? 1 : 6]; + IKSystem.Instance.FingerSystem.rightMiddleCurl = l_curls[l_mirror ? 2 : 7]; + IKSystem.Instance.FingerSystem.rightRingCurl = l_curls[l_mirror ? 3 : 8]; + IKSystem.Instance.FingerSystem.rightPinkyCurl = l_curls[l_mirror ? 4 : 9]; + } + else + { + if(m_fingerTracking) + { + RestoreFingerTracking(); + m_fingerTracking = false; + } + } + + Matrix4x4 l_offset = m_puppetParser.GetOffset(); + Vector3 l_pos = l_offset * ms_pointVector; + Quaternion l_rot = l_offset.rotation; + + l_pos.y = 0f; + if(Settings.MirrorPosition) + l_pos.x *= -1f; + l_pos = Vector3.ClampMagnitude(l_pos, m_distanceLimit); + + l_rot = Quaternion.Euler(0f, l_rot.eulerAngles.y * (Settings.MirrorRotation ? -1f : 1f), 0f); + + Matrix4x4 l_result = PlayerSetup.Instance.transform.GetMatrix() * Matrix4x4.TRS(l_pos, l_rot, Vector3.one); + + if(Settings.Position && !m_sitting && !m_puppetParser.IsSitting() && Utils.IsWorldSafe() && Utils.IsCombatSafe()) + PlayerSetup.Instance.transform.position = l_result * ms_pointVector; + + if(Settings.Rotation && !m_sitting && !m_puppetParser.IsSitting() && Utils.IsCombatSafe()) + { + if(m_inVr) + { + Vector3 l_avatarPos = PlayerSetup.Instance._avatar.transform.position; + PlayerSetup.Instance.transform.rotation = l_result.rotation; + Vector3 l_dif = l_avatarPos - PlayerSetup.Instance._avatar.transform.position; + PlayerSetup.Instance.transform.position += l_dif; + } + else + PlayerSetup.Instance.transform.rotation = l_result.rotation; + } + } + else + { + if(!m_puppetParser.IsWaitingAnimator()) + SetTarget(null); + } + + if(Vector3.Distance(this.transform.position, m_puppetParser.transform.position) > m_distanceLimit) + SetTarget(null); + } + } + + void LateUpdate() + { + if(m_active && (m_animator != null) && (m_puppetParser != null) && m_puppetParser.IsPoseParsed()) + { + OverrideIK(); + + m_puppetParser.GetPose().CopyTo(ref m_pose); + if(Settings.MirrorPose) + Utils.MirrorPose(ref m_pose); + m_poseHandler.SetHumanPose(ref m_pose); + } + } + + // Patches + internal void OnAvatarClear() + { + m_inVr = Utils.IsInVR(); + + if(m_puppetParser != null) + Object.Destroy(m_puppetParser); + m_puppetParser = null; + + m_animator = null; + m_vrIk = null; + m_lookAtIk = null; + + m_poseHandler?.Dispose(); + m_poseHandler = null; + m_active = false; + m_distanceLimit = float.MaxValue; + m_fingerTracking = false; + m_pose = new HumanPose(); + } + internal void OnAvatarSetup() + { + m_animator = PlayerSetup.Instance._animator; + m_vrIk = PlayerSetup.Instance._avatar.GetComponent(); + m_lookAtIk = PlayerSetup.Instance._avatar.GetComponent(); + + if((m_animator != null) && m_animator.isHuman) + { + m_poseHandler = new HumanPoseHandler(m_animator.avatar, m_animator.transform); + m_poseHandler.GetHumanPose(ref m_pose); + + if(m_vrIk != null) + { + m_vrIk.onPreSolverUpdate.AddListener(this.OnVRIKPreUpdate); + m_vrIk.onPostSolverUpdate.AddListener(this.OnVRIKPostUpdate); + } + + if(m_lookAtIk != null) + { + m_lookAtIk.onPreSolverUpdate.AddListener(this.OnLookAtIKPreUpdate); + m_lookAtIk.onPostSolverUpdate.AddListener(this.OnLookAtIKPostUpdate); + } + } + else + m_animator = null; + } + + // IK updates + void OnVRIKPreUpdate() + { + if(m_active) + { + m_ikWeight = m_vrIk.solver.IKPositionWeight; + m_vrIk.solver.IKPositionWeight = 0f; + } + } + void OnVRIKPostUpdate() + { + if(m_active) + m_vrIk.solver.IKPositionWeight = m_ikWeight; + } + + void OnLookAtIKPreUpdate() + { + if(m_active && !Settings.LookAtMix) + { + m_lookIkWeight = m_lookAtIk.solver.IKPositionWeight; + m_lookAtIk.solver.IKPositionWeight = 0f; + } + } + void OnLookAtIKPostUpdate() + { + if(m_active && !Settings.LookAtMix) + m_lookAtIk.solver.IKPositionWeight = m_lookIkWeight; + } + + // Arbitrary + public void SetTarget(GameObject p_target) + { + if(m_animator != null) + { + if(!m_active) + { + if(p_target != null) + { + m_puppetParser = p_target.AddComponent(); + m_distanceLimit = Utils.GetWorldMovementLimit(); + + m_active = true; + OnActivityChange?.Invoke(m_active); + } + } + else + { + if(p_target == null) + { + if(m_puppetParser != null) + Object.Destroy(m_puppetParser); + m_puppetParser = null; + + if(!m_sitting) + { + Quaternion l_rot = PlayerSetup.Instance.transform.rotation; + PlayerSetup.Instance.transform.rotation = Quaternion.Euler(0f, l_rot.eulerAngles.y, 0f); + } + + RestoreIK(); + RestoreFingerTracking(); + m_fingerTracking = false; + + m_active = false; + OnActivityChange?.Invoke(m_active); + } + } + } + } + + public bool IsActive() => m_active; + public bool IsFingerTrackingActive() => m_fingerTracking; + + void OverrideIK() + { + if((m_vrIk != null) && !BodySystem.isCalibrating) + BodySystem.TrackingPositionWeight = 0f; + } + void RestoreIK() + { + if((m_vrIk != null) && !BodySystem.isCalibrating) + { + BodySystem.TrackingPositionWeight = 1f; + m_vrIk.solver.Reset(); + } + } + void RestoreFingerTracking() + { + CVRInputManager.Instance.individualFingerTracking = (m_inVr && Utils.AreKnucklesInUse() && !Utils.GetIndexGestureToggle()); + IKSystem.Instance.FingerSystem.controlActive = CVRInputManager.Instance.individualFingerTracking; + + if(!CVRInputManager.Instance.individualFingerTracking) + { + CVRInputManager.Instance.fingerCurlLeftThumb = 0f; + CVRInputManager.Instance.fingerCurlLeftIndex = 0f; + CVRInputManager.Instance.fingerCurlLeftMiddle = 0f; + CVRInputManager.Instance.fingerCurlLeftRing = 0f; + CVRInputManager.Instance.fingerCurlLeftPinky = 0f; + CVRInputManager.Instance.fingerCurlRightThumb = 0f; + CVRInputManager.Instance.fingerCurlRightIndex = 0f; + CVRInputManager.Instance.fingerCurlRightMiddle = 0f; + CVRInputManager.Instance.fingerCurlRightRing = 0f; + CVRInputManager.Instance.fingerCurlRightPinky = 0f; + + IKSystem.Instance.FingerSystem.leftThumbCurl = 0f; + IKSystem.Instance.FingerSystem.leftIndexCurl = 0f; + IKSystem.Instance.FingerSystem.leftMiddleCurl = 0f; + IKSystem.Instance.FingerSystem.leftRingCurl = 0f; + IKSystem.Instance.FingerSystem.leftPinkyCurl = 0f; + IKSystem.Instance.FingerSystem.rightThumbCurl = 0f; + IKSystem.Instance.FingerSystem.rightIndexCurl = 0f; + IKSystem.Instance.FingerSystem.rightMiddleCurl = 0f; + IKSystem.Instance.FingerSystem.rightRingCurl = 0f; + IKSystem.Instance.FingerSystem.rightPinkyCurl = 0f; + } + } + } +} diff --git a/ml_pmc/Properties/AssemblyInfo.cs b/ml_pmc/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..84f0e07 --- /dev/null +++ b/ml_pmc/Properties/AssemblyInfo.cs @@ -0,0 +1,12 @@ +using System.Reflection; + +[assembly: AssemblyTitle("PlayerMovementCopycat")] +[assembly: AssemblyVersion("1.0.0")] +[assembly: AssemblyFileVersion("1.0.0")] + +[assembly: MelonLoader.MelonInfo(typeof(ml_pmc.PlayerMovementCopycat), "PlayerMovementCopycat", "1.0.0", "SDraw", "https://github.com/SDraw/ml_mods_cvr")] +[assembly: MelonLoader.MelonGame(null, "ChilloutVR")] +[assembly: MelonLoader.MelonPriority(3)] +[assembly: MelonLoader.MelonAdditionalDependencies("BTKUILib")] +[assembly: MelonLoader.MelonPlatform(MelonLoader.MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonLoader.MelonPlatformDomain(MelonLoader.MelonPlatformDomainAttribute.CompatibleDomains.MONO)] \ No newline at end of file diff --git a/ml_pmc/PuppetParser.cs b/ml_pmc/PuppetParser.cs new file mode 100644 index 0000000..531795a --- /dev/null +++ b/ml_pmc/PuppetParser.cs @@ -0,0 +1,156 @@ +using ABI_RC.Core.Player; +using UnityEngine; + +namespace ml_pmc +{ + [DisallowMultipleComponent] + class PuppetParser : MonoBehaviour + { + static readonly Vector4 ms_pointVector = new Vector4(0f, 0f, 0f, 1f); + + PuppetMaster m_puppetMaster = null; + Animator m_animator = null; + AnimatorCullingMode m_cullMode; + float m_armatureScale = 1f; + float m_armatureHeight = 0f; + + bool m_waitAnimator = true; + HumanPoseHandler m_poseHandler = null; + HumanPose m_pose; + bool m_poseParsed = false; + + Matrix4x4 m_matrix = Matrix4x4.identity; + Matrix4x4 m_offset = Matrix4x4.identity; + + bool m_sitting = false; + float m_leftGesture = 0f; + float m_rightGesture = 0f; + bool m_fingerTracking = false; + float[] m_fingerCurls = null; + + internal PuppetParser() + { + m_fingerCurls = new float[10]; + } + + // Unity events + void Start() + { + m_puppetMaster = this.GetComponent(); + m_matrix = this.transform.GetMatrix(); + StartCoroutine(WaitForAnimator()); + } + + void OnDestroy() + { + if(m_animator != null) + m_animator.cullingMode = m_cullMode; + + m_poseHandler?.Dispose(); + } + + void Update() + { + if(m_puppetMaster != null) + { + m_sitting = m_puppetMaster.PlayerAvatarMovementDataInput.AnimatorSitting; + m_leftGesture = m_puppetMaster.PlayerAvatarMovementDataInput.AnimatorGestureLeft; + m_rightGesture = m_puppetMaster.PlayerAvatarMovementDataInput.AnimatorGestureRight; + m_fingerTracking = m_puppetMaster.PlayerAvatarMovementDataInput.IndexUseIndividualFingers; + if(m_fingerTracking) + { + m_fingerCurls[0] = m_puppetMaster.PlayerAvatarMovementDataInput.LeftThumbCurl; + m_fingerCurls[1] = m_puppetMaster.PlayerAvatarMovementDataInput.LeftIndexCurl; + m_fingerCurls[2] = m_puppetMaster.PlayerAvatarMovementDataInput.LeftMiddleCurl; + m_fingerCurls[3] = m_puppetMaster.PlayerAvatarMovementDataInput.LeftRingCurl; + m_fingerCurls[4] = m_puppetMaster.PlayerAvatarMovementDataInput.LeftPinkyCurl; + m_fingerCurls[5] = m_puppetMaster.PlayerAvatarMovementDataInput.RightThumbCurl; + m_fingerCurls[6] = m_puppetMaster.PlayerAvatarMovementDataInput.RightIndexCurl; + m_fingerCurls[7] = m_puppetMaster.PlayerAvatarMovementDataInput.RightMiddleCurl; + m_fingerCurls[8] = m_puppetMaster.PlayerAvatarMovementDataInput.RightRingCurl; + m_fingerCurls[9] = m_puppetMaster.PlayerAvatarMovementDataInput.RightPinkyCurl; + } + } + + if(!ReferenceEquals(m_animator, null)) + { + if(m_animator != null) + { + Matrix4x4 l_current = this.transform.GetMatrix(); + m_offset = m_matrix.inverse * l_current; + m_matrix = l_current; + } + else + Reset(); + } + } + + void LateUpdate() + { + if(m_animator != null) + { + m_poseHandler.GetHumanPose(ref m_pose); + m_pose.bodyPosition *= m_armatureScale; + m_pose.bodyPosition.y += m_armatureHeight; + m_poseParsed = true; + } + } + + // Arbitrary + System.Collections.IEnumerator WaitForAnimator() + { + while(m_puppetMaster.avatarObject == null) + yield return null; + + while(m_animator == null) + { + m_animator = m_puppetMaster.avatarObject.GetComponent(); + yield return null; + } + + if(m_animator.isHuman) + { + m_cullMode = m_animator.cullingMode; + m_animator.cullingMode = AnimatorCullingMode.AlwaysAnimate; + + Transform l_hips = m_animator.GetBoneTransform(HumanBodyBones.Hips); + if((l_hips != null) && (l_hips.parent != null)) + { + m_armatureScale = l_hips.parent.localScale.y; + m_armatureHeight = ((m_puppetMaster.transform.GetMatrix().inverse * l_hips.parent.GetMatrix()) * ms_pointVector).y; + } + + m_poseHandler = new HumanPoseHandler(m_animator.avatar, m_animator.transform); + m_matrix = this.transform.GetMatrix(); + } + else + Reset(); + + m_waitAnimator = false; + } + + void Reset() + { + m_animator = null; + m_poseHandler?.Dispose(); + m_poseHandler = null; + m_pose = new HumanPose(); + m_poseParsed = false; + m_offset = Matrix4x4.identity; + m_sitting = false; + m_leftGesture = 0f; + m_rightGesture = 0f; + } + + public bool IsWaitingAnimator() => m_waitAnimator; + public bool HasAnimator() => !ReferenceEquals(m_animator, null); + public ref HumanPose GetPose() => ref m_pose; + public bool IsPoseParsed() => m_poseParsed; + public ref Matrix4x4 GetOffset() => ref m_offset; + public bool IsSitting() => m_sitting; + public float GetLeftGesture() => m_leftGesture; + public float GetRightGesture() => m_rightGesture; + public bool HasFingerTracking() => m_fingerTracking; + public ref float[] GetFingerCurls() => ref m_fingerCurls; + } +} diff --git a/ml_pmc/README.md b/ml_pmc/README.md new file mode 100644 index 0000000..1f02e47 --- /dev/null +++ b/ml_pmc/README.md @@ -0,0 +1,21 @@ +# Player Movement Copycat +Allows to copy pose, gestures and movement of your friends. + +# Installation +* Install [BTKUILib](https://github.com/BTK-Development/BTKUILib) +* Install [latest MelonLoader](https://github.com/LavaGang/MelonLoader) +* Get [latest release DLL](../../../releases/latest): + * Put `ml_pmc.dll` in `Mods` folder of game + +# Usage +Available options in BTKUILib players list upon player selection: +* **Copy movement:** starts/stops copycating of selected player. + * Note: Selected player should be your friend, be in your view range and not obstructed by other players/world objects/props. +* **Apply position:** enables/disables position changes of selected player; `true` by default. + * Note: Forcibly disabled in worlds that don't allow flight. +* **Apply rotation:** enables/disables rotation changes of selected player; `true` by default. +* **Copy gestures:** enables/disables gestures copy of selected player; `true` by default. +* **Apply LookAtIK:** enables/disables additional head rotation based on camera view in desktop mode; `true` by default. +* **Mirror pose:** enables/disables pose and gestures mirroring; `false` by default. +* **Mirror position:** enables/disables mirroring of position changes of selected player along 0XZ plane; `false` by default. +* **Mirror rotation:** enables/disables mirroring of rotation changes of selected player along 0XZ plane; `false` by default. diff --git a/ml_pmc/Settings.cs b/ml_pmc/Settings.cs new file mode 100644 index 0000000..d412dc2 --- /dev/null +++ b/ml_pmc/Settings.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; + +namespace ml_pmc +{ + static class Settings + { + public enum ModSetting + { + Position, + Rotation, + Gestures, + LookAtMix, + MirrorPose, + MirrorPosition, + MirrorRotation + } + + public static bool Position { get; private set; } = true; + public static bool Rotation { get; private set; } = true; + public static bool Gestures { get; private set; } = true; + public static bool LookAtMix { get; private set; } = true; + public static bool MirrorPose { get; private set; } = false; + public static bool MirrorPosition { get; private set; } = false; + public static bool MirrorRotation { get; private set; } = false; + + public static Action PositionChange; + public static Action RotationChange; + public static Action GesturesChange; + public static Action LookAtMixChange; + public static Action MirrorPoseChange; + public static Action MirrorPositionChange; + public static Action MirrorRotationChange; + + static MelonLoader.MelonPreferences_Category ms_category = null; + static List ms_entries = null; + + internal static void Init() + { + ms_category = MelonLoader.MelonPreferences.CreateCategory("PMC", null, true); + ms_entries = new List() + { + ms_category.CreateEntry(ModSetting.Position.ToString(), Position), + ms_category.CreateEntry(ModSetting.Rotation.ToString(), Rotation), + ms_category.CreateEntry(ModSetting.Gestures.ToString(), Gestures), + ms_category.CreateEntry(ModSetting.LookAtMix.ToString(), LookAtMix), + ms_category.CreateEntry(ModSetting.MirrorPose.ToString(), MirrorPose), + ms_category.CreateEntry(ModSetting.MirrorPosition.ToString(), MirrorPosition), + ms_category.CreateEntry(ModSetting.MirrorRotation.ToString(), MirrorRotation), + }; + + Position = (bool)ms_entries[(int)ModSetting.Position].BoxedValue; + Rotation = (bool)ms_entries[(int)ModSetting.Rotation].BoxedValue; + Gestures = (bool)ms_entries[(int)ModSetting.Gestures].BoxedValue; + LookAtMix = (bool)ms_entries[(int)ModSetting.LookAtMix].BoxedValue; + MirrorPose = (bool)ms_entries[(int)ModSetting.MirrorPose].BoxedValue; + MirrorPosition = (bool)ms_entries[(int)ModSetting.MirrorPosition].BoxedValue; + MirrorRotation = (bool)ms_entries[(int)ModSetting.MirrorRotation].BoxedValue; + } + + public static void SetSetting(ModSetting p_setting, object p_value) + { + switch(p_setting) + { + case ModSetting.Position: + { + Position = (bool)p_value; + PositionChange?.Invoke((bool)p_value); + } + break; + + case ModSetting.Rotation: + { + Rotation = (bool)p_value; + RotationChange?.Invoke((bool)p_value); + break; + } + + case ModSetting.Gestures: + { + Gestures = (bool)p_value; + GesturesChange?.Invoke((bool)p_value); + } + break; + + case ModSetting.LookAtMix: + { + LookAtMix = (bool)p_value; + LookAtMixChange?.Invoke((bool)p_value); + } + break; + + // + case ModSetting.MirrorPose: + { + MirrorPose = (bool)p_value; + MirrorPoseChange?.Invoke((bool)p_value); + } + break; + + case ModSetting.MirrorPosition: + { + MirrorPosition = (bool)p_value; + MirrorPositionChange?.Invoke((bool)p_value); + } + break; + + case ModSetting.MirrorRotation: + { + MirrorRotation = (bool)p_value; + MirrorRotationChange?.Invoke((bool)p_value); + } + break; + } + + if(ms_entries != null) + ms_entries[(int)p_setting].BoxedValue = p_value; + } + } +} diff --git a/ml_pmc/Utils.cs b/ml_pmc/Utils.cs new file mode 100644 index 0000000..fbc462a --- /dev/null +++ b/ml_pmc/Utils.cs @@ -0,0 +1,81 @@ +using ABI.CCK.Components; +using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace ml_pmc +{ + static class Utils + { + static readonly FieldInfo ms_indexGestureToggle = typeof(InputModuleSteamVR).GetField("_steamVrIndexGestureToggleValue", BindingFlags.Instance | BindingFlags.NonPublic); + + static readonly (int, int)[] ms_sideMuscles = new (int, int)[] + { + (29,21), (30,22), (31,23), (32,24), (33,25), (34,26), (35,27), (36,28), + (46,37), (47,38), (48,39), (49,40), (50,41), (51,42), (52,43), (53,44), (54,45), + (75,55), (76,56), (77,57), (78,58), (79,59), (80,60), (81,61), (82,62), (83,63), (84,64), + (85,65), (86,66), (87,67), (88,68), (89, 69), (90,70), (91,71), (92,72), (93,73), (94,74) + }; + static readonly int[] ms_centralMuscles = new int[] { 1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 18, 20 }; + + public static bool IsInVR() => ((CheckVR.Instance != null) && CheckVR.Instance.hasVrDeviceLoaded); + public static bool AreKnucklesInUse() => PlayerSetup.Instance._trackerManager.trackerNames.Contains("knuckles"); + public static bool GetIndexGestureToggle() => (bool)ms_indexGestureToggle.GetValue(CVRInputManager.Instance.GetComponent()); + + public static bool IsWorldSafe() => ((CVRWorld.Instance != null) && CVRWorld.Instance.allowFlying); + public static bool IsCombatSafe() => ((CombatSystem.Instance == null) || !CombatSystem.Instance.isDown); + + public static float GetWorldMovementLimit() + { + float l_result = 1f; + if(CVRWorld.Instance != null) + { + l_result = CVRWorld.Instance.baseMovementSpeed; + l_result *= CVRWorld.Instance.sprintMultiplier; + l_result *= CVRWorld.Instance.inAirMovementMultiplier; + l_result *= CVRWorld.Instance.flyMultiplier; + } + return l_result; + } + + public static Matrix4x4 GetMatrix(this Transform p_transform, bool p_pos = true, bool p_rot = true, bool p_scl = false) + { + return Matrix4x4.TRS(p_pos ? p_transform.position : Vector3.zero, p_rot ? p_transform.rotation : Quaternion.identity, p_scl ? p_transform.lossyScale : Vector3.one); + } + + public static void CopyTo(this HumanPose p_source, ref HumanPose p_target) + { + p_target.bodyPosition = p_source.bodyPosition; + p_target.bodyRotation = p_source.bodyRotation; + + int l_count = Mathf.Min(p_source.muscles.Length, p_target.muscles.Length); + for(int i = 0; i < l_count; i++) + p_target.muscles[i] = p_source.muscles[i]; + } + + public static void MirrorPose(ref HumanPose p_pose) + { + int l_count = p_pose.muscles.Length; + foreach(var l_pair in ms_sideMuscles) + { + if((l_count > l_pair.Item1) && (l_count > l_pair.Item2)) + { + float l_temp = p_pose.muscles[l_pair.Item1]; + p_pose.muscles[l_pair.Item1] = p_pose.muscles[l_pair.Item2]; + p_pose.muscles[l_pair.Item2] = l_temp; + } + } + foreach(int l_index in ms_centralMuscles) + { + if(l_count > l_index) + p_pose.muscles[l_index] *= -1f; + } + + p_pose.bodyRotation.x *= -1f; + p_pose.bodyRotation.w *= -1f; + p_pose.bodyPosition.x *= -1f; + } + } +} diff --git a/ml_pmc/ml_pmc.csproj b/ml_pmc/ml_pmc.csproj new file mode 100644 index 0000000..4679faa --- /dev/null +++ b/ml_pmc/ml_pmc.csproj @@ -0,0 +1,101 @@ + + + + + Debug + AnyCPU + {758514D3-6E1A-4E05-A156-B4E5C74AB5C4} + Library + Properties + ml_pmc + ml_pmc + v4.7.2 + 512 + true + + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + + + + D:\Games\Steam\steamapps\common\ChilloutVR\MelonLoader\net35\0Harmony.dll + False + False + + + False + D:\Games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp.dll + False + + + False + D:\Games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp-firstpass.dll + False + + + D:\Games\Steam\steamapps\common\ChilloutVR\Mods\BTKUILib.dll + False + False + + + D:\Games\Steam\steamapps\common\ChilloutVR\MelonLoader\net35\MelonLoader.dll + False + False + + + + + + + + + + D:\Games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.AnimationModule.dll + False + False + + + D:\Games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.CoreModule.dll + False + False + + + False + D:\Games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.PhysicsModule.dll + + + + + + + + + + + + + + + + + + + + copy /y "$(TargetPath)" "D:\Games\Steam\steamapps\common\ChilloutVR\Mods\" + + \ No newline at end of file diff --git a/ml_pmc/ml_pmc.csproj.user b/ml_pmc/ml_pmc.csproj.user new file mode 100644 index 0000000..4d08fe9 --- /dev/null +++ b/ml_pmc/ml_pmc.csproj.user @@ -0,0 +1,6 @@ + + + + D:\Games\Steam\steamapps\common\ChilloutVR\MelonLoader\net35\;D:\Games\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\;D:\Games\Steam\steamapps\common\ChilloutVR\Mods\ + + \ No newline at end of file diff --git a/ml_pmc/resources/dancing.png b/ml_pmc/resources/dancing.png new file mode 100644 index 0000000000000000000000000000000000000000..d54c17c730829147bd61f5da4199790614913204 GIT binary patch literal 4168 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