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 0000000..d54c17c Binary files /dev/null and b/ml_pmc/resources/dancing.png differ diff --git a/ml_pmc/resources/dancing_on.png b/ml_pmc/resources/dancing_on.png new file mode 100644 index 0000000..ff60d8b Binary files /dev/null and b/ml_pmc/resources/dancing_on.png differ