diff --git a/BetterVR/BetterVR.csproj b/BetterVR/BetterVR.csproj index 9c02d14..c7b84ce 100644 --- a/BetterVR/BetterVR.csproj +++ b/BetterVR/BetterVR.csproj @@ -1,145 +1,172 @@ - + Debug AnyCPU - {FAE04EC0-301F-11D3-BF4B-00C04F79EFBF} - library - netcoreapp3.1 + {930A00A6-2935-4EA0-81F0-B16D3E71B74F} + Library + Properties + net46 BetterVR HS2_BetterVR - v4.6 512 true + false + true true - embedded + pdbonly false ..\bin\HS2_BetterVR\BepInEx\plugins DEBUG;TRACE;HS2 prompt - 4 - false + 3 + CppCoreCheckRules.ruleset + x64 - embedded + pdbonly true ..\bin\HS2_BetterVR\BepInEx\plugins TRACE;HS2 prompt - 4 - true - false + 3 + x64 - - - False - ..\packages\IllusionLibs.BepInEx.Harmony.2.2.0.1\lib\net35\0Harmony.dll - False - - - ..\packages\IllusionLibs.HoneySelect2.Assembly-CSharp.2020.5.29.2\lib\net46\Assembly-CSharp_VR.dll - False - - - ..\packages\IllusionLibs.HoneySelect2.Assembly-CSharp-firstpass.2020.5.29.2\lib\net46\Assembly-CSharp-firstpass.dll - False - - - ..\packages\IllusionLibs.BepInEx.5.1.0\lib\net35\BepInEx.dll - False - False - - - ..\packages\IllusionLibs.BepInEx.Harmony.2.0.3.1\lib\net35\BepInEx.Harmony.dll - False - - - ..\packages\ABMX.HS2ABMX.4.4.1\lib\net46\HS2ABMX.dll - False - False - - - ..\packages\IllusionModdingAPI.HS2API.1.15.0\lib\net46\HS2API.dll - False - False - - - ..\packages\ExtensibleSaveFormat.HoneySelect2.16.2.0.2\lib\net46\HS2_ExtensibleSaveFormat.dll - False - False - - - ..\packages\IllusionLibs.HoneySelect2.Sirenix.Serialization.2020.5.29.2\lib\net46\Sirenix.Serialization.dll + + + ..\packages\IllusionLibs.BepInEx.Harmony.2.9.0\lib\net35\0Harmony.dll + False + + + ..\packages\HS2VR.Assembly-CSharp\Assembly-CSharp.dll + False + + + ..\packages\IllusionLibs.HoneySelect2.Assembly-CSharp-firstpass.2020.5.29.4\lib\net46\Assembly-CSharp-firstpass.dll + True + True + + + ..\packages\IllusionLibs.BepInEx.5.4.22\lib\net35\BepInEx.dll + True + True + + + ..\packages\IllusionLibs.BepInEx.5.4.22\lib\net35\BepInEx.dll + False + + + ..\packages\ABMX.HS2ABMX.5.0.6\lib\net46\HS2ABMX.dll + True + True + + + ..\packages\IllusionModdingAPI.HS2API.1.36.0\lib\net46\HS2API.dll + True + True + + + ..\packages\IllusionModdingAPI.HS2API.1.36.0\lib\net46\HS2API.dll False False - - ..\packages\IllusionLibs.HoneySelect2.UnityEngine.CoreModule.2018.4.11.2\lib\net46\UnityEngine.dll - False - - - ..\packages\IllusionLibs.HoneySelect2.UniRx.2020.5.29.2\lib\net46\UniRx.dll - False - - - ..\packages\IllusionLibs.HoneySelect2.UnityEngine.CoreModule.2018.4.11.2\lib\net46\UnityEngine.CoreModule.dll - False - - - ..\packages\IllusionLibs.HoneySelect2.UnityEngine.PhysicsModule.2018.4.11.2\lib\net46\UnityEngine.PhysicsModule.dll - False - - - ..\packages\IllusionLibs.HoneySelect2.UnityEngine.UI.2018.4.11.2\lib\net46\UnityEngine.UI.dll - False - - - ..\packages\IllusionLibs.HoneySelect2.UnityEngine.IMGUIModule.2018.4.11.2\lib\net46\UnityEngine.IMGUIModule.dll - False - - - ..\packages\IllusionLibs.HoneySelect2.UnityEngine.TextRenderingModule.2018.4.11.2\lib\net46\UnityEngine.TextRenderingModule.dll - False - - - ..\packages\IllusionLibs.HoneySelect2.IL.2020.5.29.2\lib\net46\IL.dll - False + + ..\packages\ExtensibleSaveFormat.HoneySelect2.19.3.0\lib\net46\HS2_ExtensibleSaveFormat.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.IL.2020.5.29.4\lib\net46\IL.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.Sirenix.Serialization.2020.5.29.4\lib\net46\Sirenix.Serialization.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.UniRx.2020.5.29.4\lib\net46\UniRx.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.Unity.TextMeshPro.2018.4.11.4\lib\net46\Unity.TextMeshPro.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.UnityEngine.CoreModule.2018.4.11.4\lib\net46\UnityEngine.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.UnityEngine.AssetBundleModule.2018.4.11.4\lib\net46\UnityEngine.AssetBundleModule.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.UnityEngine.CoreModule.2018.4.11.4\lib\net46\UnityEngine.CoreModule.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.UnityEngine.ImageConversionModule.2018.4.11.4\lib\net46\UnityEngine.ImageConversionModule.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.UnityEngine.IMGUIModule.2018.4.11.4\lib\net46\UnityEngine.IMGUIModule.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.UnityEngine.PhysicsModule.2018.4.11.4\lib\net46\UnityEngine.PhysicsModule.dll + True + True + + + ..\packages\IllusionLibs.HoneySelect2.UnityEngine.TextRenderingModule.2018.4.11.4\lib\net46\UnityEngine.TextRenderingModule.dll + True + True ..\packages\SteamVR.dll False + + + ..\packages\IllusionLibs.HoneySelect2.UnityEngine.UI.2018.4.11.4\lib\net46\UnityEngine.UI.dll + True + True + + + + ..\packages\IllusionLibs.HoneySelect2.UnityEngine.UIModule.2018.4.11.4\lib\net46\UnityEngine.UIModule.dll + True + True + - - - - - - - - - - - + - - - - + + + + diff --git a/BetterVR/BetterVRPlugin.Helper.cs b/BetterVR/BetterVRPlugin.Helper.cs index 04f4087..e3b4449 100644 --- a/BetterVR/BetterVRPlugin.Helper.cs +++ b/BetterVR/BetterVRPlugin.Helper.cs @@ -1,15 +1,15 @@ +using HTC.UnityPlugin.Vive; +using HS2VR; +using IllusionUtility.GetUtility; +using System.Text.RegularExpressions; using UnityEngine; -using System.Collections; +using UnityEngine.UI; +using UnityEngine.Events; namespace BetterVR { public static class BetterVRPluginHelper - { - public static GameObject VROrigin; - public static bool init = true;//False once headset GameObject found - internal static bool isRunning = false;//True while actively searching for headset GameObject - - + { public enum VR_Hand { left, @@ -17,31 +17,144 @@ public enum VR_Hand none } + private static readonly Regex HAND_NAME_MATCHER = new Regex("Hand|hand"); + private static readonly Regex P_NAME_MATCHER = new Regex("Dan|dan"); - /// - /// Get the top level VR game object - /// - internal static GameObject GetVROrigin() + public static GameObject VROrigin; + public static UnityEvent recenterVR { set; private get; } + + private static GameObject simplePClone; + private static int pDisplayMode = 1; // 0: invisible, 1: full, 2: silhouette + + private static Camera _VRCamera; + public static Camera VRCamera { - if (VROrigin == null) + get { - VROrigin = GameObject.Find("VROrigin"); + if (_VRCamera == null) _VRCamera = GameObject.Find("Camera (eye)")?.GetComponent(); + if (_VRCamera == null) _VRCamera = GameObject.Find("rCamera (eye)")?.GetComponent(); + if (_VRCamera == null) _VRCamera = GameObject.Find("rCamera")?.GetComponent(); + if (_VRCamera == null) _VRCamera = GameObject.Find("Camera")?.GetComponent(); + if (_VRCamera == null) _VRCamera = Camera.main; + if (_VRCamera == null) + { + BetterVRPlugin.Logger.LogWarning("VR Camera not found, may try again later"); + var cameras = GameObject.FindObjectsOfType(); + string cameraNames = ""; + foreach (var camera in cameras) cameraNames += cameraNames + "; "; + BetterVRPlugin.Logger.LogDebug("Current cameras in scene: " + cameraNames); + } + return _VRCamera; + } + } - //Show vr controler GO tree - if (VROrigin != null && init) { - init = false; - if (BetterVRPlugin.debugLog) DebugTools.LogChildrenComponents(VROrigin, true); - } - } + private static Image privacyScreen; + private static VRGlove _leftGlove; + private static VRGlove _rightGlove; - // var origin = SteamVR_Render.Top()?.origin; //The headset rig render - // if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" SteamVR Origin {origin?.gameObject}"); + internal static VRGlove leftGlove { + get { + TryInitializeGloves(); + return _leftGlove; + } + } + internal static VRGlove rightGlove + { + get + { + TryInitializeGloves(); + return _rightGlove; + } + } - return VROrigin; + private static Transform _leftControllerCenter; + private static Transform _rightControllerCenter; + internal static Transform leftControllerCenter + { + get + { + if (CreateTransformIfNotPresent(ref _leftControllerCenter, parent: FindLeftControllerRenderModel(out var center)?.parent)) + { + _leftControllerCenter.name = "LeftControllerCenter"; + _leftControllerCenter.position = center; + } + return _leftControllerCenter; + } + } + internal static Transform rightControllerCenter + { + get + { + if (CreateTransformIfNotPresent(ref _rightControllerCenter, parent: FindRightControllerRenderModel(out var center)?.parent)) + { + _rightControllerCenter.name = "RightControllerCenter"; + _rightControllerCenter.position = center; + } + return _rightControllerCenter; + } } + private static Transform _leftCursorAttach; + private static Transform _rightCursorAttach; + internal static Transform leftCursorAttach + { + get + { + if (CreateTransformIfNotPresent(ref _leftCursorAttach, parent: leftControllerCenter)) + { + _leftCursorAttach.name = "LeftCursorAttach"; + _leftCursorAttach.localPosition = new Vector3(0, 0.0625f, 0.125f); + } + return _leftCursorAttach; + } + } + internal static Transform rightCursorAttach + { + get + { + if (CreateTransformIfNotPresent(ref _rightCursorAttach, parent: rightControllerCenter)) + { + _rightCursorAttach.name = "RightCursorAttach"; + _rightCursorAttach.localPosition = new Vector3(0, 0.0625f, 0.125f); + } + return _rightCursorAttach; + } + } + + private static RadialMenu _leftRadialMenu; + private static RadialMenu _rightRadialMenu; + internal static RadialMenu leftRadialMenu { + get + { + if (!_leftRadialMenu) + { + _leftRadialMenu = new GameObject("LeftRadialMenu").AddComponent(); + _leftRadialMenu.gameObject.SetActive(false); + } + _leftRadialMenu.hand = leftCursorAttach; + return _leftRadialMenu; + } + } + internal static RadialMenu rightRadialMenu + { + get + { + if (!_rightRadialMenu) + { + _rightRadialMenu = new GameObject("RightRadialMenu").AddComponent(); + _rightRadialMenu.gameObject.SetActive(false); + } + _rightRadialMenu.hand = rightCursorAttach; + return _rightRadialMenu; + } + } + + private static HandHeldToy _handHeldToy; + internal static HandHeldToy handHeldToy + { + get { return (_handHeldToy && _handHeldToy.gameObject) ? _handHeldToy : (_handHeldToy = new GameObject("BetterVRHandHeldToy").AddComponent()); } + } - /// /// Use an enum to get the correct hand /// internal static GameObject GetHand(VR_Hand hand) @@ -52,69 +165,312 @@ internal static GameObject GetHand(VR_Hand hand) return null; } - /// /// Get The left hand controller vr game object /// internal static GameObject GetLeftHand() { - var leftHand = GameObject.Find("ViveControllers/Left"); - if (leftHand == null) return null; + var leftHand = GameObject.Find("ViveControllers/Left") ?? GameObject.Find("Controller (left)"); + // if (leftHand && BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" GetLeftHand id {leftHand.GetInstanceID()}"); + return leftHand; + } - if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" GetLeftHand id {leftHand.GetInstanceID()}"); + internal static GameObject GetRightHand() + { + var rightHand = GameObject.Find("ViveControllers/Right") ?? GameObject.Find("Controller (right)"); + // if (rightHand && BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" GetRightHand id {rightHand.GetInstanceID()}"); + return rightHand; + } - return leftHand.gameObject; + /// + /// Lazy wait for VR headset origin to exists + /// + internal static void Init(GameObject VROrigin) + { + BetterVRPluginHelper.VROrigin = VROrigin; + FixWorldScale(); } + /// + /// Enlarge the VR camera, to make the world appear to shrink by xx% + /// + internal static void FixWorldScale(bool enable = true) + { + var viveRig = GameObject.Find("ViveRig"); + if (viveRig == null) return; + viveRig.transform.localScale = Vector3.one * (enable ? BetterVRPlugin.PlayerScale : 1); + } - internal static GameObject GetRightHand() + // Moves VR camera to the player's head. + internal static void ResetView() { - var rightHand = GameObject.Find("ViveControllers/Right"); - if (rightHand == null) return null; + if (!VROrigin) return; - if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" GetRightHand id {rightHand.GetInstanceID()}"); + var preventInitCamera = VROrigin.GetComponent(); + if (preventInitCamera) preventInitCamera.enabled = false; - return rightHand.gameObject; + // Remove any vertical rotation. + Quaternion rotation = VROrigin.transform.rotation; + VROrigin.transform.rotation = Quaternion.Euler(0, rotation.y, 0); + + recenterVR?.Invoke(); + VRSettingUI.CameraInitAction?.Invoke(); } + internal static void CyclePlayerPDisplayMode() + { + // Sync display mode before changing it. + if (!Manager.Config.HData.Son) pDisplayMode = 0; + // Cycle player part display mode. + pDisplayMode = (pDisplayMode + 1) % 3; + // Toggle player part visibility. + UpdatePDisplay(); + UpdatePlayerColliderActivity(); + } - internal static void CheckForVROrigin(BetterVRPlugin instance) + internal static void UpdatePlayerColliderActivity() { - if (BetterVRPluginHelper.isRunning) return; - instance.StartCoroutine(BetterVRPluginHelper.Init()); + var player = BetterVRPlugin.GetPlayer(); + if (player == null) return; + var colliders = player.objTop.GetComponentsInChildren(true); + foreach (var collider in colliders) + { + if (HAND_NAME_MATCHER.IsMatch(collider.name)) + { + collider.enabled = Manager.Config.HData.Visible; + } + else if (P_NAME_MATCHER.IsMatch(collider.name)) + { + collider.enabled = Manager.Config.HData.Son; + } + } } + private static void UpdatePDisplay() + { + Manager.Config.HData.Son = (pDisplayMode != 0); - /// - /// Lazy wait for VR headset origin to exists - /// - internal static IEnumerator Init() + var player = BetterVRPlugin.GetPlayer(); + if (!player || !player.loadEnd) return; + + bool shouldUseSimpleP = Manager.Config.HData.Son && pDisplayMode == 2; + bool shouldUseFullP = Manager.Config.HData.Son && pDisplayMode == 1; + + var simpleBodyEtc = player.cmpSimpleBody?.targetEtc; + GameObject simpleBody = simpleBodyEtc?.objBody; + if (simplePClone != null) + { + if (!shouldUseSimpleP || simpleBody == null || simplePClone.transform.parent != simpleBody.transform.parent) + { + GameObject.Destroy(simplePClone); + simplePClone = null; + } + } + + if (shouldUseSimpleP && simplePClone == null) + { + GameObject simpleP = simpleBodyEtc?.objDanTop; + if (simpleBody && simpleP) + { + simplePClone = GameObject.Instantiate(simpleP, simpleP.transform.parent); + simplePClone.transform.SetPositionAndRotation(simpleP.transform.position, simpleP.transform.rotation); + simplePClone.transform.localScale = simpleP.transform.localScale; + // Reparent so that it is a sibling instead of a child of simpleBody and + // can be displayed even if simpleBody is hidden. + simplePClone.transform.SetParent(simpleBody.transform.parent, worldPositionStays: true); + var renderers = simplePClone.GetComponentsInChildren(true); + foreach (var renderer in renderers) + { + renderer.enabled = true; + renderer.GetOrAddComponent(); + } + } + } + + simplePClone?.SetActive(shouldUseSimpleP); + + // Hide the original part now that there is a clone. + var tamaRenderer = simpleBodyEtc?.objDanTama?.GetComponentInChildren(true); + if (tamaRenderer) tamaRenderer.enabled = false; + + var saoRenderer = simpleBodyEtc?.objDanSao?.GetComponentInChildren(true); + if (saoRenderer) saoRenderer.enabled = false; + + var regularBodyEtc = player.cmpBody?.targetEtc; + if (regularBodyEtc != null) + { + regularBodyEtc.objMNPB?.SetActive(shouldUseFullP); + regularBodyEtc.objDanTop?.SetActive(shouldUseFullP); + regularBodyEtc.objDanSao?.SetActive(shouldUseFullP); + regularBodyEtc.objDanTama?.SetActive(shouldUseFullP); + } + } + + internal static void FinishH() + { + bool FinishedSameTime = TryFinishHSameTime(); + if (!FinishedSameTime) Singleton.Instance?.OnClickFinish(); + } + + internal static bool TryFinishHSameTime() + { + var hCtrl = Singleton.Instance; + var sprite = Singleton.Instance; + var anim = Singleton.Instance?.Hscene?.GetProcBase(); + + if (!hCtrl || !sprite || anim == null || hCtrl.loopType < 2) return false; + if (anim is Houshi || anim is Spnking || anim is Peeping) return false; + if (anim is Aibu || anim is Masturbation || anim is Les) + { + // No finish button available on the UI, simply set feel to gauge to full + hCtrl.feel_f = 1; + return true; + } + + if (hCtrl.feel_m < 0.75f) return false; + + sprite.OnClickFinishSame(); + return true; + } + + internal static void TryInitializeGloves() { - isRunning = true; + if (!_leftGlove) _leftGlove = VRGlove.CreateLeftGlove(); + if (!_rightGlove) _rightGlove = VRGlove.CreateRightGlove(); + } - while (BetterVRPluginHelper.VROrigin == null) - { - BetterVRPluginHelper.GetVROrigin(); - yield return new WaitForSeconds(1); - } + internal static void UpdatePrivacyScreen(Color? color = null) + { + EnsurePrivacyScreen().gameObject.SetActive(BetterVRPlugin.UsePrivacyScreen.Value); + if (color != null) privacyScreen.color = (Color) color; + } - BetterVRPluginHelper.FixWorldScale(); + internal static Vector2 GetLeftHandPadStickCombinedOutput() + { + Vector2 output = ViveInput.GetPadAxisEx(HandRole.LeftHand); + output.x += ViveInput.GetAxisEx(HandRole.LeftHand, ControllerAxis.JoystickX); + output.y += ViveInput.GetAxisEx(HandRole.LeftHand, ControllerAxis.JoystickY); + return output; + } - isRunning = false; + internal static Vector2 GetRightHandPadStickCombinedOutput() + { + Vector2 output = ViveInput.GetPadAxisEx(HandRole.RightHand); + output.x += ViveInput.GetAxisEx(HandRole.RightHand, ControllerAxis.JoystickX); + output.y += ViveInput.GetAxisEx(HandRole.RightHand, ControllerAxis.JoystickY); + return output; } + internal static void UpdateControllersVisibilty() + { + UpdateControllerVisibilty(FindControllerRenderModel(GetLeftHand(), out var lCenter)); + UpdateControllerVisibilty(FindControllerRenderModel(GetRightHand(), out var rCenter)); + } - /// - /// Enlarge the VR camera, to make the world appear to shrink by xx% - /// - public static void FixWorldScale(bool enable = true) + internal static string GetNameAndComponents(GameObject go) { - var viveRig = GameObject.Find("ViveRig"); - if (viveRig != null) + var result = go.name + ":"; + var components = go.GetComponents(); + foreach (var c in components) result += c.GetType() + ";"; + return result; + } + + private static bool CreateTransformIfNotPresent(ref Transform transform, Transform parent) + { + if (parent == null) { - viveRig.transform.localScale = Vector3.one * (enable ? 1.10f : 1); + if (transform != null) GameObject.Destroy(transform.gameObject); + transform = null; + return false; } + + if (transform != null && transform.parent == parent) return false; + + if (transform != null) GameObject.Destroy(transform.gameObject); + + transform = new GameObject().transform; + transform.parent = parent; + transform.localPosition = Vector3.zero; + transform.localRotation = Quaternion.identity; + transform.localScale = Vector3.one; + + return true; + } + + private static Transform FindLeftControllerRenderModel(out Vector3 center) + { + return FindControllerRenderModel(GetLeftHand(), out center); + } + + private static Transform FindRightControllerRenderModel(out Vector3 center) + { + return FindControllerRenderModel(GetRightHand(), out center); + } + + private static Transform FindControllerRenderModel(GameObject controller, out Vector3 center) + { + center = Vector3.zero; + + if (controller == null) return null; + + Transform renderModel = controller.transform.FindLoop("Model") ?? controller.transform.FindLoop("OpenVRRenderModel"); + if (!renderModel) return null; + + var meshFilter = renderModel.GetComponentInChildren(true); + center = + meshFilter ? meshFilter.transform.TransformPoint(meshFilter.mesh.bounds.center) : controller.transform.position; + + return renderModel; } + private static void UpdateControllerVisibilty(Transform renderModel) + { + if (!renderModel) return; + bool shouldShowController = !VRGlove.isShowingGloves || !BetterVRPlugin.IsHidingControllersEnabled(); + var renderers = renderModel.GetComponentsInChildren(true); + foreach (var renderer in renderers) renderer.enabled = shouldShowController; + } + + private static Image EnsurePrivacyScreen() { + if (privacyScreen && privacyScreen.gameObject && privacyScreen.transform.parent) + { + return privacyScreen; + } + + Canvas privacyCanvas = new GameObject("PrivacyCanvas").AddComponent(); + privacyCanvas.renderMode = RenderMode.ScreenSpaceOverlay; + privacyCanvas.sortingOrder = 30000; + GameObject privacyOverlay = new GameObject("PrivacySreen"); + privacyOverlay.transform.SetParent(privacyCanvas.transform); + privacyScreen = privacyOverlay.AddComponent(); + privacyScreen.transform.SetParent(privacyCanvas.transform); + privacyScreen.rectTransform.sizeDelta = new Vector2((float)(Screen.width * 4), (float)(Screen.height * 4)); + privacyScreen.color = Color.black; + Object.DontDestroyOnLoad(privacyCanvas.gameObject); + + return privacyScreen; + } + + internal class SilhouetteMaterialSetter : MonoBehaviour + { + void Update() + { + var player = BetterVRPlugin.GetPlayer(); + if (!player || !player.loadEnd) return; + + var source = player.cmpSimpleBody?.targetEtc?.objBody?.GetComponentInChildren(true); + if (!source) return; + + var meshRenderer = GetComponent(); + if (meshRenderer) meshRenderer.sharedMaterials = source.sharedMaterials; + + var skinnedMeshRenderer = GetComponent(); + if (skinnedMeshRenderer) skinnedMeshRenderer.sharedMaterials = source.sharedMaterials; + + UpdateControllersVisibilty(); + + Destroy(this); + } + } } -} \ No newline at end of file +} diff --git a/BetterVR/BetterVRPlugin.cs b/BetterVR/BetterVRPlugin.cs index e75cf5d..d9813de 100644 --- a/BetterVR/BetterVRPlugin.cs +++ b/BetterVR/BetterVRPlugin.cs @@ -1,6 +1,11 @@ using BepInEx; using BepInEx.Logging; +using Manager; +using HTC.UnityPlugin.Vive; using HarmonyLib; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; namespace BetterVR { @@ -9,9 +14,7 @@ namespace BetterVR public partial class BetterVRPlugin : BaseUnityPlugin { public const string GUID = "BetterVR"; - public const string Version = "0.2"; - - + public const string Version = "0.63"; internal static new ManualLogSource Logger { get; private set; } #if DEBUG @@ -20,50 +23,170 @@ public partial class BetterVRPlugin : BaseUnityPlugin internal static bool debugLog = false; #endif + private static StripUpdater leftHandStripUpdater; + private static StripUpdater rightsHandStripUpdater; + + internal static float ILUTimer { get; private set; } = 0; + internal static bool ILUCooldown { get; private set; } = false; + internal void Start() { Logger = base.Logger; - DebugTools.logger = Logger; - VRControllerColliderHelper.pluginInstance = this; + // DebugTools.logger = Logger; PluginConfigInit(); - //Set up game mode detectors to start certain logic when loading into main game - VRControllerColliderHelper.TriggerHelperCoroutine(); - //Watch for headset initialized - BetterVRPluginHelper.CheckForVROrigin(this); - //Harmony init. It's magic! - // Harmony harmony_controller = new Harmony(GUID + "_controller"); - // VRControllerHooks.InitHooks(harmony_controller, this); + Harmony harmony_controller = new Harmony(GUID + "_controller"); + VRControllerHooks.InitHooks(harmony_controller, this); - Harmony harmony_menu = new Harmony(GUID + "_menu"); + Harmony harmony_menu = new Harmony(GUID + "_menu"); VRMenuHooks.InitHooks(harmony_menu, this); - - //Potentially important Hs2 classes - //ControllerManager has button input triggers, and the laser pointer - //ControllerManagerSample same thing? - //ShowMenuOnClick shows controller GUI - //vrTest - // internal static bool isOculus = XRDevice.model.Contains("Oculus"); + //ControllerManager has button input triggers, and the laser pointer + //ControllerManagerSample same thing? + //ShowMenuOnClick shows controller GUI + //vrTest + // internal static bool isOculus = XRDevice.model.Contains("Oculus"); - } + BetterVRPluginHelper.UpdatePrivacyScreen(Color.white); + } + public static float touchInteractionCooldown; - //Check for controller input changes + // Check for controller input changes internal void Update() { + if (touchInteractionCooldown > 0) touchInteractionCooldown -= Time.deltaTime; + + if (leftHandStripUpdater == null) leftHandStripUpdater = new StripUpdater(VRControllerInput.roleL); + leftHandStripUpdater?.CheckStrip(BetterVRPlugin.GestureStrip.Value == "Left hand"); + + if (rightsHandStripUpdater == null) rightsHandStripUpdater = new StripUpdater(VRControllerInput.roleR); + rightsHandStripUpdater?.CheckStrip(BetterVRPlugin.GestureStrip.Value == "Right hand"); + + BetterVRPluginHelper.TryInitializeGloves(); + + if (ViveInput.GetPressDownEx(HandRole.LeftHand, ControllerButton.Trigger) || + ViveInput.GetPressDownEx(HandRole.RightHand, ControllerButton.Trigger) && + Time.timeScale == 0) + { + // Fix the bug that time scale becomes zero after opening BepInex config and closing game settings + Time.timeScale = 1; + } + + CheckRadialMenu(BetterVRPluginHelper.leftRadialMenu, HandRole.LeftHand); + CheckRadialMenu(BetterVRPluginHelper.rightRadialMenu, HandRole.RightHand); + // if (BetterVRPlugin.debugLog && Time.frameCount % 10 == 0) BetterVRPlugin.Logger.LogInfo($" SqueezeToTurn {SqueezeToTurn.Value} VRControllerInput.VROrigin {VRControllerInput.VROrigin}"); - if (BetterVRPluginHelper.VROrigin == null) BetterVRPluginHelper.CheckForVROrigin(this); - //When the user squeezes the controller, apply hand rotation to headset - if (SqueezeToTurn.Value && BetterVRPluginHelper.VROrigin != null) + VRControllerInput.UpdateSqueezeMovement(); + + if (VRControllerInput.IsILUGesture(HandRole.LeftHand) || VRControllerInput.IsILUGesture(HandRole.RightHand)) { - VRControllerInput.CheckInputForSqueezeTurn(); + ILUTimer += Time.deltaTime; + if (!ILUCooldown && ILUTimer > 3f) + { + BetterVRPluginHelper.FinishH(); + ILUCooldown = true; + } } + else + { + ILUTimer = 0; + ILUCooldown = false; + } + } + + internal static AIChara.ChaControl GetPlayer() + { + return Singleton.Instance?.Hscene?.GetMales()?[0]; } + private static void CheckRadialMenu(RadialMenu radialMenu, HandRole handRole) + { + bool shouldActivateMenu = + VRControllerInput.IsChillGesture(handRole) || + ViveInput.GetPressDownEx(handRole, ControllerButton.AKey); + + bool shouldKeepMenuActivated = + (VRControllerInput.inHandTrackingMode && ViveInput.GetPressEx(handRole, ControllerButton.Grip)) || + ViveInput.GetPressEx(handRole, ControllerButton.AKey); + + bool menuShouldBeActive = shouldActivateMenu || (shouldKeepMenuActivated && radialMenu.isActiveAndEnabled); + if (menuShouldBeActive && !radialMenu.gameObject.activeSelf) + { + radialMenu.gameObject.SetActive(true); + radialMenu.captions = new string[] + { + "Toy", + "", + "Finish H loop stage", + "Scale reset (press trigger)", + "P show/hide", + "View reset (press trigger)", + "Male show/hide", + "Glove posing (other hand)" + }; + } + + if (!radialMenu.isActiveAndEnabled) return; + + int selectedItemIndex = radialMenu.selectedItemIndex; + // bool isTriggerDown = ViveInput.GetPressDownEx(handRole, ControllerButton.Trigger); + bool isTriggerUp = ViveInput.GetPressUpEx(handRole, ControllerButton.Trigger); + if (!menuShouldBeActive) radialMenu.gameObject.SetActive(false); + + if (menuShouldBeActive && !isTriggerUp) return; + + switch (selectedItemIndex) + { + case 0: + BetterVRPluginHelper.handHeldToy.CycleMode(handRole == HandRole.RightHand); + BetterVRPluginHelper.UpdateControllersVisibilty(); + VRControllerCollider.UpdateDynamicBoneColliders(); + break; + case 1: + // InteractionCollider.shouldVisualizeColliders = !InteractionCollider.shouldVisualizeColliders; + VRControllerCollider.UpdateDynamicBoneColliders(); + break; + case 2: + BetterVRPluginHelper.FinishH(); + break; + case 3: + if (isTriggerUp) VRControllerInput.ResetWorldScale(); + break; + case 4: + BetterVRPluginHelper.CyclePlayerPDisplayMode(); + VRControllerCollider.UpdateDynamicBoneColliders(); + break; + case 5: + if (!isTriggerUp) break; + radialMenu.gameObject.SetActive(false); + BetterVRPluginHelper.ResetView(); + BetterVRPluginHelper.UpdateControllersVisibilty(); + VRControllerCollider.UpdateDynamicBoneColliders(); + break; + case 6: + // Toggle player body visibility. + Manager.Config.HData.Visible = !Manager.Config.HData.Visible; + BetterVRPluginHelper.UpdatePlayerColliderActivity(); + VRControllerCollider.UpdateDynamicBoneColliders(); + break; + case 7: + if (!isTriggerUp) break; + if (handRole == HandRole.LeftHand) { + BetterVRPluginHelper.rightGlove?.StartRepositioning(); + } + else + { + BetterVRPluginHelper.leftGlove?.StartRepositioning(); + } + break; + default: + break; + } + } } -} \ No newline at end of file +} diff --git a/BetterVR/GUI/BetterVRPlugin.Config.cs b/BetterVR/GUI/BetterVRPlugin.Config.cs index d2c7098..3de855b 100644 --- a/BetterVR/GUI/BetterVRPlugin.Config.cs +++ b/BetterVR/GUI/BetterVRPlugin.Config.cs @@ -1,16 +1,42 @@ using BepInEx.Configuration; -using HarmonyLib; +using System.Collections.Generic; +using UnityEngine; namespace BetterVR { - public partial class BetterVRPlugin + public partial class BetterVRPlugin { public static ConfigEntry EnableControllerColliders { get; private set; } + public static ConfigEntry ControllerColliderRadius { get; private set; } + public static ConfigEntry GestureStrip { get; private set; } + + public static ConfigEntry HandHSpeedGesture { get; private set; } + public static ConfigEntry HandHSpeedSensitivity { get; private set; } + + public static ConfigEntry HapticFeedbackIntensity { get; private set; } public static ConfigEntry SetVRControllerPointerAngle { get; private set; } - public static ConfigEntry SqueezeToTurn { get; private set; } + public static ConfigEntry PlayerLogScale { get; private set; } + public static ConfigEntry SqueezeToTurn { get; private set; } + public static ConfigEntry AllowVerticalRotation { get; private set; } + public static ConfigEntry ToyMovesVerticallyWhenAttachedToBody { get; private set; } public static ConfigEntry FixWorldSizeScale { get; private set; } public static ConfigEntry MultipleRandomHeroine { get; private set; } - + public static ConfigEntry UsePrivacyScreen { get; private set; } + public static ConfigEntry SkipTitleScene { get; private set; } + public static ConfigEntry UnlockAllPositions { get; private set; } + public static ConfigEntry HandDisplay { get; private set; } + public static ConfigEntry LeftGloveOffset { get; private set; } + public static ConfigEntry LeftGloveRotation { get; private set; } + public static ConfigEntry RightGloveOffset { get; private set; } + public static ConfigEntry RightGloveRotation { get; private set; } + public static ConfigEntry GloveScale { get; private set; } + + public static ConfigEntry UseFingerTrackingGestures { get; private set; } + + public static float PlayerScale { + get { return Mathf.Pow(2, PlayerLogScale.Value); } + set { PlayerLogScale.Value = Mathf.Log(value, 2); } + } /// /// Init the Bepinex config manager options @@ -20,53 +46,159 @@ public void PluginConfigInit() EnableControllerColliders = Config.Bind("VR General", "Enable Controller Colliders (boop!)", true, "Allows collision of VR controllers with all dynamic bones"); - EnableControllerColliders.SettingChanged += EnableControllerColliders_SettingsChanged; - - SqueezeToTurn = Config.Bind("VR General", "Squeeze to Turn", true, - new ConfigDescription("Allows you to turn the headset with hand rotation while zqueezing the controller.")); - // SetVRControllerPointerAngle = Config.Bind("VR General", "(Not working yet)Laser Pointer Angle", 0, - // new ConfigDescription("0 is the default angle, and negative is down.", - // new AcceptableValueRange(-90, 90))); - // SetVRControllerPointerAngle.SettingChanged += SetVRControllerPointerAngle_SettingsChanged; + ControllerColliderRadius = Config.Bind( + "VR General", "Controller Collider Radius", 0.09f, + new ConfigDescription( + "Radius of the colliders on the controller", + new AcceptableValueRange(0.01f, 0.5f))); + + GestureStrip = Config.Bind( + "VR General", "Enable Gesture Strip", "Right hand", + new ConfigDescription( + "Enable holding trigger and dragging away to undress or holding trigger and dragging onto to dress", + new AcceptableValueList(new string[] { "Disabled", "Left hand", "Right hand" }))); + + HandHSpeedGesture = Config.Bind( + "VR General", "Hand H Speed Gesture", "Auto", + new ConfigDescription( + "Enable controlling H action speed using hand motion", + new AcceptableValueList(new string[] { "Disabled", "Button-initiated", "Auto" }))); + + HandHSpeedSensitivity = Config.Bind( + "VR General", "Hand H Speed Sensitivty", 3, + new ConfigDescription( + "Speed sensitivy when using hand movement to control H speed when touching certain parts", + new AcceptableValueRange(0f, 8f))); + + HapticFeedbackIntensity = Config.Bind( + "VR General", "Haptic Feedback Intensity", 0.125f, + new ConfigDescription("Haptic feedback intensity on controllers", new AcceptableValueRange(0f, 1f))); + + SqueezeToTurn = Config.Bind( + "VR General", "Squeeze to Turn", "One-handed", + new ConfigDescription( + "Allows you to turn the headset with hand rotation while squeezing the controller.", + new AcceptableValueList(new string[] { "Disabled", "One-handed", "Two-handed"}))); + + AllowVerticalRotation = Config.Bind( + "VR General", "Allow Vertical Rotation", false, new ConfigDescription("Allows rotating the world vertically.")); + + ToyMovesVerticallyWhenAttachedToBody = Config.Bind( + "VR General", "Toy Moves Vertically When Attached To Body", true, + new ConfigDescription("Unlocks toy vertical movement when it is attached to body.")); + + PlayerLogScale = Config.Bind( + "VR General", "Log2 of Player Scale", Mathf.Log(1.15f, 2f), + new ConfigDescription( + "Log2 of player scale when fixing world size scale, default is Log2(1.15); hold both triggers and both grips to adjust.", + new AcceptableValueRange(-4, 4))); + PlayerLogScale.SettingChanged += FixWorldSizeScale_SettingsChanged; + + SetVRControllerPointerAngle = Config.Bind("VR General", "Laser Pointer Angle", -5, + new ConfigDescription("0 is the default angle, and negative is down", + new AcceptableValueRange(-90, 90))); + SetVRControllerPointerAngle.SettingChanged += SetVRControllerPointerAngle_SettingsChanged; FixWorldSizeScale = Config.Bind("VR General", "Fix World Scale", true, new ConfigDescription("Everything appears larger in VR, so this will shrink the worldsize down to a more realistic size.")); FixWorldSizeScale.SettingChanged += FixWorldSizeScale_SettingsChanged; MultipleRandomHeroine = Config.Bind("VR General", "Multiple Heroine when Random", false, - new ConfigDescription("Will add 2 Heroine to the HScene when the 'Random' button is selected. (Default is 1)")); + new ConfigDescription("Will add 2 Heroine to the HScene when the 'Random' button is selected. (Default is 1)")); + + UsePrivacyScreen = Config.Bind("VR General", "Use Privacy Screen", true, + new ConfigDescription("Puts a black screen on desktop window")); + UsePrivacyScreen.SettingChanged += UsePrivacyScreen_SettingsChanged; + + SkipTitleScene = Config.Bind( + "VR General", "Skip Title Scene", false, + new ConfigDescription("Skip title scene and go straight to the select scene on game start.")); + + UnlockAllPositions = Config.Bind( + "VR General", "Unlock all positions", true, new ConfigDescription("Unlock all positions regardless of character status")); + + HandDisplay = Config.Bind( + "VR General", "Hand Display", "Gloves", + new ConfigDescription( + "How the hands should be diplayed as in VR", + new AcceptableValueList(new string[] { "Gloves", "Controllers", "GlovesAndControllers" }))); + + LeftGloveOffset = Config.Bind( + "VR General", + "Left Hand Offset", + new Vector3(-0.05f, 0.25f, -0.28f), + "Offset of left glove relative to controller center, use radial menu option on the other hand to start adjusting and press trigger to stop adjusting"); + + LeftGloveRotation = Config.Bind( + "VR General", + "Left Hand Rotation", + Quaternion.Euler(315, 0, 90), + "Rotation of left glove relative to controller, use radial menu option on the other hand to start adjusting and press trigger to stop adjusting"); + + RightGloveOffset = Config.Bind( + "VR General", + "Right Hand Offset", + new Vector3(0.05f, 0.25f, -0.28f), + "Offset of right glove relative to controller center, use radial menu option on the other hand to start adjusting and press trigger to stop adjusting"); ; + + RightGloveRotation = Config.Bind( + "VR General", + "Right Hand Rotation", + Quaternion.Euler(315, 0, -90), + "Rotation of right glove relative to controller, use radial menu option on the other hand to start adjusting and press trigger to stop adjusting"); + + GloveScale = Config.Bind( + "VR General", "Hand Scale", 0.14f, + new ConfigDescription("Scale of the VR gloves", new AcceptableValueRange(0.01f, 2f))); + + UseFingerTrackingGestures = Config.Bind( + "VR General", "Use hand tracking gestures", false, new ConfigDescription("Use advanced finger tracking gestures")); + } - } + internal static bool IsTwoHandedTurnEnabled() + { + return SqueezeToTurn.Value == "Two-handed"; + } + internal static bool IsOneHandedTurnEnabled() + { + return SqueezeToTurn.Value == "One-handed"; + } - /// - /// On config options changed by user, trigger stuff - /// - internal void EnableControllerColliders_SettingsChanged(object sender, System.EventArgs e) + internal static bool IsHandHSpeedGestureEnabled() { - if (!EnableControllerColliders.Value) - { - VRControllerColliderHelper.StopHelperCoroutine(); - } - else - { - VRControllerColliderHelper.TriggerHelperCoroutine(); - } + return HandHSpeedGesture.Value != "Disabled"; } + internal static bool HandHSpeedGestureRequiresButtonPress() + { + return HandHSpeedGesture.Value == "Button-initiated"; + } + + internal static bool GlovesEnabled() + { + return HandDisplay.Value == "Gloves" || HandDisplay.Value == "GlovesAndControllers"; + } - // internal void SetVRControllerPointerAngle_SettingsChanged(object sender, System.EventArgs e) - // { - // VRControllerPointer.UpdateOneOrMoreCtrlPointers(SetVRControllerPointerAngle.Value); - // } + internal static bool IsHidingControllersEnabled() + { + return HandDisplay.Value == "Gloves"; + } + internal void SetVRControllerPointerAngle_SettingsChanged(object sender, System.EventArgs e) + { + VRControllerPointer.UpdateAngles(); + } internal void FixWorldSizeScale_SettingsChanged(object sender, System.EventArgs e) { BetterVRPluginHelper.FixWorldScale(FixWorldSizeScale.Value); } - + internal void UsePrivacyScreen_SettingsChanged(object sender, System.EventArgs e) + { + BetterVRPluginHelper.UpdatePrivacyScreen(); + } } -} \ No newline at end of file +} diff --git a/BetterVR/GaugeHitIndicator.cs b/BetterVR/GaugeHitIndicator.cs new file mode 100644 index 0000000..1ff3b31 --- /dev/null +++ b/BetterVR/GaugeHitIndicator.cs @@ -0,0 +1,211 @@ +using TMPro; +using UnityEngine; + +namespace BetterVR +{ + internal class GaugeHitIndicator : MonoBehaviour + { + private static readonly Color START_COLOR = new Color(0.5f, 0.25f, 0.25f); + private static readonly Color FINISH_COLOR = Color.red; + private const int H_CAMERA_LAYER = 22; + + internal float smoothGaugeHit { get; private set; } = 0; + private float gaugeAcceleration; + private Vector3 velocity = Vector3.zero; + + private static Camera _gaugeCamera; + private static Camera gaugeCamera + { + get + { + var vrCamera = BetterVRPluginHelper.VRCamera; + if (vrCamera == null) return null; + if (_gaugeCamera == null) + { + _gaugeCamera = new GameObject().AddComponent(); + _gaugeCamera.clearFlags = CameraClearFlags.Depth; + BetterVRPlugin.Logger.LogInfo("VRCamera depth: " + vrCamera.depth + " culling mask: " + vrCamera.cullingMask); + _gaugeCamera.depth = 1; + _gaugeCamera.cullingMask = 1 << H_CAMERA_LAYER; + _gaugeCamera.renderingPath = RenderingPath.VertexLit; + } + if (_gaugeCamera.transform.parent != vrCamera.transform.parent) + { + // There is some script that automaticallly moves all cameras in the scene with HMD, + // so there is no need to parent the UI camera to the VR camera itself. + _gaugeCamera.transform.parent = vrCamera.transform.parent; + _gaugeCamera.transform.position = vrCamera.transform.position; + _gaugeCamera.transform.rotation = vrCamera.transform.rotation; + _gaugeCamera.transform.localScale = vrCamera.transform.localScale; + } + return _gaugeCamera; + } + } + + private TextMeshPro _heartSymbol = null; + private TextMeshPro _horizontalLines = null; + private TextMeshPro heartSymbol + { + get { return _heartSymbol ?? (_heartSymbol = CreateSymbol("\u2665")); } + } + private TextMeshPro horizontalLines + { + get { return _horizontalLines ?? (_horizontalLines = CreateSymbol("- -")); } + } + + void FixedUpdate() + { + var ctrl = Singleton.Instance; + var camera = BetterVRPluginHelper.VRCamera; + var vrOrigin = BetterVRPluginHelper.VROrigin; + if (!camera || !vrOrigin || !ctrl || !heartSymbol || !horizontalLines) return; + + bool isGaugeHit = ctrl.isGaugeHit && ctrl.loopType != -1; + + if (transform.parent != vrOrigin.transform) + { + transform.parent = vrOrigin.transform; + transform.localScale = Vector3.one / 32; + } + + if (isGaugeHit || (BetterVRPlugin.ILUTimer > 0 && !BetterVRPlugin.ILUCooldown)) + { + gaugeCamera.enabled = true; + smoothGaugeHit = Mathf.SmoothDamp(smoothGaugeHit, 1, ref gaugeAcceleration, 0.125f); + } + else + { + smoothGaugeHit = Mathf.SmoothDamp(smoothGaugeHit, -0.125f, ref gaugeAcceleration, 0.25f); + if (smoothGaugeHit < 0) + { + smoothGaugeHit = 0; + gaugeAcceleration = 0; + gameObject.SetActive(false); + return; + } + } + + var upDirectionInCamera = camera.transform.TransformVector( + ((Vector2) camera.transform.InverseTransformVector(vrOrigin.transform.up)).normalized); + var targetPosition = GetSnapPosition(camera.transform, upDirectionInCamera); + bool snappedToTarget = (targetPosition != null); + + if (targetPosition == null) + { + targetPosition = camera.transform.TransformPoint(0f, 0, 0.5f) + 0.1875f * upDirectionInCamera; + } + + if (smoothGaugeHit > 1 / 64f) + { + transform.position = Vector3.SmoothDamp( + transform.position, (Vector3) targetPosition, ref velocity, 1f); + } + else + { + transform.position = (Vector3)targetPosition; + velocity = Vector3.zero; + } + + transform.LookAt(camera.transform.position, upDirectionInCamera); + + UpdateSymbols(ctrl, snappedToTarget); + } + + private Vector3? GetSnapPosition(Transform camera, Vector3 upDirection) + { + var characters = Singleton.Instance?.Hscene?.GetFemales(); + + if (characters != null) + { + foreach (var character in characters) + { + if (character == null || !character.isActiveAndEnabled || !character.visibleAll) continue; + var head = character.objHeadBone.transform.position; + var target = head + upDirection * 0.25f; + if (isTargetInSnapRange(target, camera)) return target; + } + } + + return null; + } + + private TextMeshPro CreateSymbol(string text) + { + var textMesh = new GameObject().AddComponent().gameObject.AddComponent(); + textMesh.transform.SetParent(transform); + textMesh.transform.localPosition = Vector3.zero; + textMesh.transform.localRotation = Quaternion.identity; + textMesh.text = text; + textMesh.fontSize = 16; + textMesh.color = new Color(1, 0.25f, 0.25f); + textMesh.alignment = TextAlignmentOptions.Center; + textMesh.gameObject.layer = H_CAMERA_LAYER; + + textMesh.renderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off; + textMesh.renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; + textMesh.renderer.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off; + + return textMesh; + } + + private void UpdateSymbols(HSceneFlagCtrl ctrl, bool snappedToTarget) + { + var feelLevel = ctrl.feel_f; + Color color = + ShouldUsePulsingColor(ctrl.isGaugeHit, feelLevel) ? + Color.Lerp(FINISH_COLOR, Color.white, Mathf.Abs(GetPulsePhase(Singleton.Instance))) : + Color.Lerp(START_COLOR, FINISH_COLOR, feelLevel * feelLevel); + + heartSymbol.transform.localScale = Vector3.one * smoothGaugeHit; + heartSymbol.color = color; + heartSymbol.transform.localRotation = GetRotationPulse(Singleton.Instance); + heartSymbol.gameObject.SetActive(Manager.Config.HData.FeelingGauge); + + float h = horizontalLines.transform.localScale.y; + h = Mathf.Lerp(h, snappedToTarget ? -0.5f : 1.25f, Time.deltaTime * 2); + horizontalLines.transform.localScale = new Vector3(smoothGaugeHit * 5, Mathf.Clamp(h, 0, smoothGaugeHit), 1); + horizontalLines.color = color; + horizontalLines.gameObject.SetActive(Manager.Config.HData.FeelingGauge); + + } + + internal static float GetPulsePhase(HSceneFlagCtrl ctrl) + { + if (ctrl == null) return 0; + if (ctrl.feel_f >= 0.96f) return (Time.time * 3.5f) % 2 - 1; + if (ctrl.feel_f >= 0.75f) return (Time.time * 2) % 2 - 1; + return (Time.time * 1.5f) % 2 - 1; + } + + private static bool ShouldUsePulsingColor(bool isGaugeHit, float feelLevel) + { + if (VRControllerInput.IsILUGesture()) + { + return true; + } + + if (!isGaugeHit) return false; + if (feelLevel > 0.74f && feelLevel < 0.75f) return true; + return feelLevel > 0.97f; + } + + private static Quaternion GetRotationPulse(HSceneFlagCtrl ctrl) + { + return Quaternion.Euler(0, 0, Mathf.Lerp(-15, 15, Mathf.Abs(GetPulsePhase(ctrl)))); + } + + private static bool isTargetInSnapRange(Vector3 target, Transform camera) + { + var localPoint = camera.InverseTransformPoint(target); + + // If the target is too near or too far, do not snap the indicator to it. + if (localPoint.z < 0.125f || localPoint.z > 0.75f) return false; + + // If the target is too much off the center of view, do not snap the indicator to it. + if (Mathf.Abs(localPoint.x) > localPoint.z * 0.375f) return false; + if (localPoint.y < localPoint.z * (-0.5f) || localPoint.y > localPoint.z * 0.25f) return false; + + return true; + } + } +} diff --git a/BetterVR/HSpeedGesture.cs b/BetterVR/HSpeedGesture.cs new file mode 100644 index 0000000..2de9f45 --- /dev/null +++ b/BetterVR/HSpeedGesture.cs @@ -0,0 +1,459 @@ +using HTC.UnityPlugin.Vive; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; +using UnityEngine; + +namespace BetterVR +{ + public class HSpeedGesture : MonoBehaviour + { + private static readonly Regex SPNKABLE_COLLIDER_NAME_MATCHER = new Regex(@"[Ll]eg|[Ss]iri"); + private static readonly Regex MOUTH_MATCHER = new Regex(@"[Mm]outh"); + + internal float speed { get; private set; } + internal ViveRoleProperty roleProperty; + internal Transform capsuleStart; + internal Transform capsuleEnd; + internal float activationRadius = 0.25f; + internal float deactivationDistance = 0.5f; + internal float sensitivityMultiplier = 1; + internal InteractionCollider interactingCollider { get; private set; } + + // TODO: remove + internal static Vector2 hitArea; + + internal bool isTouching { get; private set; } + private static HSpeedGestureReceiver receiver; + private static FadingHaptic _leftHandHHaptic; + private static FadingHaptic _rightHandHHaptic; + private static FadingHaptic leftHandFadingHaptic + { + get + { + if (_leftHandHHaptic == null) + { + _leftHandHHaptic = new GameObject("LeftHandHHaptic").AddComponent(); + _leftHandHHaptic.onLeftHand = true; + _leftHandHHaptic.onRightHand = false; + _leftHandHHaptic.enabled = false; + } + return _leftHandHHaptic; + } + } + private static FadingHaptic rightHandFadingHaptic + { + get + { + if (_rightHandHHaptic == null) + { + _rightHandHHaptic = new GameObject("RightHandHHaptic").AddComponent(); + _rightHandHHaptic.onLeftHand = false; + _rightHandHHaptic.onRightHand = true; + _rightHandHHaptic.enabled = false; + } + return _rightHandHHaptic; + } + } + + void Awake() + { + if (receiver == null) receiver = new GameObject("HSpeedGestureReceiver").AddComponent(); + receiver.AddSourceGesture(this); + } + + void OnDestroy() + { + receiver.RemoveSourceGesture(this); + } + + void FixedUpdate() + { + if (!BetterVRPlugin.IsHandHSpeedGestureEnabled() || roleProperty == null) return; + var hCtrl = Singleton.Instance; + if (!hCtrl) return; + + speed = + VivePose.GetVelocity(roleProperty).magnitude * BetterVRPlugin.HandHSpeedSensitivity.Value * sensitivityMultiplier + + VivePose.GetAngularVelocity(roleProperty).magnitude * 0.0625f; + isTouching = ShouldBeTouching(speed); + if (!isTouching) return; + receiver.enabled = true; + + var hScene = Singleton.Instance?.Hscene; + var anim = hScene?.GetProcBase(); + + if (anim != null && anim is Spnking && interactingCollider != null && speed > 12 && + SPNKABLE_COLLIDER_NAME_MATCHER.IsMatch(interactingCollider.name) && + receiver.Spnk(hScene, hCtrl)) + { + BetterVRPlugin.Logger.LogDebug("Spnk speed: " + speed); + TriggerSpnkHaptic(); + } + else if (isTouching && speed > 0) + { + TriggerTouchingHaptic(hCtrl); + } + } + + private bool ShouldBeTouching(float speed) + { + var handRole = VRControllerInput.GetHandRole(roleProperty); + bool triggerOrGrip = + ViveInput.GetPressEx(handRole, ControllerButton.Trigger) || + ViveInput.GetPressEx(handRole, ControllerButton.Grip); + + // Movement is too slow to start activity. + if (!isTouching && speed < 0.5f && !triggerOrGrip) return false; + + if (capsuleStart == null) capsuleStart = transform; + if (capsuleEnd == null) capsuleEnd = transform; + + float scale = transform.lossyScale.x; + + if (interactingCollider) + { + Vector3 capsuleCenter = Vector3.Lerp(capsuleStart.position, capsuleEnd.position, 0.5f); + var collider = interactingCollider.GetComponent(); + if (collider != null && Vector3.Distance(capsuleCenter, collider.ClosestPoint(capsuleCenter)) < deactivationDistance * scale) + { + // Staying in the range of the current collider, can stay touching + return true; + } + } + + interactingCollider = null; + + if (BetterVRPlugin.HandHSpeedGestureRequiresButtonPress() && + handRole != HandRole.Invalid && !triggerOrGrip) + { + return false; + } + + // Look for possible interacting colliders + Collider[] colliders = Physics.OverlapCapsule(capsuleStart.position, capsuleEnd.position, activationRadius * scale, layerMask: 1 << StripUpdater.H_CAMERA_LAYER); + foreach (var collider in colliders) + { + var interactionCollider = collider.GetComponent(); + if (interactionCollider == null || !interactionCollider.IsCharacterVisible()) continue; + + if (interactionCollider.sensitivityLevel >= 2) + { + interactingCollider = interactionCollider; + return true; + } + + if (interactionCollider.sensitivityLevel == 1) + { + interactingCollider = interactionCollider; + continue; + } + + if (roleProperty == VRControllerInput.roleH && MOUTH_MATCHER.IsMatch(collider.name)) + { + interactingCollider = interactionCollider; + } + } + + return interactingCollider != null; + } + + private void TriggerSpnkHaptic() + { + if (BetterVRPlugin.HapticFeedbackIntensity.Value == 0) return; + if (roleProperty == VRControllerInput.roleL) + { + leftHandFadingHaptic.duration = 0.375f; + leftHandFadingHaptic.enabled = true; + } + else if (roleProperty == VRControllerInput.roleR) + { + rightHandFadingHaptic.duration = 0.375f; + rightHandFadingHaptic.enabled = true; + } + } + + private void TriggerTouchingHaptic(HSceneFlagCtrl ctrl) + { + if (BetterVRPlugin.HapticFeedbackIntensity.Value == 0) return; + + var amplitude = BetterVRPlugin.HapticFeedbackIntensity.Value; + if (receiver?.gaugeHitIndicator && receiver.gaugeHitIndicator.smoothGaugeHit > 0.25f) + { + // Emulate heart beats + float strength = (1 - GaugeHitIndicator.GetPulsePhase(ctrl)) % 1; + if (ctrl.feel_f >= 0.75f) + { + amplitude *= strength; + } + else + { + amplitude *= Mathf.Lerp(0.5f, 0.0625f, strength); + } + } + else + { + // Emulate touch feel + amplitude *= speed / 4; + } + var frequency = 35; + if (roleProperty != VRControllerInput.roleH) + { + ViveInput.TriggerHapticVibration(roleProperty, frequency: frequency, amplitude: amplitude); + return; + } + + // HMD has no haptic, trigger haptic on the hands unless the hands are already touching. + var leftGesture = BetterVRPluginHelper.GetLeftHand()?.GetComponentInChildren(); + if (leftGesture == null || !leftGesture.isTouching) + { + ViveInput.TriggerHapticVibration(VRControllerInput.roleL, frequency: frequency, amplitude: amplitude / 4); + } + + var rightGesture = BetterVRPluginHelper.GetRightHand()?.GetComponentInChildren(); + if (rightGesture == null || !rightGesture.isTouching) + { + ViveInput.TriggerHapticVibration(VRControllerInput.roleR, frequency: frequency, amplitude: amplitude / 4); + } + } + } + + public class HSpeedGestureReceiver : MonoBehaviour + { + internal const float IDLE_SPEED = -8f / 64; + internal const float MIN_EFFECTIVE_SPEED = -7f / 64; + internal const float LOOP_0_DEACTIVATION_THRESHOLD = -6f / 64; + internal const float LOOP_0_ACTIVATION_THRESHOLD = 0.5f; + internal const float LOOP_01_DIVIDER = 1f; // This number is from the vanilla game + internal const float LOOP_1_DEACTIVATION_THRESHOLD = 0.125f; + internal const float LOOP_1_ACTIVATION_THRESHOLD = 1.5f; + internal const float ORIGINAL_SPEED_GAUGE_RATE = 0.01f; + internal const float CUSTOM_SPEED_GAUGE_RATE = 1f / 256; + + internal static float outputY { get; private set; } + + private static FieldInfo modeCtrlField; + + internal float smoothTargetSpeed = 0; + internal GaugeHitIndicator gaugeHitIndicator { get; private set; } + private HashSet sourceGestures = new HashSet(); + + void Awake() + { + modeCtrlField = typeof(HScene).GetField("modeCtrl", BindingFlags.NonPublic | BindingFlags.Instance); + } + + void OnDisable() + { + outputY = 0; + var ctrl = Singleton.Instance; + if (ctrl) ctrl.speedGuageRate = ORIGINAL_SPEED_GAUGE_RATE; + } + + void FixedUpdate() + { + var hCtrl = Singleton.Instance; + if (!hCtrl) return; + + UpdateSmoothTargetSpeed(hCtrl, Time.fixedDeltaTime); + + if (smoothTargetSpeed < MIN_EFFECTIVE_SPEED) + { + enabled = false; + return; + } + + // Reduce feel increase rate for realism. + hCtrl.speedGuageRate = hCtrl.feel_f < 0.75f ? CUSTOM_SPEED_GAUGE_RATE : ORIGINAL_SPEED_GAUGE_RATE; + + if (hCtrl.isGaugeHit || (BetterVRPlugin.ILUTimer > 0 && !BetterVRPlugin.ILUCooldown)) + { + if (gaugeHitIndicator == null) + { + gaugeHitIndicator = new GameObject("GaugeHitIndicator").AddComponent(); + } + gaugeHitIndicator.gameObject.SetActive(true); + } + + UpdateOutput(hCtrl); + + if (IsAibu() && !hCtrl.isGaugeHit && hCtrl.loopType >= 0 && hCtrl.loopType <= 2 && + smoothTargetSpeed < LOOP_0_DEACTIVATION_THRESHOLD && !VRControllerInput.IsILUGesture()) + { + // Allow stopping action with hand motion in Aibu mode. + StopMotion(hCtrl); + } + + if (hCtrl.isGaugeHit && hCtrl.feel_f > 0.998f && hCtrl.feel_m > 0.75f) BetterVRPluginHelper.TryFinishHSameTime(); + } + + internal static bool IsAibu() + { + var anim = Singleton.Instance?.Hscene?.GetProcBase(); + return anim != null && anim is Aibu; + } + + internal static bool IsHoushi() + { + var anim = Singleton.Instance?.Hscene?.GetProcBase(); + return anim != null && anim is Houshi; + } + + private void UpdateOutput(HSceneFlagCtrl hCtrl) + { + if (hCtrl.loopType == -1) + { + if (AllowStartingHWithGesture()) + { + outputY = 1; + } + else + { + outputY = 0; + } + return; + } + + outputY = 0; + + // No-turning-back point, stay in gauge hit area + if (hCtrl.feel_f > 0.965f && hCtrl.isGaugeHit) return; + + float bufferRadius = hCtrl.wheelActionCount; + var clampedSpeed = Mathf.Clamp(smoothTargetSpeed, 0, 2); + if (hCtrl.speed < LOOP_01_DIVIDER) + { + if (clampedSpeed < LOOP_1_ACTIVATION_THRESHOLD) clampedSpeed = Mathf.Min(clampedSpeed, LOOP_01_DIVIDER - bufferRadius); + } + else + { + if (clampedSpeed > LOOP_1_DEACTIVATION_THRESHOLD) clampedSpeed = Mathf.Max(clampedSpeed, LOOP_01_DIVIDER + bufferRadius); + } + + // if (clampedSpeed >= hCtrl.speed + bufferRadius) outputY = 1; + //if (clampedSpeed <= hCtrl.speed - bufferRadius) outputY = -1; + + hCtrl.speed = clampedSpeed; + } + + internal bool Spnk(HScene hScene, HSceneFlagCtrl ctrl) + { + var anim = hScene.GetProcBase(); + if (anim == null || !(anim is Spnking)) return false; + return anim.Proc(GetModeCtrl(hScene), ctrl.nowAnimationInfo, 1); + } + + internal void AddSourceGesture(HSpeedGesture gesture) + { + sourceGestures.Add(gesture); + } + + internal void RemoveSourceGesture(HSpeedGesture gesture) + { + sourceGestures.Remove(gesture); + } + + private bool AllowStartingHWithGesture() + { + if (smoothTargetSpeed < LOOP_0_ACTIVATION_THRESHOLD) return false; + return IsAibu() || VRControllerInput.IsILUGesture(); + } + + private void UpdateSmoothTargetSpeed(HSceneFlagCtrl hCtrl, float deltaTime) + { + float targetSpeed = IDLE_SPEED; + bool hasMildTouching = false; + + foreach (var gesture in sourceGestures) + { + if (!gesture.isTouching) continue; + + targetSpeed = Mathf.Max(targetSpeed, 0); + + if (gesture.interactingCollider.sensitivityLevel == 1) + { + if (gesture.speed > 0 && gesture.speed < 1.75f) hasMildTouching = true; + if (hCtrl.loopType > 0 || smoothTargetSpeed > HSpeedGestureReceiver.LOOP_01_DIVIDER - hCtrl.wheelActionCount) + { + // Do not let touching less sensitive parts affect speed during fast movement. + if (!IsHoushi()) continue; + } + } + + var speedInput = Mathf.Clamp(gesture.speed - 0.125f, 0, 2f); + + if (hCtrl.loopType == 2) + { + // Curve speed down to require faster movement. + speedInput *= speedInput / 2; + } + else if (gesture.interactingCollider.sensitivityLevel >= 3) + { + // Curve speed up to increase sensitivity. + speedInput = Mathf.Sqrt(speedInput * 2); + } + + targetSpeed = Mathf.Max(targetSpeed, speedInput); + } + + float accelerationFactor = 1; + if (hCtrl.isGaugeHit) { + // Damp the speed more when it is in gauge hit zone to avoid voice flickering; + // Reward mild collider touching in fast loops by stabilizing gauge hit more. + accelerationFactor = hasMildTouching && hCtrl.loopType > 0 ? 0.125f : 0.25f; + } + else if (hCtrl.loopType == -1) + { + // Damp the speed to delay starting Aibu. + accelerationFactor = 0.25f; + } + + smoothTargetSpeed = Mathf.Lerp(smoothTargetSpeed, targetSpeed, deltaTime * accelerationFactor); + + // BetterVRPlugin.Logger.LogWarning( + // "H Loop type: " + hCtrl.loopType + " current speed: " + hCtrl.speed + + // " smooth target speed: " + smoothTargetSpeed); + } + + private static int GetModeCtrl(HScene hScene) + { + return (int)(modeCtrlField.GetValue(hScene) ?? -1); + } + + private static void StopMotion(HSceneFlagCtrl ctrl) + { + HScene hScene = Singleton.Instance?.Hscene; + hScene?.GetProcBase()?.SetStartMotion(true, GetModeCtrl(hScene), ctrl.nowAnimationInfo); + } + } + + internal class FadingHaptic : MonoBehaviour + { + const float DEFAULT_DURATION = 3; + private float timePassed = 0; + internal float duration = DEFAULT_DURATION; + internal bool onLeftHand = true; + internal bool onRightHand = true; + + void OnEnable() + { + timePassed = 0; + } + + void FixedUpdate() + { + if (timePassed > duration) + { + enabled = false; + return; + } + + var intensity = BetterVRPlugin.HapticFeedbackIntensity.Value * (1 - Mathf.Pow(timePassed / duration, 4f)); + + if (onLeftHand) ViveInput.TriggerHapticVibrationEx(HandRole.LeftHand, amplitude: intensity); + if (onRightHand) ViveInput.TriggerHapticVibrationEx(HandRole.RightHand, amplitude: intensity); + + timePassed += Time.fixedDeltaTime; + } + } +} diff --git a/BetterVR/HandHeldToy.cs b/BetterVR/HandHeldToy.cs new file mode 100644 index 0000000..229fd47 --- /dev/null +++ b/BetterVR/HandHeldToy.cs @@ -0,0 +1,319 @@ +using HTC.UnityPlugin.Vive; +using UnityEngine; + +namespace BetterVR +{ + public class HandHeldToy : MonoBehaviour + { + private const float HAND_HELD_RADIUS = 0.15f; + private const float BODY_HELD_RADIUS = 0.5f; + private const float HEIGHT = 3f; + private const float GRAB_RANGE = 0.1f; + private const float RECENTER_THRESHOLD = 0.5f; + + private static Transform bodyAttach; + private Vector3 bodyAttachAngularVelocity; + + private int mode = 0; + private GameObject simpleModel; + private GameObject fullModel; + private HSpeedGesture hSpeedGesture; + + internal DynamicBoneCollider collider { get; private set; } + + void Awake() + { + CreateSimpleModel(); + TryCreateFullModel(); + CreateCollider(); + simpleModel.SetActive(false); + fullModel?.SetActive(false); + collider.enabled = false; + hSpeedGesture.enabled = false; + } + + void Update() + { + if (mode == 0) return; + + if (VRControllerInput.isDraggingScale) + { + transform.SetParent(null, worldPositionStays: true); + hSpeedGesture.enabled = false; + return; + } + + var stayAttachedToBody = IsAttachedToBody() && VRControllerInput.inHandTrackingMode; + + if (!stayAttachedToBody && ViveInput.GetPressDownEx(HandRole.LeftHand, ControllerButton.Grip)) + { + var controllerCenter = BetterVRPluginHelper.leftControllerCenter; + if (controllerCenter != null && + GrabDistance(controllerCenter.position) < controllerCenter.transform.lossyScale.x * GRAB_RANGE) + { + hSpeedGesture.roleProperty = VRControllerInput.roleL; + AttachAndBringToRangeOf(controllerCenter); + } + } + else if (!stayAttachedToBody && ViveInput.GetPressDownEx(HandRole.RightHand, ControllerButton.Grip)) + { + var controllerCenter = BetterVRPluginHelper.rightControllerCenter; + if (controllerCenter != null && GrabDistance(controllerCenter.position) < controllerCenter.transform.lossyScale.x * GRAB_RANGE) + { + hSpeedGesture.roleProperty = VRControllerInput.roleR; + AttachAndBringToRangeOf(controllerCenter); + } + } + else if ( + !IsAttachedToBody() && + !ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.Grip) && + !ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.Grip)) + { + transform.SetParent(null, worldPositionStays: true); + hSpeedGesture.enabled = false; + } + + if (ShouldAttachToBody()) AttachToBody(); + + if (IsAttachedToBody()) + { + CorrectBodyAttach(smooth: true); + RotateModelsTowardTarget(); + } + + hSpeedGesture.enabled = transform.parent != null && mode != 0; + } + + internal void CycleMode(bool isRightHand) + { + mode = (mode + 1) % 3; + + if (mode != 0) + { + // Bring the newly appeared object into range of hand. + if (isRightHand) + { + AttachAndBringToRangeOf(BetterVRPluginHelper.rightControllerCenter); + } + else + { + AttachAndBringToRangeOf(BetterVRPluginHelper.leftControllerCenter); + } + transform.SetParent(null, worldPositionStays: true); + } + + TryCreateFullModel(); + if (!fullModel && mode == 1) mode = 2; + + fullModel?.SetActive(mode == 1); + simpleModel.SetActive(mode == 2); + collider.enabled = (mode != 0); + + BetterVRPluginHelper.UpdatePlayerColliderActivity(); + } + + private float GrabDistance(Vector3 grabPosition) + { + var grabOffset = transform.InverseTransformPoint(grabPosition); + grabOffset.y = Mathf.Max(0, Mathf.Abs(grabOffset.y) - HEIGHT / 2); + return transform.TransformVector(grabOffset).magnitude; + } + + private bool ShouldAttachToBody() + { + if (transform.parent == null || mode == 0 || !hSpeedGesture) return false; + + HandRole currentHand; + if (hSpeedGesture.roleProperty == VRControllerInput.roleL) + { + currentHand = HandRole.LeftHand; + } + else if (hSpeedGesture.roleProperty == VRControllerInput.roleR) + { + currentHand = HandRole.RightHand; + } + else { + return false; + } + + return VRControllerInput.CanGrabToy(currentHand); + } + + private void AttachToBody() + { + if (bodyAttach == null || bodyAttach.gameObject == null) + { + bodyAttach = new GameObject("ToyBodyAttach").transform; + } + + bodyAttach.parent = BetterVRPluginHelper.VROrigin?.transform; + if (bodyAttach.parent == null) return; + + CorrectBodyAttach(smooth: false); + + hSpeedGesture.roleProperty = VRControllerInput.roleH; + hSpeedGesture.activationRadius = BODY_HELD_RADIUS; + + transform.SetParent(bodyAttach, worldPositionStays: true); + } + + private bool IsAttachedToBody() + { + return bodyAttach != null && transform.parent == bodyAttach; + } + + private void CorrectBodyAttach(bool smooth = false) + { + var camera = BetterVRPluginHelper.VRCamera; + if (bodyAttach == null || bodyAttach.parent == null || camera == null) return; + + // Make body attach face forward horizontally. + var targetForward = Vector3.ProjectOnPlane(camera.transform.forward, bodyAttach.up); + + var targetPosition = camera.transform.TransformPoint(Vector3.down * 0.1875f); + + if (!BetterVRPlugin.ToyMovesVerticallyWhenAttachedToBody.Value) + { + targetPosition = + bodyAttach.parent.position + Vector3.ProjectOnPlane(targetPosition - bodyAttach.parent.position, bodyAttach.parent.up); + } + + if (smooth) { + targetForward = Vector3.SmoothDamp(bodyAttach.forward, targetForward, ref bodyAttachAngularVelocity, 0.25f); + targetPosition = Vector3.Lerp(bodyAttach.position, targetPosition, Time.deltaTime * 32); + } + else + { + bodyAttachAngularVelocity = Vector3.zero; + } + + bodyAttach.SetPositionAndRotation(targetPosition, Quaternion.LookRotation(targetForward, bodyAttach.parent.up)); + } + + private void ResetModelOrientation() + { + collider.transform.localRotation = Quaternion.identity; + collider.transform.localPosition = Vector3.zero; + simpleModel.transform.localRotation = Quaternion.identity; + simpleModel.transform.localPosition = Vector3.zero; + if (fullModel) + { + fullModel.transform.localRotation = Quaternion.identity; + fullModel.transform.position += transform.position - fullModel.GetComponent().bounds.center; + } + } + + private void RotateModelsTowardTarget() + { + var target = hSpeedGesture?.interactingCollider; + + var rotation = Quaternion.identity; + var localPivot = Vector3.down * HEIGHT / 4; + if (target != null && (target.name.Contains("agina") || target.name.Contains("okan"))) + { + rotation = Quaternion.LookRotation( + transform.InverseTransformPoint(target.transform.position) - localPivot, Vector3.back); + rotation = rotation * Quaternion.Euler(90, 0, 0); + } + + simpleModel.transform.localRotation = Quaternion.Slerp(simpleModel.transform.localRotation, rotation, Time.deltaTime * 4); + simpleModel.transform.localPosition = localPivot - simpleModel.transform.localRotation * localPivot; + collider.transform.localRotation = simpleModel.transform.localRotation; + collider.transform.localPosition = simpleModel.transform.localPosition; + if (fullModel) + { + fullModel.transform.localRotation = simpleModel.transform.localRotation; + fullModel.transform.position += simpleModel.transform.position - fullModel.GetComponent().bounds.center; + } + } + + private void CreateSimpleModel() + { + simpleModel = GameObject.CreatePrimitive(PrimitiveType.Cylinder); + simpleModel.transform.parent = transform; + simpleModel.transform.localScale = new Vector3(HAND_HELD_RADIUS * 2, HEIGHT / 2 - HAND_HELD_RADIUS, HAND_HELD_RADIUS * 2); + simpleModel.transform.localPosition = Vector3.zero; + simpleModel.GetOrAddComponent().GetOrAddComponent(); + Destroy(simpleModel.GetComponent()); + + var frontCap = GameObject.CreatePrimitive(PrimitiveType.Sphere); + frontCap.transform.parent = simpleModel.transform; + frontCap.transform.localScale = new Vector3(1, simpleModel.transform.localScale.x / simpleModel.transform.localScale.y, 1); + frontCap.transform.localPosition = Vector3.up; + frontCap.GetOrAddComponent().GetOrAddComponent(); + Destroy(frontCap.GetComponent()); + + var rearCap = GameObject.CreatePrimitive(PrimitiveType.Sphere); + rearCap.transform.parent = simpleModel.transform; + rearCap.transform.localScale = new Vector3(1, simpleModel.transform.localScale.x / simpleModel.transform.localScale.y, 1); + rearCap.transform.localPosition = Vector3.down; + rearCap.GetOrAddComponent().GetOrAddComponent(); + Destroy(rearCap.GetComponent()); + + hSpeedGesture = gameObject.AddComponent(); + hSpeedGesture.capsuleStart = rearCap.transform; + hSpeedGesture.capsuleEnd = frontCap.transform; + hSpeedGesture.activationRadius = HAND_HELD_RADIUS; + } + + private void TryCreateFullModel() + { + if (fullModel) return; + + GameObject prefab = null; + try + { + prefab = AssetBundleManager.LoadAssetBundle(AssetBundleNames.HH_Item01)?.Bundle?.LoadAsset( + "assets/illusion/assetbundle/hscene/h/h_item/01/p_item_vibe.prefab"); + } catch { + BetterVRPlugin.Logger.LogWarning("Cannot find toy prefab"); + return; + } + + if (!prefab) return; + + var renderer = prefab.GetComponentInChildren(); + if (renderer != null) + { + fullModel = GameObject.Instantiate(renderer.gameObject); + } + else + { + var skinnedMeshRenderer = prefab.GetComponentInChildren(); + if (!skinnedMeshRenderer) return; + + fullModel = GameObject.Instantiate(skinnedMeshRenderer.gameObject); + var mesh = skinnedMeshRenderer.sharedMesh; + var materials = skinnedMeshRenderer.materials; + Destroy(fullModel.GetComponent()); + fullModel.AddComponent().mesh = mesh; + fullModel.AddComponent().materials = materials; + } + + fullModel.transform.parent = transform; + fullModel.transform.position += transform.position - fullModel.GetComponent().bounds.center; + } + + private void CreateCollider() + { + collider = new GameObject("HandHeldToyCollider").AddComponent(); + collider.m_Direction = DynamicBoneColliderBase.Direction.Y; + collider.m_Radius = HAND_HELD_RADIUS; + collider.m_Height = HEIGHT; + collider.transform.parent = transform; + collider.transform.localPosition = Vector3.zero; + collider.transform.localRotation = Quaternion.identity; + } + + private void AttachAndBringToRangeOf(Transform parent) + { + transform.SetParent(parent, worldPositionStays: true); + if (parent != null && transform.localPosition.magnitude > RECENTER_THRESHOLD) + { + transform.localPosition = Vector3.zero; + } + hSpeedGesture.activationRadius = HAND_HELD_RADIUS; + ResetModelOrientation(); + } + } +} diff --git a/BetterVR/Properties/AssemblyInfo.cs b/BetterVR/Properties/AssemblyInfo.cs index dd90060..6bb9d90 100644 --- a/BetterVR/Properties/AssemblyInfo.cs +++ b/BetterVR/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; using BetterVR; @@ -6,7 +6,7 @@ // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("BetterVR")] -[assembly: AssemblyDescription("Adds a few enhancements to VR for story mode")] +[assembly: AssemblyDescription("Adds a handful of enhancements and new motion control features to VR for story mode")] [assembly: AssemblyProduct("BetterVR")] [assembly: AssemblyCopyright("Copyright © 2019")] diff --git a/BetterVR/RadialMenu.cs b/BetterVR/RadialMenu.cs new file mode 100644 index 0000000..a16edb5 --- /dev/null +++ b/BetterVR/RadialMenu.cs @@ -0,0 +1,174 @@ +using UnityEngine; +using UnityEngine.Rendering; +using System.Collections.Generic; +using TMPro; + +namespace BetterVR +{ + public class RadialMenu : MonoBehaviour + { + private const int ITEM_COUNT = 8; + private const float RADIUS = 1 / 16f; + private const float DEADZONE_RADIUS = 1 / 32f; + private const float ACTIVATION_DELAY_SECONDS = 0.33f; + + private GameObject cursor; + private TextMeshPro caption; + private List icons; + private LineRenderer lineRenderer; + private float activationTime; + + internal int selectedItemIndex { get; private set; } + internal Transform hand; + private string[] _captions; + internal string[] captions + { + private get { return _captions; } + set + { + _captions = value; + if (value == null) return; + for (int i = 0; i < _captions.Length; i++) + { + icons[i].text = _captions[i] == "" ? "-" : _captions[i].Substring(0, 1); + } + } + } + + void Awake() + { + CreateLines(); + CreateCursor(); + CreateCaption(); + } + + void OnEnable() + { + transform.parent = hand; + transform.localPosition = Vector3.zero; + transform.localRotation = Quaternion.Euler(90, 0, 0); + var camera = BetterVRPluginHelper.VRCamera?.transform; + if (camera) + { + transform.rotation = Quaternion.LookRotation( + Vector3.ProjectOnPlane(transform.position - camera.position, transform.up), + transform.up); + } + transform.localScale = Vector3.one; + transform.SetParent(BetterVRPluginHelper.VROrigin?.transform, worldPositionStays: true); + selectedItemIndex = -1; + activationTime = 0; + lineRenderer.enabled = false; + foreach (var icon in icons) icon.enabled = false; + } + + void Update() + { + transform.position += Vector3.Project(hand.position - transform.position, transform.forward); + + activationTime += Time.deltaTime; + + if (activationTime > ACTIVATION_DELAY_SECONDS) + { + lineRenderer.enabled = true; + foreach (var icon in icons) icon.enabled = true; + } + + Vector2 handProjection = transform.InverseTransformPoint(hand.position); + float handOffsetAmount = handProjection.magnitude; + if (handOffsetAmount < DEADZONE_RADIUS || activationTime < ACTIVATION_DELAY_SECONDS) + { + selectedItemIndex = -1; + cursor.transform.position = hand.position; + cursor.GetComponent().material.color = Color.gray; + caption.text = ""; + return; + } + + var angle = Vector2.SignedAngle(Vector2.right, handProjection); + int signedIndex = Mathf.RoundToInt(angle / 360 * ITEM_COUNT) % ITEM_COUNT; + selectedItemIndex = signedIndex >= 0 ? signedIndex : signedIndex + ITEM_COUNT; + + cursor.GetComponent().material.color = Color.yellow; + cursor.transform.localPosition = + GetCursorLocalDirection(selectedItemIndex) * Mathf.Clamp(handOffsetAmount, 0, RADIUS); + caption.text = GetCaption(selectedItemIndex); + } + + private Vector3 GetCursorLocalDirection(int itemIndex) + { + float angle = Mathf.PI * 2 * itemIndex / ITEM_COUNT; + return new Vector3(Mathf.Cos(angle), Mathf.Sin(angle)); + } + + private string GetCaption(int itemIndex) + { + if (itemIndex < 0 || captions == null || captions.Length <= itemIndex) return ""; + return captions[itemIndex]; + } + + private void CreateLines() + { + lineRenderer = gameObject.AddComponent(); + lineRenderer.useWorldSpace = false; + lineRenderer.positionCount = ITEM_COUNT * 2; + lineRenderer.widthMultiplier = 0.01f; + lineRenderer.material.color = Color.red; + + for (int i = 0; i < ITEM_COUNT; i++) + { + var cursorDirection = GetCursorLocalDirection(i); + lineRenderer.SetPosition(i * 2, Vector3.zero); + lineRenderer.SetPosition(i * 2 + 1, cursorDirection * RADIUS); + } + } + + private void CreateCursor() + { + cursor = GameObject.CreatePrimitive(PrimitiveType.Sphere); + cursor.transform.parent = transform; + cursor.transform.localScale = Vector3.one * RADIUS / 4; + var renderer = cursor.GetComponent(); + renderer.receiveShadows = false; + renderer.shadowCastingMode = ShadowCastingMode.Off; + renderer.lightProbeUsage = LightProbeUsage.Off; + renderer.reflectionProbeUsage = ReflectionProbeUsage.Off; + Destroy(cursor.GetComponent()); + } + + private void CreateCaption() + { + var captionCanvas = new GameObject("RadialMenuCaptionCanvas").AddComponent(); + captionCanvas.transform.SetParent(transform, worldPositionStays: false); + captionCanvas.transform.localScale = Vector3.one * 0.01f; + captionCanvas.GetComponent().sizeDelta = Vector3.one * 400 * RADIUS; + + var captionTransform = new GameObject("RadialMenuCaption").AddComponent(); + captionTransform.transform.SetParent(captionCanvas.transform, worldPositionStays: false); + captionTransform.localScale = Vector3.one; + captionTransform.anchorMin = captionTransform.anchorMax = new Vector2(0.5f, 0.875f); + captionTransform.offsetMin = new Vector2(-0.5f, -0.125f); + captionTransform.offsetMax = new Vector2(0.5f, 0.125f); + + caption = captionTransform.GetOrAddComponent(); + caption.alignment = TextAlignmentOptions.Center; + caption.fontSize = 16f; + caption.color = Color.yellow; + caption.m_width = captionTransform.sizeDelta.x; + + icons = new List(); + for (int i = 0; i < ITEM_COUNT; i++) + { + var iconTransform = new GameObject("RadialMenuIcon" + i).AddComponent(); + iconTransform.transform.SetParent(captionCanvas.transform, worldPositionStays: false); + iconTransform.transform.localScale = Vector3.one; + iconTransform.anchorMin = iconTransform.anchorMax = Vector3.one * 0.5f + GetCursorLocalDirection(i) * 0.3125f; + var icon = iconTransform.GetOrAddComponent(); + icon.alignment = TextAlignmentOptions.Center; + icon.fontSize = 12f; + icon.color = new Color(0.75f, 0.25f, 0); + icons.Add(icon); + } + } + } +} diff --git a/BetterVR/VRController.Collider.Helper.cs b/BetterVR/VRController.Collider.Helper.cs deleted file mode 100644 index 5d81267..0000000 --- a/BetterVR/VRController.Collider.Helper.cs +++ /dev/null @@ -1,42 +0,0 @@ -using UnityEngine; -using System.Collections; - -namespace BetterVR -{ - public static class VRControllerColliderHelper - { - internal static bool coroutineActive = false; - internal static BetterVRPlugin pluginInstance; - - - internal static void TriggerHelperCoroutine() - { - //Only trigger if not already running, and in main game - if (coroutineActive) return; - coroutineActive = true; - - pluginInstance.StartCoroutine(LoopEveryXSeconds()); - } - - - internal static void StopHelperCoroutine() - { - pluginInstance.StopCoroutine(LoopEveryXSeconds()); - coroutineActive = false; - } - - - /// - /// Got tired of searching for the correct hooks, just check for new dynamic bones on a loop. Genious! (Should be able to use CharCustFunCtrl for this later) - /// - internal static IEnumerator LoopEveryXSeconds() - { - while (coroutineActive) - { - VRControllerCollider.SetVRControllerColliderToDynamicBones(); - yield return new WaitForSeconds(3); - } - } - - } -} \ No newline at end of file diff --git a/BetterVR/VRController.Collider.cs b/BetterVR/VRController.Collider.cs index ff8bdbe..27448c6 100644 --- a/BetterVR/VRController.Collider.cs +++ b/BetterVR/VRController.Collider.cs @@ -1,162 +1,190 @@ +using System.Linq; +using System.Text.RegularExpressions; +using Manager; using UnityEngine; -using IllusionUtility.GetUtility; namespace BetterVR { - public static class VRControllerCollider - { - - /// - /// Searches for dynamic bones, and when found links them to the colliders set on the controllers - /// - internal static void SetVRControllerColliderToDynamicBones() - { - //Get all dynamic bones - var dynamicBonesV2 = GameObject.FindObjectsOfType(); - var dynamicBones = GameObject.FindObjectsOfType(); - if (dynamicBonesV2.Length == 0 && dynamicBones.Length == 0) return; - - //Get the top level VR game object - var VROrigin = BetterVRPluginHelper.GetVROrigin(); - if (VROrigin == null) return; - - //Get the controller objects we want to attach colliders to.transform.gameObject - var leftHand = BetterVRPluginHelper.GetLeftHand(); - var rightHand = BetterVRPluginHelper.GetRightHand(); - if (leftHand == null && rightHand == null) return; - - if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" Found hand for collider"); - - //Attach a dynamic bone collider to each, then link that to all dynamic bones - if (leftHand) AttachToControllerAndLink(leftHand, leftHand.GetInstanceID().ToString(), dynamicBones, dynamicBonesV2); - if (rightHand) AttachToControllerAndLink(rightHand, rightHand.GetInstanceID().ToString(), dynamicBones, dynamicBonesV2); - } - - - /// - /// Gets the transform of the squeeze button - /// - internal static Transform GetColliderPosition(GameObject hand) - { - //render location - var renderTf = hand.transform.FindLoop("OpenVRRenderModel"); - if (renderTf == null) return null; - - return renderTf; - } - - - /// - /// Adds the colliders to the controllers, and then links the dynamic bones - /// - internal static void AttachToControllerAndLink(GameObject controller, string name, DynamicBone[] dynamicBones, DynamicBone_Ver02[] dynamicBonesV2) - { - //For each vr controller add dynamic bone collider to it - var controllerCollider = GetOrAttachCollider(controller, name); - if (controllerCollider == null) return; - - //For each controller, make it collidable with all dynaic bones (Did I miss any?) - AddControllerColliderToDBv2(controllerCollider, dynamicBonesV2); - AddControllerColliderToDB(controllerCollider, dynamicBones); - } - - - /// - /// Checks for existing controller collider, or creates them - /// - internal static DynamicBoneCollider GetOrAttachCollider(GameObject controllerGameObject, string colliderName) - { - if (controllerGameObject == null) return null; - - //Check for existing DB collider that may have been attached earlier - var existingDBCollider = controllerGameObject.GetComponentInChildren(); - if (existingDBCollider == null) + public static class VRControllerCollider + { + private static readonly Regex INDEX_COLLIDING_BONE_MATCHER = new Regex(@"agina|okan|[Aa]na"); + private static DynamicBoneCollider leftControllerCollider; + private static DynamicBoneCollider rightControllerCollider; + private static DynamicBoneCollider floorCollider; + private static DynamicBoneCollider mouthCollider; + internal static Transform characterForHeightReference; + + internal static void UpdateDynamicBoneColliders() + { + var females = Singleton.Instance?.Hscene?.GetFemales(); + if (females == null || females.Length == 0) return; + + UpdateControllerColliders(); + UpdateIndexColliders(); + UpdateHandHeldToyCollider(); + UpdateFloorCollider(); + UpdateMouthCollider(); + + foreach (var character in females) { - //Add a DB collider to the controller - return AddDBCollider(controllerGameObject, colliderName); - } - - return existingDBCollider; - } - - - /// - /// Adds a dynamic bone collider to a controller GO (Thanks Anon11) - /// - internal static DynamicBoneCollider AddDBCollider(GameObject controllerGameObject, string colliderName, float colliderRadius = 0.1f, float collierHeight = 0f, - Vector3 colliderCenter = new Vector3(), DynamicBoneCollider.Direction colliderDirection = default) + if (character == null) continue; + var dynamicBones = character.GetComponentsInChildren(true); + foreach (var bone in dynamicBones) AddCollidersToBone(bone); + var dynamicBonesV2 = character.GetComponentsInChildren(true); + foreach (var bone in dynamicBonesV2) AddCollidersToBone(bone); + } + } + + private static void UpdateControllerColliders() + { + UpdateControllerCollider( + ref leftControllerCollider, + "LeftControllerCollider", + BetterVRPluginHelper.leftControllerCenter, + BetterVRPluginHelper.leftGlove?.GetComponent(), + -1); + UpdateControllerCollider( + ref rightControllerCollider, + "RightControllerCollider", + BetterVRPluginHelper.rightControllerCenter, + BetterVRPluginHelper.rightGlove?.GetComponent(), + 1); + } + + private static void UpdateControllerCollider( + ref DynamicBoneCollider collider, string name, + Transform controllerCenter, FingerPoseUpdater fingerPoses, float lateralFactor) { - var renderModelTf = GetColliderPosition(controllerGameObject); - if (renderModelTf == null) return null; - - //Build the dynamic bone collider - var colliderObject = new GameObject(colliderName); - var collider = colliderObject.AddComponent(); - collider.m_Radius = colliderRadius; - collider.m_Height = collierHeight; - collider.m_Center = colliderCenter; - collider.m_Direction = colliderDirection; - colliderObject.transform.SetParent(renderModelTf, false); - - //Move the collider more into the hand for the index controller - // var localPos = renderModelTf.up * -0.09f + renderModelTf.forward * -0.075f; - // var localPos = renderModelTf.forward * -0.075f; - // colliderObject.transform.localPosition = localPos; - - if (BetterVRPlugin.debugLog) DebugTools.DrawSphereAndAttach(renderModelTf, colliderRadius); - // if (BetterVRPlugin.debugLog) DebugTools.DrawLineAndAttach(renderModelTf, renderModelTf.TransformPoint(localPos), renderModelTf.position, localPos); - - return collider; - } - - - /// - /// Links V2 dynamic bones to a controller collider - /// - internal static void AddControllerColliderToDBv2(DynamicBoneCollider controllerCollider, DynamicBone_Ver02[] dynamicBones) - { - if (controllerCollider == null) return; - if (dynamicBones.Length == 0) return; - - int newDBCount = 0; + if (!controllerCenter) return; + + if (!collider) + { + collider = new GameObject(name).AddComponent(); + collider.m_Direction = DynamicBoneColliderBase.Direction.Z; + } + + collider.m_Radius = BetterVRPlugin.ControllerColliderRadius.Value; + // A height too small will cause the collider to be ignored by some dynamic bones. + collider.m_Height = BetterVRPlugin.ControllerColliderRadius.Value * 5; + + if (collider.transform.parent != controllerCenter) collider.transform.parent = controllerCenter; + + if (fingerPoses?.middle) + { + collider.transform.localPosition = + controllerCenter.InverseTransformPoint(fingerPoses.middle.position) + Vector3.forward * 0.005f; + } + else + { + collider.transform.localPosition = new Vector3(0.01f * lateralFactor, 0, 0.005f); + } + + collider.enabled = BetterVRPlugin.EnableControllerColliders.Value; + } + + private static void UpdateMouthCollider() + { + if (mouthCollider == null) + { + mouthCollider = new GameObject("MouthCollider").AddComponent(); + mouthCollider.m_Direction = DynamicBoneColliderBase.Direction.Z; + Transform capsuleStart = new GameObject("MouthColliderCapsuleRear").transform; + Transform capsuleEnd = new GameObject("MouthColliderCapsuleFront").transform; + capsuleStart.parent = capsuleEnd.parent = mouthCollider.transform; + capsuleStart.localPosition = Vector3.back * 0.1f; + capsuleEnd.localPosition = Vector3.forward * 0.1f; + var h = mouthCollider.GetOrAddComponent(); + h.capsuleStart = capsuleStart; + h.capsuleEnd = capsuleEnd; + h.roleProperty = VRControllerInput.roleH; + h.sensitivityMultiplier = 3; + } + + mouthCollider.m_Radius = BetterVRPlugin.ControllerColliderRadius.Value; + // A height too small will cause the collider to be ignored by some dynamic bones. + mouthCollider.m_Height = mouthCollider.m_Radius * 3; + + var camera = BetterVRPluginHelper.VRCamera; + if (!camera) return; + + if (mouthCollider.transform.parent != camera.transform) mouthCollider.transform.parent = camera.transform; + mouthCollider.transform.localRotation = Quaternion.identity; + mouthCollider.transform.localPosition = new Vector3(0, -0.08f, 0.03f); + + mouthCollider.enabled = BetterVRPlugin.EnableControllerColliders.Value; + } + + private static void UpdateFloorCollider() + { + if (floorCollider == null) + { + floorCollider = new GameObject("FloorCollider").AddComponent(); + floorCollider.m_Radius = 50f; + floorCollider.m_Height = 150f; + floorCollider.m_Center = Vector3.down * 75; + floorCollider.m_Direction = DynamicBoneColliderBase.Direction.Y; + } + + floorCollider.transform.rotation = Quaternion.identity; + Transform vrOrgin = BetterVRPluginHelper.VROrigin?.transform; + if (vrOrgin) + { + floorCollider.transform.position = + new Vector3( + vrOrgin.position.x, + characterForHeightReference == null ? vrOrgin.position.y : characterForHeightReference.position.y, + vrOrgin.position.z); + } + } + + private static void UpdateIndexColliders() + { + if (VRGlove.isShowingGloves && BetterVRPluginHelper.leftGlove) + { + var leftIndexCollider = BetterVRPluginHelper.leftGlove.GetComponent()?.indexCollider; + if (leftIndexCollider) leftIndexCollider.enabled = BetterVRPlugin.EnableControllerColliders.Value; + } - //For each heroine dynamic bone, add controller collider - for (int z = 0; z < dynamicBones.Length; z++) + if (VRGlove.isShowingGloves && BetterVRPluginHelper.rightGlove) { - //Check for existing interaction - if (!dynamicBones[z].Colliders.Contains(controllerCollider)) - { - dynamicBones[z].Colliders.Add(controllerCollider); - newDBCount++; - } - } - - if (newDBCount > 0 && BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" Linked {newDBCount} new V2 colliders"); - } - - /// - /// Links V1 dynamic bones to a controller collider - /// - internal static void AddControllerColliderToDB(DynamicBoneCollider controllerCollider, DynamicBone[] dynamicBones) + var rightIndexCollider = BetterVRPluginHelper.rightGlove.GetComponent()?.indexCollider; + if (rightIndexCollider) rightIndexCollider.enabled = BetterVRPlugin.EnableControllerColliders.Value; + } + } + + private static void UpdateHandHeldToyCollider() + { + var collider = BetterVRPluginHelper.handHeldToy?.collider; + if (collider) collider.enabled = BetterVRPlugin.EnableControllerColliders.Value; + } + + private static void AddCollidersToBone(Component bone) + { + AddColliderToBone(leftControllerCollider, bone); + AddColliderToBone(rightControllerCollider, bone); + AddColliderToBone(mouthCollider, bone); + AddColliderToBone(floorCollider, bone); + AddColliderToBone(BetterVRPluginHelper.handHeldToy?.collider, bone); + AddColliderToBone(BetterVRPluginHelper.leftGlove?.GetComponent()?.indexCollider, bone, INDEX_COLLIDING_BONE_MATCHER); + AddColliderToBone(BetterVRPluginHelper.rightGlove?.GetComponent()?.indexCollider, bone, INDEX_COLLIDING_BONE_MATCHER); + } + + private static void AddColliderToBone(DynamicBoneCollider collider, Component bone, Regex boneNameMatcher = null) { - if (controllerCollider == null) return; - if (dynamicBones.Length == 0) return; - - int newDBCount = 0; - - //For each heroine dynamic bone, add controller collider - for (int z = 0; z < dynamicBones.Length; z++) + if (collider == null) return; + if (boneNameMatcher != null && !boneNameMatcher.IsMatch(bone.name)) return; + + if (bone is DynamicBone) + { + var colliders = ((DynamicBone) bone).m_Colliders; + if (!colliders.Contains(collider)) colliders.Add(collider); + } + if (bone is DynamicBone_Ver02) { - //Check for existing interaction - if (!dynamicBones[z].m_Colliders.Contains(controllerCollider)) - { - dynamicBones[z].m_Colliders.Add(controllerCollider); - newDBCount++; - } - } - - if (newDBCount > 0 && BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" Linked {newDBCount} new V1 colliders"); - } - - } -} \ No newline at end of file + var colliders = ((DynamicBone_Ver02)bone).Colliders; + if (!colliders.Contains(collider)) colliders.Add(collider); + + } + } + } +} diff --git a/BetterVR/VRController.Hooks.cs b/BetterVR/VRController.Hooks.cs index 46c3e17..573e7b9 100644 --- a/BetterVR/VRController.Hooks.cs +++ b/BetterVR/VRController.Hooks.cs @@ -1,7 +1,12 @@ +using HTC.UnityPlugin.Vive; using HarmonyLib; +using System.Reflection; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; namespace BetterVR -{ +{ internal static class VRControllerHooks { @@ -13,35 +18,186 @@ public static void InitHooks(Harmony harmonyInstance, BetterVRPlugin _pluginInst harmonyInstance.PatchAll(typeof(VRControllerHooks)); } - /// /// When the vr controller laser pointer is updated, change the angle to the configured value /// [HarmonyPostfix, HarmonyPatch(typeof(ControllerManager), nameof(ControllerManager.SetLeftLaserPointerActive), typeof(bool))] internal static void LaserPointer_SetLeftLaserPointerActive(ControllerManager __instance, bool value) { - if (!value) return; - - //If the pointer game object is active, then set the cursor angle - if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" LaserPointer L active, setting angle to {BetterVRPlugin.SetVRControllerPointerAngle.Value}"); + if (!value) return; + VRControllerInput.controllerManager = __instance; pluginInstance.StartCoroutine( - VRControllerPointer.SetAngleAfterTime(BetterVRPlugin.SetVRControllerPointerAngle.Value, BetterVRPluginHelper.VR_Hand.left) - ); - + VRControllerPointer.SetLaserAngleWithDelay(BetterVRPluginHelper.VR_Hand.left)); } - [HarmonyPostfix, HarmonyPatch(typeof(ControllerManager), nameof(ControllerManager.SetRightLaserPointerActive), typeof(bool))] internal static void LaserPointer_SetRightLaserPointerActive(ControllerManager __instance, bool value) { - if (!value) return; - - //If the pointer game object is active, then set the cursor angle - if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" LaserPointer R active, setting angle to {BetterVRPlugin.SetVRControllerPointerAngle.Value}"); + if (!value) return; + VRControllerInput.controllerManager = __instance; pluginInstance.StartCoroutine( - VRControllerPointer.SetAngleAfterTime(BetterVRPlugin.SetVRControllerPointerAngle.Value, BetterVRPluginHelper.VR_Hand.right) - ); + VRControllerPointer.SetLaserAngleWithDelay(BetterVRPluginHelper.VR_Hand.right)); + } + + [HarmonyPrefix, HarmonyPatch(typeof(HS2VR.GripMoveCrtl), "ControllerMove")] + internal static bool ControllerMovePatch() + { + if (!ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.Grip) && + !ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.Grip)) + { + // If no grip is pressed, allow vanilla logic to handle turning. + return true; + } + + return !BetterVRPlugin.IsOneHandedTurnEnabled() && !BetterVRPlugin.IsTwoHandedTurnEnabled(); + } + + [HarmonyPrefix, HarmonyPatch(typeof(HScene), "GetAxis")] + public static bool HSceneGetAxisPatch(HandRole _hand, ref Vector2 __result) + { + bool shouldFallback = PatchGetAxis(_hand, ref __result); + if (HSpeedGestureReceiver.outputY != 0) + { + __result.y += HSpeedGestureReceiver.outputY; + shouldFallback = false; + } + return shouldFallback; + } + + [HarmonyPrefix, HarmonyPatch(typeof(HSceneSpriteFinishCategory), "GetAxis")] + public static bool HSceneSpriteCategoryGetAxisPatch(HandRole _hand, ref Vector2 __result) + { + return PatchGetAxis(_hand, ref __result); + } + + [HarmonyPrefix, HarmonyPatch(typeof(HS2VR.GripMoveCrtl), "GetAxis")] + public static bool PatchGetAxis(HandRole _hand, ref Vector2 __result) + { + if (_hand != HandRole.RightHand) + { + // For safety, fall back to vanilla logic in left hand as a workaround in case there is something missing in this patch. + return true; + } + + // This method works for Oculus controllers' thumbsticks too. + var axis = BetterVRPluginHelper.GetRightHandPadStickCombinedOutput(); + + if (axis == Vector2.zero) return true; + + // The vanilla pad/thumb stick detection is half broken and does not work on some platforms, giving rise to the necessity of this patch. + __result = axis; + + return false; + } + + // [HarmonyPostfix, HarmonyPatch(typeof(AIChara.ChaControl), nameof(AIChara.ChaControl.Initialize))] + // internal static void ChaControlStartPatch(AIChara.ChaControl __instance, GameObject _objRoot) + // { + // __instance.GetOrAddComponent().Init(__instance); + // } + + [HarmonyPrefix, HarmonyPatch(typeof(AIChara.ChaControl), "LoadCharaFbxDataAsync")] + internal static void ChaControlLoadCharaFbxDataAsyncPrefix(AIChara.ChaControl __instance) + { + if (__instance.sex == 1) __instance.GetOrAddComponent().Init(__instance); + + if (__instance.name.Contains("chaF_001")) + { + VRControllerCollider.characterForHeightReference = __instance.transform; + } + } + + [HarmonyPostfix, HarmonyPatch(typeof(AIChara.ChaControl), "LoadCharaFbxDataAsync")] + internal static void ChaControlLoadCharaFbxDataAsyncPostfix(AIChara.ChaControl __instance) + { + BetterVRPluginHelper.UpdateControllersVisibilty(); + VRControllerCollider.UpdateDynamicBoneColliders(); } + [HarmonyPrefix, HarmonyPatch(typeof(HSceneSprite), nameof(HSceneSprite.OnClickFinishInSide))] + internal static void HSceneFinishPatch() + { + pluginInstance.GetOrAddComponent().enabled = true; + } + + [HarmonyPrefix, HarmonyPatch(typeof(HSceneSprite), nameof(HSceneSprite.OnClickFinishOutSide))] + internal static void HSceneFinishPatchO() + { + HSceneFinishPatch(); + } + + [HarmonyPrefix, HarmonyPatch(typeof(HSceneSprite), nameof(HSceneSprite.OnClickFinishVomit))] + internal static void HSceneFinishPatchV() + { + HSceneFinishPatch(); + } + + [HarmonyPrefix, HarmonyPatch(typeof(HSceneSprite), nameof(HSceneSprite.OnClickFinishDrink))] + internal static void HSceneFinishPatchD() + { + HSceneFinishPatch(); + } + + [HarmonyPrefix, HarmonyPatch(typeof(HSceneSprite), nameof(HSceneSprite.OnClickFinishSame))] + internal static void HSceneFinishPatchS() + { + HSceneFinishPatch(); + } + + private const float MIN_TOUCH_INTERACTION_INTERVAL = 0.5f; + private static Selectable lastTouchedSelectable; + + [HarmonyPostfix, HarmonyPatch(typeof(Selectable), nameof(Selectable.OnPointerEnter))] + internal static void SelectablePointerEnterPatch(Selectable __instance, PointerEventData eventData) + { + if (!VRControllerInput.inHandTrackingMode) return; + if (!(eventData is VivePointerEventData)) return; + var handRole = ((VivePointerEventData)eventData).viveRole == VRControllerInput.roleL ? HandRole.LeftHand : HandRole.RightHand; + + lastTouchedSelectable = __instance; + var cooldown = BetterVRPlugin.touchInteractionCooldown; + if (cooldown > 0) return; + BetterVRPlugin.touchInteractionCooldown = MIN_TOUCH_INTERACTION_INTERVAL; + if (!VRControllerInput.MenuAutoGrab.CanClickByTouch(handRole)) return; + + var clickables = __instance.GetComponentsInChildren(); + // Enable touch-clicking in hand tracking mode + foreach (var clickable in clickables) clickable.OnPointerClick(eventData); + } + + [HarmonyPrefix, HarmonyPatch(typeof(VivePointerEventData), nameof(VivePointerEventData.GetPress))] + internal static bool VivePointerEventDataPressPatch(VivePointerEventData __instance, ref bool __result) + { + var role = __instance.viveRole == VRControllerInput.roleL ? HandRole.LeftHand : HandRole.RightHand; + if (!VRControllerInput.MenuAutoGrab.InTouchMode(role)) return true; + + if (!__instance.pointerCurrentRaycast.isValid) return true; + + // Enable touch-panning in hand tracking mode + __result = true; + return false; + } + + [HarmonyPrefix, HarmonyPatch(typeof(Illusion.Component.UI.ColorPicker.Info), "SetImagePosition")] + internal static void ColorPickerFix(Illusion.Component.UI.ColorPicker.Info __instance, PointerEventData cursorPos) + { + var dummyRtField = + typeof(Illusion.Component.UI.ColorPicker.Info).GetField( + "dummyRT", BindingFlags.NonPublic | BindingFlags.Instance); + var dummyRt = (RectTransform)dummyRtField.GetValue(__instance); + if (dummyRt != null) return; + + // Some color pickers in the game is missing dummyRT and does not respond to cursor drag properly. + // Add dummyRT to fix it as needed. + dummyRt = new GameObject().AddComponent(); + dummyRt.transform.SetParent(__instance.transform); + dummyRt.transform.localPosition = Vector3.zero; + dummyRt.transform.localRotation = Quaternion.identity; + dummyRt.anchorMin = dummyRt.anchorMax = Vector2.zero; + dummyRt.offsetMin = Vector2.one * -0.5f; + dummyRt.offsetMax = Vector2.one * 0.5f; + dummyRtField.SetValue(__instance, dummyRt); + BetterVRPlugin.Logger.LogInfo("Added dummyRt to " + __instance.name + " to fix color picker " + dummyRt.rect); + } } -} \ No newline at end of file +} diff --git a/BetterVR/VRController.Input.cs b/BetterVR/VRController.Input.cs index 47c87ef..d12503d 100644 --- a/BetterVR/VRController.Input.cs +++ b/BetterVR/VRController.Input.cs @@ -1,47 +1,871 @@ -using UnityEngine; +using HTC.UnityPlugin.Pointer3D; using HTC.UnityPlugin.Vive; +using Illusion.Extensions; +using System; +using System.Reflection; +using TMPro; +using UnityEngine; namespace BetterVR -{ +{ public static class VRControllerInput { + internal static ViveRoleProperty roleH { get; private set; } = ViveRoleProperty.New(DeviceRole.Hmd); + internal static ViveRoleProperty roleR { get; private set; } = ViveRoleProperty.New(HandRole.RightHand); + internal static ViveRoleProperty roleL { get; private set; } = ViveRoleProperty.New(HandRole.LeftHand); + private static ControllerManager _controllerManager; + internal static ControllerManager controllerManager { + get { return _controllerManager ?? (_controllerManager = GameObject.FindObjectOfType()); } + set { _controllerManager = value; } + } - internal static ViveRoleProperty roleR = ViveRoleProperty.New(HandRole.RightHand); - internal static ViveRoleProperty roleL = ViveRoleProperty.New(HandRole.LeftHand); + internal static bool inHandTrackingMode { get; private set; } + internal static bool isDraggingScale { get { return twoHandedWorldGrab != null && twoHandedWorldGrab.canScale; } } + private static TwoHandedWorldGrab _twoHandedWorldGrab; + private static TwoHandedWorldGrab twoHandedWorldGrab + { + get + { + if (_twoHandedWorldGrab == null || _twoHandedWorldGrab.gameObject == null) + { + _twoHandedWorldGrab = new GameObject("WorldGrabScale").AddComponent(); + _twoHandedWorldGrab.enabled = false; + } + return _twoHandedWorldGrab; + } + } + private static bool leftHandTriggerAndGrip; + private static bool rightHandTriggerAndGrip; + private static Vector3? originalLeftRaycasterPosition; + private static Vector3? originalRightRaycasterPosition; + internal static Vector3 handMidpointLocal + { + get { return Vector3.Lerp(VivePose.GetPose(roleL).pos, VivePose.GetPose(roleR).pos, 0.5f); } + } + + internal static float handDistanceLocal + { + get + { + return Vector3.Distance(VivePose.GetPose(VRControllerInput.roleL).pos, VivePose.GetPose(VRControllerInput.roleR).pos); + } + } + internal static Pointer3DRaycaster leftRaycaster + { + get { return BetterVRPluginHelper.GetLeftHand()?.GetComponentInChildren(true); } + } + + internal static Pointer3DRaycaster rightRaycaster + { + get { return BetterVRPluginHelper.GetRightHand()?.GetComponentInChildren(true); } + } + + internal static HandRole GetHandRole(ViveRoleProperty roleProperty) + { + if (roleProperty == roleL) return HandRole.LeftHand; + if (roleProperty == roleR) return HandRole.RightHand; + return HandRole.Invalid; + } + /// - /// When user squeezes the grip, turn the camera via wrists angular veolcity + /// Handles world scaling, rotation, and locomotion when user squeezes the grip /// - internal static void CheckInputForSqueezeTurn() + internal static void UpdateSqueezeMovement() { - //When squeezing the grip, apply hand rotation to the headset - if (ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.Grip)) + bool wasInHandTrackingMode = inHandTrackingMode; + UpdateHandTrackingMode(); + if (inHandTrackingMode != wasInHandTrackingMode) { - BetterVRPluginHelper.GetVROrigin(); - if (BetterVRPluginHelper.VROrigin == null) return; + UpdateCursorAttachPosition(); + RestoreRaycasters(); + } + VRControllerPointer.UpdateStabilizer(BetterVRPluginHelper.GetLeftHand(), ShouldFreezeLaser(HandRole.LeftHand)); + VRControllerPointer.UpdateStabilizer(BetterVRPluginHelper.GetRightHand(), ShouldFreezeLaser(HandRole.RightHand)); + + Transform vrOrigin = BetterVRPluginHelper.VROrigin?.transform; + if (!vrOrigin) return; + + var wasHoldingLeftHandTriggerAndGrip = leftHandTriggerAndGrip; + var wasHoldingRightHandTriggerAndGrip = rightHandTriggerAndGrip; + + leftHandTriggerAndGrip = + ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.Trigger) && + ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.Grip); + rightHandTriggerAndGrip = + ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.Trigger) && + ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.Grip); + bool bothGrips = + ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.Grip) && + ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.Grip); + + bool twoHandedTurn = BetterVRPlugin.IsTwoHandedTurnEnabled() && bothGrips; + bool shouldScale = inHandTrackingMode ? + CanScaleByGesture() : (leftHandTriggerAndGrip && rightHandTriggerAndGrip); + + twoHandedWorldGrab.enabled = shouldScale || twoHandedTurn; + twoHandedWorldGrab.canScale = shouldScale; + + bool allowOneHandedWorldGrab = + !twoHandedWorldGrab.enabled && (BetterVRPlugin.IsOneHandedTurnEnabled() || BetterVRPlugin.IsTwoHandedTurnEnabled()); - //Hand velocity along Y axis - var velocity = VivePose.GetAngularVelocity(roleL); - BetterVRPluginHelper.VROrigin.transform.Rotate(0f, -(velocity.y * Time.deltaTime * 100)/3f, 0f, Space.Self); + if (BetterVRPluginHelper.leftControllerCenter) + { + var leftHandWorldGrab = BetterVRPluginHelper.leftControllerCenter.GetOrAddComponent(); + if (!leftHandTriggerAndGrip || rightHandTriggerAndGrip || !allowOneHandedWorldGrab) + { + leftHandWorldGrab.enabled = false; + } + else if (leftHandWorldGrab.enabled == false) + { + if ((!inHandTrackingMode || CanScaleOrMoveByGesture(HandRole.RightHand)) || + !MenuAutoGrab.hasMenu || + (!wasHoldingLeftHandTriggerAndGrip && IsCloseToWaist(BetterVRPluginHelper.leftControllerCenter.position))) + { + leftHandWorldGrab.enabled = true; + } + } } - //Do for both hands - if (ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.Grip)) + if (BetterVRPluginHelper.rightControllerCenter) { - BetterVRPluginHelper.GetVROrigin(); - if (BetterVRPluginHelper.VROrigin == null) return; + var rightHandWorldGrab = BetterVRPluginHelper.rightControllerCenter.GetOrAddComponent(); + if (!rightHandTriggerAndGrip || leftHandTriggerAndGrip || !allowOneHandedWorldGrab) + { + rightHandWorldGrab.enabled = false; + } + else if (rightHandWorldGrab.enabled == false) + { + if ((!inHandTrackingMode || CanScaleOrMoveByGesture(HandRole.LeftHand)) || + !MenuAutoGrab.hasMenu || + (!wasHoldingRightHandTriggerAndGrip && IsCloseToWaist(BetterVRPluginHelper.rightControllerCenter.position))) + { + rightHandWorldGrab.enabled = true; + } + } + } - var velocity = VivePose.GetAngularVelocity(roleR); - BetterVRPluginHelper.VROrigin.transform.Rotate(0f, -(velocity.y * Time.deltaTime * 100)/3f, 0f, Space.Self); + if (!isDraggingScale && bothGrips && + ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.AKey) && + ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.AKey)) + { + twoHandedWorldGrab.enabled = false; + ResetWorldScale(); } + } + + internal static void ResetWorldScale() + { + var vrOrigin = BetterVRPluginHelper.VROrigin?.transform; + if (!vrOrigin) return; + + var handMidpoint = vrOrigin.TransformPoint(handMidpointLocal); + + BetterVRPlugin.PlayerLogScale.Value = (float)BetterVRPlugin.PlayerLogScale.DefaultValue; + + RestoreHandMidpointWorldPosition(handMidpoint); + } + + internal static void RestoreHandMidpointWorldPosition(Vector3? desiredWorldPosition) + { + var vrOrigin = BetterVRPluginHelper.VROrigin?.transform; + if (desiredWorldPosition == null || vrOrigin == null) return; + vrOrigin.Translate((Vector3)desiredWorldPosition - vrOrigin.TransformPoint(handMidpointLocal), Space.World); + } + + internal static bool IsILUGesture() + { + return IsILUGesture(HandRole.LeftHand) || IsILUGesture(HandRole.RightHand); + } + + internal static bool IsILUGesture(HandRole handRole) + { + return inHandTrackingMode && + !ViveInput.GetPressEx(handRole, ControllerButton.AKeyTouch) && + !ViveInput.GetPressEx(handRole, ControllerButton.BkeyTouch) && + ViveInput.GetAxisEx(handRole, ControllerAxis.IndexCurl) < 0.3f && + ViveInput.GetAxisEx(handRole, ControllerAxis.MiddleCurl) > 0.8f && + ViveInput.GetAxisEx(handRole, ControllerAxis.RingCurl) > 0.8f && + ViveInput.GetAxisEx(handRole, ControllerAxis.PinkyCurl) < 0.3f; + } + + internal static bool IsChillGesture(HandRole handRole) + { + return inHandTrackingMode && + !ViveInput.GetPressEx(handRole, ControllerButton.AKeyTouch) && + !ViveInput.GetPressEx(handRole, ControllerButton.BkeyTouch) && + ViveInput.GetAxisEx(handRole, ControllerAxis.IndexCurl) > 0.8f && + ViveInput.GetAxisEx(handRole, ControllerAxis.MiddleCurl) > 0.8f && + ViveInput.GetAxisEx(handRole, ControllerAxis.RingCurl) > 0.8f && + ViveInput.GetAxisEx(handRole, ControllerAxis.PinkyCurl) < 0.3f; + } + + internal static bool IsPeaceGesture(HandRole handRole) + { + if (!inHandTrackingMode) return false; + if (!ViveInput.GetPressEx(handRole, ControllerButton.AKeyTouch) && + !ViveInput.GetPressEx(handRole, ControllerButton.BkeyTouch)) return false; + + return + ViveInput.GetAxisEx(handRole, ControllerAxis.IndexCurl) < 0.3f && + ViveInput.GetAxisEx(handRole, ControllerAxis.MiddleCurl) < 0.3f && + ViveInput.GetAxisEx(handRole, ControllerAxis.RingCurl) > 0.8f && + ViveInput.GetAxisEx(handRole, ControllerAxis.PinkyCurl) > 0.8f; + } + + internal static bool TightGrip(HandRole handRole) + { + return inHandTrackingMode && + ViveInput.GetAxisEx(handRole, ControllerAxis.MiddleCurl) > 0.8f && + ViveInput.GetAxisEx(handRole, ControllerAxis.RingCurl) > 0.8f && + ViveInput.GetAxisEx(handRole, ControllerAxis.PinkyCurl) > 0.8f; + } - //Oculus input - // if (_hand == HandRole.LeftHand) - // { - // return OVRInput.Get(OVRInput.RawAxis2D.LThumbstick, OVRInput.Controller.Active); - // } - // return OVRInput.Get(OVRInput.RawAxis2D.RThumbstick, OVRInput.Controller.Active); + internal static bool TightGrip(ViveRoleProperty handRole) + { + return inHandTrackingMode && + ViveInput.GetAxis(handRole, ControllerAxis.MiddleCurl) > 0.8f && + ViveInput.GetAxis(handRole, ControllerAxis.RingCurl) > 0.8f && + ViveInput.GetAxis(handRole, ControllerAxis.PinkyCurl) > 0.8f; + } + + internal static bool CanStripUsingGesture(ViveRoleProperty handRole, bool wasGrabbing) + { + if (!inHandTrackingMode || ViveInput.GetAxis(handRole, ControllerAxis.MiddleCurl) < 0.5f) return false; + if (wasGrabbing) return true; + + return ViveInput.GetAxis(handRole, ControllerAxis.IndexCurl) < 0.5f && + ViveInput.GetAxis(handRole, ControllerAxis.MiddleCurl) > 0.6f && + ViveInput.GetAxis(handRole, ControllerAxis.RingCurl) < 0.7f && + ViveInput.GetAxis(handRole, ControllerAxis.PinkyCurl) < 0.4f; + } + + internal static bool ShouldFreezeLaser(HandRole handRole) + { + if (ViveInput.GetPressEx(handRole, ControllerButton.AKeyTouch) || + ViveInput.GetPressEx(handRole, ControllerButton.BkeyTouch) || + !TightGrip(handRole)) return false; + var otherHandRole = handRole == HandRole.LeftHand ? HandRole.RightHand : HandRole.LeftHand; + return TightGrip(otherHandRole) && ViveInput.GetAxisEx(otherHandRole, ControllerAxis.IndexCurl) > 0.8f; + } + + internal static bool CanGrabToy(HandRole handRole) + { + if (inHandTrackingMode) + { + return ViveInput.GetPressEx(handRole, ControllerButton.Grip) && + ViveInput.GetPressEx(handRole, ControllerButton.AKeyTouch) && + ViveInput.GetAxisEx(handRole, ControllerAxis.PinkyCurl) < 0.3f; + } + + return ViveInput.GetPressEx(handRole, ControllerButton.Grip) && + ViveInput.GetPressEx(handRole, ControllerButton.AKey); + } + + internal static bool CanOpenMenuByGesture(HandRole handRole) + { + if (!inHandTrackingMode) return false; + if (!ViveInput.GetPressEx(handRole, ControllerButton.AKeyTouch) && + !ViveInput.GetPressEx(handRole, ControllerButton.BkeyTouch)) return false; + return ViveInput.GetAxisEx(handRole, ControllerAxis.IndexCurl) < 0.3f && + ViveInput.GetAxisEx(handRole, ControllerAxis.MiddleCurl) < 0.3f && + ViveInput.GetAxisEx(handRole, ControllerAxis.RingCurl) > 0.3f && + ViveInput.GetAxisEx(handRole, ControllerAxis.PinkyCurl) < 0.3f; + } + + internal static bool CanCloseMenuByGesture(HandRole handRole) + { + return handRole != HandRole.Invalid && IsChillGesture(handRole); + } + + internal static bool CanScaleByGesture() + { + return CanScaleOrMoveByGesture(HandRole.LeftHand) && CanScaleOrMoveByGesture(HandRole.RightHand); + } + + internal static bool CanScaleOrMoveByGesture(HandRole handRole) + { + if (!inHandTrackingMode) return false; + if (ViveInput.GetAxisEx(handRole, ControllerAxis.IndexCurl) < 0.4f && + ViveInput.GetAxisEx(handRole, ControllerAxis.MiddleCurl) > 0.7f && + ViveInput.GetAxisEx(handRole, ControllerAxis.RingCurl) < 0.4f && + ViveInput.GetAxisEx(handRole, ControllerAxis.PinkyCurl) > 0.7f) return true; + return ViveInput.GetAxisEx(handRole, ControllerAxis.IndexCurl) > 0.7f && + ViveInput.GetAxisEx(handRole, ControllerAxis.MiddleCurl) < 0.4f && + ViveInput.GetAxisEx(handRole, ControllerAxis.RingCurl) > 0.7f && + ViveInput.GetAxisEx(handRole, ControllerAxis.PinkyCurl) < 0.3f; + } + + private static void UpdateHandTrackingMode() + { + if (!BetterVRPlugin.UseFingerTrackingGestures.Value) + { + inHandTrackingMode = false; + return; + } + + const float MIN_CURL_DIFF = 0.5f; + + if (inHandTrackingMode) + { + if (ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.AKey) || + ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.BKey) || + ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.AKey) || + ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.AKey)) + { + inHandTrackingMode = false; + } + } + else + { + if (GetCurlDiff(HandRole.LeftHand) > MIN_CURL_DIFF || GetCurlDiff(HandRole.RightHand) > MIN_CURL_DIFF) + { + inHandTrackingMode = true; + } + } + } + + private static void UpdateCursorAttachPosition() + { + if (BetterVRPluginHelper.leftCursorAttach != null) + { + if (inHandTrackingMode && BetterVRPluginHelper.leftGlove != null) + { + BetterVRPluginHelper.leftCursorAttach.position = BetterVRPluginHelper.leftGlove.transform.TransformPoint(new Vector3(-3f, 0.25f, 0.75f)); + } + else { + BetterVRPluginHelper.leftCursorAttach.localPosition = new Vector3(0, 0.0625f, 0.125f); + } + } + + if (BetterVRPluginHelper.rightCursorAttach != null) + { + if (inHandTrackingMode && BetterVRPluginHelper.rightGlove != null) { + BetterVRPluginHelper.rightCursorAttach.position = BetterVRPluginHelper.rightGlove.transform.TransformPoint(new Vector3(3f, 0.25f, 0.75f)); + } + else { + BetterVRPluginHelper.rightCursorAttach.localPosition = new Vector3(0, 0.0625f, 0.125f); + } + } + } + + internal static void ResumeMovement() + { + var preventMovement = BetterVRPluginHelper.VROrigin?.GetComponent(); + if (preventMovement) preventMovement.enabled = false; + } + + internal static void PlaceTouchModeRaycasters() + { + if (leftRaycaster) + { + if (originalLeftRaycasterPosition == null) originalLeftRaycasterPosition = leftRaycaster.transform.localPosition; + leftRaycaster.FarDistance = inHandTrackingMode ? 0.08f : 20f; + if (inHandTrackingMode && !TightGrip(HandRole.RightHand)) { + var index = BetterVRPluginHelper.leftGlove?.GetComponent()?.index; + if (index) leftRaycaster.transform.position = index.position; + } + } + if (rightRaycaster) + { + if (originalRightRaycasterPosition == null) originalRightRaycasterPosition = rightRaycaster.transform.localPosition; + rightRaycaster.FarDistance = inHandTrackingMode ? 0.08f : 20f; + if (inHandTrackingMode && !TightGrip(HandRole.LeftHand)) + { + var index = BetterVRPluginHelper.rightGlove?.GetComponent()?.index; + if (index) rightRaycaster.transform.position = index.position; + } + } + } + + internal static void RestoreRaycasters() + { + if (leftRaycaster) + { + leftRaycaster.FarDistance = 20f; + if (originalLeftRaycasterPosition != null) leftRaycaster.transform.localPosition = originalLeftRaycasterPosition.Value; + } + if (rightRaycaster) + { + rightRaycaster.FarDistance = 20f; + if (originalRightRaycasterPosition != null) rightRaycaster.transform.localPosition = originalRightRaycasterPosition.Value; + } + } + + private static float GetCurlDiff(HandRole handRole) + { + var middleCurl = ViveInput.GetAxisEx(handRole, ControllerAxis.MiddleCurl); + var pinkyCurl = ViveInput.GetAxisEx(handRole, ControllerAxis.PinkyCurl); + return Mathf.Abs(middleCurl - pinkyCurl); + } + + private static bool IsCloseToWaist(Vector3 position) + { + var range = BetterVRPlugin.PlayerScale * 0.25f; + Collider[] colliders = Physics.OverlapSphere(position, range, 1 << StripUpdater.H_CAMERA_LAYER); + foreach (Collider collider in colliders) + { + InteractionCollider interactionCollider = collider.GetComponent(); + if (interactionCollider != null && interactionCollider.IsCharacterVisible() && interactionCollider.name.Contains("osi")) return true; + } + return false; + } + + public class TwoHandedWorldGrab : MonoBehaviour + { + private float scaleDraggingFactor; + private Vector3? desiredHandMidpointWorldCoordinates; + private static Vector3? lastHandPositionDifference = null; + private static TextMeshPro _scaleIndicator; + private static TextMeshPro scaleIndicator + { + get + { + if (!_scaleIndicator || !_scaleIndicator.gameObject) _scaleIndicator = CreateScaleIndicator(); + return _scaleIndicator; + } + } + private bool _canScale = false; + internal bool canScale + { + get { return _canScale; } + set + { + if (_canScale != value) InitializeScaleDraggingFactor(); + _canScale = value; + } + } + + void OnEnable() + { + if (canScale) InitializeScaleDraggingFactor(); + + + var vrOrigin = BetterVRPluginHelper.VROrigin; + if (vrOrigin == null) + { + desiredHandMidpointWorldCoordinates = null; + lastHandPositionDifference = null; + } + else + { + ResumeMovement(); + desiredHandMidpointWorldCoordinates = vrOrigin.transform.TransformPoint(handMidpointLocal); + lastHandPositionDifference = VivePose.GetPose(roleR).pos - VivePose.GetPose(roleL).pos; + } + } + + void OnDisable() + { + _canScale = false; + if (scaleIndicator) scaleIndicator.enabled = false; + } + + void OnRenderObject() + { + var vrOrigin = BetterVRPluginHelper.VROrigin?.transform; + if (!vrOrigin) return; + + if (BetterVRPlugin.IsTwoHandedTurnEnabled()) + { + Vector3 handPositionDifference = VivePose.GetPose(roleR).pos - VivePose.GetPose(roleL).pos; + if (lastHandPositionDifference != null) + { + Quaternion localRotationDelta = + Quaternion.FromToRotation(handPositionDifference, (Vector3)lastHandPositionDifference); + + if (BetterVRPlugin.AllowVerticalRotation.Value) + { + vrOrigin.rotation = vrOrigin.rotation * localRotationDelta; + } + else + { + vrOrigin.Rotate(0, localRotationDelta.eulerAngles.y, 0, Space.Self); + } + } + lastHandPositionDifference = handPositionDifference; + } + + scaleIndicator.enabled = canScale; + + if (canScale) + { + var scale = scaleDraggingFactor / VRControllerInput.handDistanceLocal; + BetterVRPlugin.PlayerScale = scale; + scaleIndicator?.SetText("" + String.Format("{0:0.000}", scale)); + } + + VRControllerInput.RestoreHandMidpointWorldPosition(desiredHandMidpointWorldCoordinates); + } + + private void InitializeScaleDraggingFactor() + { + scaleDraggingFactor = handDistanceLocal * BetterVRPlugin.PlayerScale; + } + + private static TextMeshPro CreateScaleIndicator() + { + var camera = BetterVRPluginHelper.VRCamera; + if (!camera) return null; + var textMesh = + new GameObject().AddComponent().gameObject.AddComponent(); + textMesh.transform.SetParent(camera.transform); + textMesh.transform.localPosition = new Vector3(0, 0.25f, 0.75f); + textMesh.transform.localRotation = Quaternion.identity; + textMesh.transform.localScale = Vector3.one * 0.1f; + textMesh.fontSize = 16; + textMesh.color = Color.blue; + textMesh.alignment = TextAlignmentOptions.Center; + return textMesh; + } + } + + public class OneHandedWorldGrab : MonoBehaviour + { + Transform worldPivot; + Transform vrOrginPlacer; + Transform stabilizer; + Quaternion vrOriginStartRotation; + Quaternion vrOriginInverseStartRotation; + Vector3 desiredControllerPosition; + + void Awake() + { + (worldPivot = new GameObject().transform).parent = new GameObject("RotationStabilizedController").transform; + worldPivot.parent.parent = transform; + worldPivot.parent.localPosition = Vector3.zero; + worldPivot.parent.localRotation = Quaternion.identity; + (vrOrginPlacer = new GameObject().transform).parent = new GameObject().transform; + } + + void OnEnable() + { + var vrOrigin = BetterVRPluginHelper.VROrigin; + if (!vrOrigin) return; + + ResumeMovement(); + + // Place the world pivot at neutral rotation. + worldPivot.rotation = vrOriginStartRotation = vrOrigin.transform.rotation; + vrOriginInverseStartRotation = Quaternion.Inverse(vrOriginStartRotation); + // Pivot the world around the controller. + worldPivot.localPosition = Vector3.zero; + + desiredControllerPosition = worldPivot.position; + } + + void OnRenderObject() + { + if (stabilizer) worldPivot.parent.rotation = stabilizer.rotation; + + var vrOrigin = BetterVRPluginHelper.VROrigin; + if (!vrOrigin) return; + + + if (!BetterVRPlugin.IsOneHandedTurnEnabled()) + { + worldPivot.rotation = Quaternion.identity; + } + else if (!BetterVRPlugin.AllowVerticalRotation.Value || + (inHandTrackingMode && !CanScaleOrMoveByGesture(HandRole.LeftHand) && !CanScaleOrMoveByGesture(HandRole.RightHand))) + { + // Remove vertical rotation. + var angles = (vrOriginInverseStartRotation * worldPivot.rotation).eulerAngles; + worldPivot.rotation = vrOriginStartRotation * Quaternion.Euler(0, angles.y, 0); + } + + // Make sure the position and rotation of the vrOriginPlacer's parent is the same as teh world pivot. + vrOrginPlacer.parent.SetPositionAndRotation(worldPivot.transform.position, worldPivot.transform.rotation); + + // Use vrOrginPlacer to record the current vrOrigin rotation and position + vrOrginPlacer.SetPositionAndRotation(vrOrigin.transform.position, vrOrigin.transform.rotation); + + // Move the vrOriginPlacer's parent to where the controller should be to see how that affects vrOriginPlacer. + vrOrginPlacer.parent.SetPositionAndRotation(desiredControllerPosition, vrOriginStartRotation); + + // Move and rotate vrOrgin to restore the original position and rotation of the controller. + vrOrigin.transform.SetPositionAndRotation(vrOrginPlacer.position, vrOrginPlacer.rotation); + } + } + + public class PreventMovement : MonoBehaviour + { + private const float EXPIRATION_TIME = 16; + private Vector3 persistentPosition; + private Quaternion persitionRotation; + private float activeTime; + + void OnEnable() + { + persistentPosition = transform.position; + persitionRotation = transform.rotation; + activeTime = 0; + } + + void Update() + { + activeTime += Time.deltaTime; + + // Stop attempting to restore camera transform if there is any input that might move the camera. + if (activeTime > EXPIRATION_TIME || + Mathf.Abs(BetterVRPluginHelper.GetLeftHandPadStickCombinedOutput().x) > 0.25f || + Mathf.Abs(BetterVRPluginHelper.GetRightHandPadStickCombinedOutput().x) > 0.25f || + !Manager.HSceneManager.isHScene) + { + this.enabled = false; + return; + } + + if (!inHandTrackingMode && + (ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.Grip) || + ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.Grip))) + { + this.enabled = false; + return; + } + + // Force restoring last known camera transform before animation change + // since vanilla game erroneously resets camera after changing animation + // even if the camera init option is toggled off. + transform.SetPositionAndRotation(persistentPosition, persitionRotation); + } + } + + // Attaches a small menu to the hand after long pressing Y/B + public class MenuAutoGrab : MonoBehaviour + { + private static FieldInfo cgMenuField; + private float BUTTON_PRESS_TIME_THRESHOLD = 0.5f; + private float leftButtonPressTime = 0; + private float rightButtonPressTime = 0; + private Transform controllerCenter; + private Vector3? originalScale; + private float? originalLaserWidth; + private CanvasGroup selectMenu; + private CanvasGroup menu; + private Transform selectMenuOriginalTransform; + private bool isInUse = false; + internal HandRole handRole { get; private set; } = HandRole.Invalid; + internal static bool hasMenu { get; private set; } + + void Awake() + { + if (cgMenuField == null) cgMenuField = typeof(HS2VR.OpenUICrtl).GetField("cgMenu", BindingFlags.Instance | BindingFlags.NonPublic); + } + + void OnRenderObject() + { + if (isInUse) VRControllerInput.PlaceTouchModeRaycasters(); + } + + void Update() + { + if (menu == null) + { + // The controller manager might become stale later, clear the cache. + controllerManager = null; + var ctrl = GetComponent(); + if (ctrl != null) menu = (CanvasGroup)cgMenuField.GetValue(ctrl); + if (menu == null) menu = selectMenu = FindSelectMenu(); + } + + var camera = BetterVRPluginHelper.VRCamera; + + if (menu == null || camera == null) + { + // Allow toggling right hand laser in title scene. + if (CanOpenMenuByGesture(HandRole.LeftHand)) + { + controllerManager?.SetRightLaserPointerActive(true); + controllerManager?.UpdateActivity(); + } + else if (CanOpenMenuByGesture(HandRole.RightHand)) + { + controllerManager?.SetLeftLaserPointerActive(true); + controllerManager?.UpdateActivity(); + } + else if (CanCloseMenuByGesture(HandRole.LeftHand) || CanCloseMenuByGesture(HandRole.RightHand)) + { + controllerManager?.SetLeftLaserPointerActive(false); + controllerManager?.SetRightLaserPointerActive(false); + controllerManager?.UpdateActivity(); + } + // The controller manager might become stale later, clear the cache. + controllerManager = null; + hasMenu = false; + return; + } + hasMenu = true; + + if (ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.Menu) || CanOpenMenuByGesture(HandRole.LeftHand)) + { + leftButtonPressTime += Time.deltaTime; + } + else + { + leftButtonPressTime = 0; + } + + if (ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.Menu) || CanOpenMenuByGesture(HandRole.RightHand)) + { + rightButtonPressTime += Time.deltaTime; + } + else + { + rightButtonPressTime = 0; + } + + if (isInUse) + { + if (menu.alpha < 0.9f) + { + // Reset menu scale to vanilla size and close it. + if (originalScale != null && menu != selectMenu) menu.transform.localScale = (Vector3)originalScale; + if (originalLaserWidth != null) SetLaserWidths((float)originalLaserWidth); + isInUse = false; + + controllerManager?.SetLeftLaserPointerActive(false); + controllerManager?.SetRightLaserPointerActive(false); + controllerManager?.UpdateActivity(); + handRole = HandRole.Invalid; + VRControllerInput.RestoreRaycasters(); + return; + } + } + + var previousHandRole = handRole; + if (leftButtonPressTime >= BUTTON_PRESS_TIME_THRESHOLD) + { + handRole = HandRole.LeftHand; + controllerCenter = BetterVRPluginHelper.leftControllerCenter; + } + else if (rightButtonPressTime >= BUTTON_PRESS_TIME_THRESHOLD) + { + handRole = HandRole.RightHand; + controllerCenter = BetterVRPluginHelper.rightControllerCenter; + } + else + { + if (CanCloseMenuByGesture(HandRole.LeftHand) || CanCloseMenuByGesture(HandRole.RightHand)) + { + if (menu != selectMenu) + { + menu.Enable(false); + } + else if (selectMenuOriginalTransform != null) + { + controllerManager?.SetLeftLaserPointerActive(false); + controllerManager?.SetRightLaserPointerActive(false); + controllerManager?.UpdateActivity(); + menu.transform.SetPositionAndRotation(selectMenuOriginalTransform.position, selectMenuOriginalTransform.rotation); + menu.transform.localScale = selectMenuOriginalTransform.localScale; + } + } + handRole = HandRole.Invalid; + } + + if (handRole == HandRole.Invalid || !controllerCenter) return; + + isInUse = true; + + if (handRole != previousHandRole) + { + // Open the menu. + menu.Enable(true, true, false); + + // Scale to the right size. + if (menu != selectMenu) { + if (originalScale == null) originalScale = menu.transform.localScale; + } + else if (selectMenuOriginalTransform == null) + { + selectMenuOriginalTransform = new GameObject("SelectMenuOriginalTransform").transform; + selectMenuOriginalTransform.parent = selectMenu.transform; + selectMenuOriginalTransform.localPosition = Vector3.zero; + selectMenuOriginalTransform.localRotation = Quaternion.identity; + selectMenuOriginalTransform.localScale = Vector3.one; + selectMenuOriginalTransform.SetParent(selectMenu.transform.parent, true); + } + Vector3 newScale = controllerCenter.lossyScale / 4096f; + menu.transform.localScale = + menu.transform.parent == null ? newScale : newScale / menu.transform.parent.lossyScale.x; + + SetLaserWidths(BetterVRPlugin.PlayerScale / 2f); + + if (inHandTrackingMode) + { + controllerManager?.SetLeftLaserPointerActive(true); + controllerManager?.SetRightLaserPointerActive(true); + } + else + { + // Hide the laser on the laser hand and show the laser on the other hand. + controllerManager?.SetLeftLaserPointerActive(handRole != HandRole.LeftHand); + controllerManager?.SetRightLaserPointerActive(handRole != HandRole.RightHand); + } + controllerManager?.UpdateActivity(); + } + + if (CanOpenMenuByGesture(HandRole.LeftHand)) { + var glove = BetterVRPluginHelper.leftGlove.transform; + // Move the menu with the hand. + menu.transform.SetPositionAndRotation( + glove.TransformPoint(-3, -1, 0), + glove.rotation * Quaternion.Euler(-45, -90, 180)); + } + else if (CanOpenMenuByGesture(HandRole.RightHand)) + { + var glove = BetterVRPluginHelper.rightGlove.transform; + // Move the menu with the hand. + menu.transform.SetPositionAndRotation( + glove.TransformPoint(3, -1, 0), + glove.rotation * Quaternion.Euler(-45, 90, 180)); + } + else if (ViveInput.GetPressEx(handRole, ControllerButton.Menu)) + { + // Move the menu with the hand. + menu.transform.SetPositionAndRotation( + controllerCenter.TransformPoint(0, 1f / 32, 3f / 16), + controllerCenter.rotation * Quaternion.Euler(90, 0, 0)); + } + } + + internal static bool CanClickByTouch(HandRole handRole) + { + return InTouchMode(handRole) && ViveInput.GetPressEx(handRole, ControllerButton.Grip); + } + + internal static bool InTouchMode(HandRole handRole) + { + return VRControllerInput.inHandTrackingMode && hasMenu && !ShouldFreezeLaser(handRole); + } + + private CanvasGroup FindSelectMenu() + { + var systemButton = GameObject.Find("btnOption"); + if (systemButton == null) return null; + for (var t = systemButton.transform.parent; t != null; t = t.parent) + { + if (t.name == "MainCanvas") return t.GetComponent(); + } + return null; + } + + private void SetLaserWidths(float width) + { + SetLaserWidth(BetterVRPluginHelper.GetLeftHand(), (float)width); + SetLaserWidth(BetterVRPluginHelper.GetRightHand(), (float)width); + } + + private void SetLaserWidth(GameObject hand, float width) + { + if (hand == null) return; + var lineRenderer = hand.transform.Find("LaserPointer")?.GetComponentInChildren(true); + if (lineRenderer == null) return; + if (originalLaserWidth == null) originalLaserWidth = lineRenderer.widthMultiplier; + lineRenderer.widthMultiplier = width; + } } } -} \ No newline at end of file +} diff --git a/BetterVR/VRController.Pointer.cs b/BetterVR/VRController.Pointer.cs index 0ef168f..93e6052 100644 --- a/BetterVR/VRController.Pointer.cs +++ b/BetterVR/VRController.Pointer.cs @@ -5,102 +5,75 @@ namespace BetterVR { public static class VRControllerPointer { - /// /// Sets the angle of the laser pointer after some time to let the game objects settle /// - public static IEnumerator SetAngleAfterTime(float userAngle, BetterVRPluginHelper.VR_Hand hand = BetterVRPluginHelper.VR_Hand.none) + public static IEnumerator SetLaserAngleWithDelay(BetterVRPluginHelper.VR_Hand hand) { yield return new WaitForSeconds(0.01f); - UpdateOneOrMoreCtrlPointers(userAngle, hand); + GetHandAndSetAngle(hand); } - - /// - /// Determine whether to set both hand angles or just one - /// - public static void UpdateOneOrMoreCtrlPointers(float userAngle, BetterVRPluginHelper.VR_Hand hand = BetterVRPluginHelper.VR_Hand.none) + public static void UpdateAngles() { - if (hand == BetterVRPluginHelper.VR_Hand.none) - { - GetHandAndSetAngle(userAngle, BetterVRPluginHelper.VR_Hand.left); - GetHandAndSetAngle(userAngle, BetterVRPluginHelper.VR_Hand.right); - } - else - { - GetHandAndSetAngle(userAngle, hand); - } + GetHandAndSetAngle(BetterVRPluginHelper.VR_Hand.left); + GetHandAndSetAngle(BetterVRPluginHelper.VR_Hand.right); } - /// /// Sets the angle of the laser pointer on the VR controller to the users configured value /// - public static void GetHandAndSetAngle(float userAngle, BetterVRPluginHelper.VR_Hand hand) - { - //Get the correct hand - var vrHand = BetterVRPluginHelper.GetHand(hand); - - //Get all children components - var laserPointerTf = vrHand.transform.Find("LaserPointer"); - - //Find the component one named laser pointer - if (laserPointerTf == null) return; - - CalculateControllerPointerAngle(userAngle, laserPointerTf.gameObject, vrHand); - } - - - /// - /// Calculates the controller laser pointer angle for a single hand - /// - public static void CalculateControllerPointerAngle(float userAngle, GameObject laserPointerGO, GameObject hand) + private static void GetHandAndSetAngle(BetterVRPluginHelper.VR_Hand vrHand) { - //Subtract the desired angle from the current angle, to get the rotational difference - var rotateAmount = GetNewAngleDifference(userAngle, laserPointerGO.transform); + var controller = BetterVRPluginHelper.GetHand(vrHand); + var controllerCenter = + vrHand == BetterVRPluginHelper.VR_Hand.left ? + BetterVRPluginHelper.leftControllerCenter : + BetterVRPluginHelper.rightControllerCenter; - //Get line renderer start position to rotate around - var lineRenderer = laserPointerGO.GetComponentInChildren(); - if (lineRenderer == null) return; + if (controller == null || controllerCenter == null) return; + var raycaster = controller.GetComponentInChildren(true); + if (raycaster == null) return; - //Get the starting position - var lineRendererStartPos = laserPointerGO.transform.TransformPoint(lineRenderer.GetPosition(0)); - - if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" lineRenderer.StartPos {lineRendererStartPos} laserPointerGO {laserPointerGO.transform.position}"); - if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" line comp to start dist {Vector3.Distance(lineRendererStartPos, laserPointerGO.transform.position)}"); + // BetterVRPluginHelper.GetRightHand()?.GetComponentInChildren(); + // if (raycaster.transform.parent?.parent != controllerModel) - //Rotate from the current position to the desired position - SetControllerPointerAngle(rotateAmount, laserPointerGO, lineRendererStartPos); - - // if (BetterVRPlugin.debugLog) DebugTools.DrawSphereAndAttach(lineRenderer.transform, 0.02f, lineRenderer.transform.position + lineRendererStartPos); - } + if (controller.transform.Find("LaserPointer") == null) return; + var oldAngles = raycaster.transform.localRotation.eulerAngles; - /// - /// Set the laser pointer oject rotation - /// - public static void SetControllerPointerAngle(float rotateAmount, GameObject laserPointerGO, Vector3 lineRendererStartPos) - { - laserPointerGO.transform.RotateAround(lineRendererStartPos, laserPointerGO.transform.right, rotateAmount); + // Leave the unpatched state available as an option in case there is some problem with the patch. + if (-BetterVRPlugin.SetVRControllerPointerAngle.Value == oldAngles.x) return; - // if (BetterVRPlugin.debugLog) DebugTools.DrawSphereAndAttach(laserPointerGO.transform, 0.02f); - } + // Rotate the laser pointer to the desired angle. + raycaster.transform.localRotation = Quaternion.Euler(-BetterVRPlugin.SetVRControllerPointerAngle.Value, 0, 0); + BetterVRPlugin.Logger.LogInfo("Updated laser pointer rotation: " + oldAngles + " -> " + raycaster.transform.localRotation.eulerAngles); + UpdateStabilizer(controller, false); + } - /// - /// Get the differnce in rotation to the new angle - /// - public static float GetNewAngleDifference(float userAngle, Transform laserPointerTf) + internal static void UpdateStabilizer(GameObject controller, bool freeze) { - //Get the current laser pointer angle (0 is default) - var eulers = laserPointerTf.rotation.eulerAngles; - - if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" LaserPointer current {eulers.x} new {userAngle}"); + var stabilizer = + controller?. + GetComponentInChildren(true)?. + GetComponentInParent(); + if (!stabilizer) return; - //Subtract the desired angle from the current angle, to get the rotational difference - return userAngle - eulers.x; + if (freeze) + { + stabilizer.positionThreshold = 1 / 32f; + stabilizer.rotationThreshold = 30; + } + else + { + // The vanilla laser pointer stabilization is too aggressive and causes a laggy feel. + // Reduce the thresholds for a better balance between stability and responsiveness. + stabilizer.positionThreshold = 0; + // Hand tracking can flicker a lot and needs more stabilization. + stabilizer.rotationThreshold = VRControllerInput.inHandTrackingMode ? 1f : 0.5f; + } } - } -} \ No newline at end of file +} diff --git a/BetterVR/VRGlove.cs b/BetterVR/VRGlove.cs new file mode 100644 index 0000000..bf50dad --- /dev/null +++ b/BetterVR/VRGlove.cs @@ -0,0 +1,318 @@ +using HTC.UnityPlugin.Vive; +using HS2VR; +using IllusionUtility.GetUtility; +using UnityEngine; + +namespace BetterVR +{ + public class VRGlove : MonoBehaviour + { + private static GameObject gloves; + private static SkinnedMeshRenderer gloveRenderer; + + private HandRole handRole; + private bool isRepositioning = false; + + internal static bool isShowingGloves { get { return gloveRenderer && gloveRenderer.gameObject.activeSelf ; } } + + internal static VRGlove CreateLeftGlove() + { + return CreateGlove(HandRole.LeftHand, "cf_J_ArmLow01_L", 1); + } + + internal static VRGlove CreateRightGlove() + { + return CreateGlove(HandRole.RightHand, "cf_J_ArmLow01_R", -1); + } + + internal void StartRepositioning() + { + if (!isShowingGloves) return; + isRepositioning = true; + } + + void Update() + { + Transform controllerCenter = + handRole == HandRole.RightHand ? + BetterVRPluginHelper.rightControllerCenter : + BetterVRPluginHelper.leftControllerCenter; + + bool shouldShowHand = BetterVRPlugin.GlovesEnabled() && controllerCenter != null; + bool isShowingHand = gloveRenderer.gameObject.activeSelf; + + gloveRenderer.gameObject.SetActive(shouldShowHand); + + if (shouldShowHand != isShowingHand) BetterVRPluginHelper.UpdateControllersVisibilty(); + + if (!shouldShowHand) return; + + var camera = BetterVRPluginHelper.VRCamera; + if (camera != null && gloveRenderer.transform.parent != camera.transform) + { + // Parent the renderer to camera to make sure that the gloves stay in the renderer's bounds and do not get culled. + gloveRenderer.transform.parent = camera.transform; + gloveRenderer.transform.localPosition = Vector3.zero; + var localBounds = gloveRenderer.localBounds; + localBounds.center = Vector3.zero; + localBounds.extents = Vector3.one * 16; + gloveRenderer.localBounds = localBounds; + } + + if (isRepositioning) + { + if (ViveInput.GetPressEx(HandRole.LeftHand, ControllerButton.Grip) || + ViveInput.GetPressEx(HandRole.RightHand, ControllerButton.Grip)) + { + // Pause repositioning + if (transform.parent != controllerCenter) transform.SetParent(controllerCenter, worldPositionStays: true); + } + else + { + // Resume repositioning + if (transform.parent != null) transform.SetParent(null, worldPositionStays: true); + } + + if (ViveInput.GetPressDownEx(HandRole.LeftHand, ControllerButton.Trigger) || + ViveInput.GetPressDownEx(HandRole.LeftHand, ControllerButton.AKey) || + ViveInput.GetPressDownEx(HandRole.RightHand, ControllerButton.Trigger) || + ViveInput.GetPressDownEx(HandRole.RightHand, ControllerButton.AKey)) + { + isRepositioning = false; + transform.SetParent(controllerCenter, worldPositionStays: true); + if (handRole == HandRole.LeftHand) + { + BetterVRPlugin.LeftGloveRotation.Value = transform.localRotation; + BetterVRPlugin.LeftGloveOffset.Value = transform.localPosition; + } + else + { + BetterVRPlugin.RightGloveRotation.Value = transform.localRotation; + BetterVRPlugin.RightGloveOffset.Value = transform.localPosition; + } + BetterVRPlugin.Logger.LogInfo("Set hand offset: " + transform.localPosition + " rotation: " + transform.localRotation.eulerAngles); + } + + return; + } + + if (transform.parent != controllerCenter) + { + transform.parent = controllerCenter; + transform.localRotation = + handRole == HandRole.RightHand ? BetterVRPlugin.RightGloveRotation.Value : BetterVRPlugin.LeftGloveRotation.Value; + transform.localPosition = + handRole == HandRole.RightHand ? BetterVRPlugin.RightGloveOffset.Value : BetterVRPlugin.LeftGloveOffset.Value; + } + + if (transform.parent == null) return; + + // The render model may have been changed by the system so the simple renderer may need to be repositioned too. + transform.localScale = Vector3.one * BetterVRPlugin.GloveScale.Value; + transform.localRotation = + handRole == HandRole.RightHand ? BetterVRPlugin.RightGloveRotation.Value : BetterVRPlugin.LeftGloveRotation.Value; + transform.localPosition = + handRole == HandRole.RightHand ? BetterVRPlugin.RightGloveOffset.Value : BetterVRPlugin.LeftGloveOffset.Value; + } + + private static void LoadGloves() + { + if (gloves != null) return; + + GameObject prefab; + try + { + prefab = AssetBundleManager.LoadAssetBundle(AssetBundleNames.Chara00Mo_Gloves_00)?.Bundle?.LoadAsset( + "assets/illusion/assetbundle/prefabs/chara/male/00/mo_gloves_00/p_cm_glove_gunte.prefab"); + } + catch + { + BetterVRPlugin.Logger.LogWarning("Cannot find gloves asset, may try again later."); + return; + } + + if (!prefab) return; + BetterVRPlugin.Logger.LogInfo("Found gloves asset"); + + gloves = GameObject.Instantiate(prefab); + + gloveRenderer = gloves.GetComponentInChildren(); + if (!gloveRenderer) return; + + gloveRenderer.GetOrAddComponent(); + } + + private static VRGlove CreateGlove(HandRole handRole, string name, float rotationFactor) + { + LoadGloves(); + if (gloves == null) return null; + var transform = gloves.transform.FindLoop(name); + if (!transform) + { + BetterVRPlugin.Logger.LogWarning("Glove bones not found, trying to reload asset"); + LoadGloves(); + if (!transform) return null; + } + + var glove = transform.GetOrAddComponent(); + glove.handRole = handRole; + var fingerPoses = transform.GetOrAddComponent(); + fingerPoses.Init(handRole, rotationFactor); + var hSpeedGesture = glove.GetOrAddComponent(); + hSpeedGesture.roleProperty = handRole == HandRole.LeftHand ? VRControllerInput.roleL : VRControllerInput.roleR; + hSpeedGesture.capsuleStart = fingerPoses.ring; + hSpeedGesture.capsuleEnd = fingerPoses.index; + hSpeedGesture.activationRadius = BetterVRPlugin.ControllerColliderRadius.Value; + return glove; + } + } + + internal class FingerPoseUpdater : MonoBehaviour + { + private HandRole handRole; + private float rotationFactor = 1; + private Transform thumb; + public Transform index { get; private set; } + public Transform middle { get; private set; } + public Transform ring { get; private set; } + private Transform pinky; + + public DynamicBoneCollider indexCollider { get; private set; } + + internal void Init(HandRole handRole, float rotationFactor) + { + this.handRole = handRole; + this.rotationFactor = rotationFactor; + } + + void Awake() + { + thumb = FindFirstMatchingTransform(transform, "umb"); + index = FindFirstMatchingTransform(transform, "ndex"); + middle = FindFirstMatchingTransform(transform, "iddle"); + ring = FindFirstMatchingTransform(transform, "ing"); + pinky = FindFirstMatchingTransform(transform, "ittle"); + + if (index) + { + indexCollider = new GameObject(name + "_indexCollider").AddComponent(); + indexCollider.m_Radius = 0.03125f; + indexCollider.m_Height = 0.25f; + indexCollider.m_Direction = DynamicBoneColliderBase.Direction.X; + var colliderParent = index.childCount > 0 ? index.GetChild(0) : index; + if (colliderParent.childCount > 0) colliderParent = colliderParent.GetChild(0); + indexCollider.transform.localScale = Vector3.one; + indexCollider.transform.parent = colliderParent; + indexCollider.transform.localPosition = Vector3.zero; + indexCollider.transform.localRotation = Quaternion.identity; + } + } + + void Update() + { + if (thumb && thumb.childCount > 0) + { + if (VRControllerInput.inHandTrackingMode) + { + if (ViveInput.GetPressEx(handRole, ControllerButton.AKeyTouch)) + { + thumb.localRotation = Quaternion.Euler(0, -20 * rotationFactor, 15 * rotationFactor); + thumb.GetChild(0).localRotation = Quaternion.Euler(0, -10* rotationFactor, 30 * rotationFactor); + thumb.GetChild(0).GetChild(0).localRotation = Quaternion.Euler(0, -45 * rotationFactor, 0 * rotationFactor); + } + else if (ViveInput.GetPressEx(handRole, ControllerButton.BkeyTouch)) { + thumb.localRotation = Quaternion.Euler(0, 0 * rotationFactor, 5 * rotationFactor); + thumb.GetChild(0).localRotation = Quaternion.Euler(0, 0 * rotationFactor, 30 * rotationFactor); + thumb.GetChild(0).GetChild(0).localRotation = Quaternion.Euler(0, -10 * rotationFactor, 0 * rotationFactor); + } + else + { + thumb.localRotation = Quaternion.Euler(0, 10 * rotationFactor, 5 * rotationFactor); + thumb.GetChild(0).localRotation = Quaternion.Euler(0, 5 * rotationFactor, 30 * rotationFactor); + thumb.GetChild(0).GetChild(0).localRotation = Quaternion.Euler(0, 0 * rotationFactor, 0 * rotationFactor); + } + } + else + { + if (ViveInput.GetPressEx(handRole, ControllerButton.AKeyTouch) || + ViveInput.GetPressEx(handRole, ControllerButton.BkeyTouch) || + ViveInput.GetPressEx(handRole, ControllerButton.PadTouch) || + ViveInput.GetPressEx(handRole, ControllerButton.MenuTouch)) + { + thumb.localRotation = Quaternion.Euler(0, 10 * rotationFactor, 5 * rotationFactor); + thumb.GetChild(0).localRotation = Quaternion.Euler(0, 5 * rotationFactor, 30 * rotationFactor); + } + else + { + thumb.localRotation = Quaternion.Euler(0, 15 * rotationFactor, 20 * rotationFactor); + thumb.GetChild(0).localRotation = Quaternion.Euler(0, 10 * rotationFactor, 35 * rotationFactor); + } + } + } + + float indexCurl = ViveInput.GetAxisEx(handRole, ControllerAxis.IndexCurl); + float middleCurl = ViveInput.GetAxisEx(handRole, ControllerAxis.MiddleCurl); + float ringCurl = ViveInput.GetAxisEx(handRole, ControllerAxis.RingCurl); + float pinkyCurl = ViveInput.GetAxisEx(handRole, ControllerAxis.PinkyCurl); + + if (VRControllerInput.inHandTrackingMode && indexCurl != 0) + { + UpdateAngle(index, indexCurl * 65); + } + else + { + float indexAngle = ViveInput.GetAxisEx(handRole, ControllerAxis.Trigger); + + if (ViveInput.GetPressEx(handRole, ControllerButton.TriggerTouch)) + { + indexAngle = indexAngle * 5 + 30; + } + else + { + indexAngle = indexAngle * indexAngle * 45 + 10; + } + + UpdateAngle(index, indexAngle); + } + + if (VRControllerInput.inHandTrackingMode || middleCurl != 0 || ringCurl != 0 || pinkyCurl != 0) + { + float maxAngle = VRControllerInput.inHandTrackingMode ? 75 : 60; + + UpdateAngle(middle, middleCurl * maxAngle); + UpdateAngle(ring, ringCurl * maxAngle); + UpdateAngle(pinky, pinkyCurl * maxAngle); + return; + } + + float gripAngle = 10; + if (ViveInput.GetPressEx(handRole, ControllerButton.TriggerTouch) || + ViveInput.GetPressEx(handRole, ControllerButton.GripTouch) || + ViveInput.GetPressEx(handRole, ControllerButton.CapSenseGripTouch)) gripAngle = 70; + gripAngle += ViveInput.GetAxisEx(handRole, ControllerAxis.CapSenseGrip) * 3; + + UpdateAngle(middle, gripAngle); + UpdateAngle(ring, gripAngle * 1.15f); + UpdateAngle(pinky, gripAngle * 1.4f); + } + + private void UpdateAngle(Transform finger, float angle) + { + if (finger == null) return; + finger.localRotation = Quaternion.Euler(0, 0, angle * rotationFactor); + if (finger.childCount > 0) UpdateAngle(finger.GetChild(0), angle * 1.0625f); + } + + private static Transform FindFirstMatchingTransform(Transform transform, string partialName) + { + if (transform.name.Contains(partialName)) return transform; + + for (int i = 0; i < transform.childCount; i++) + { + Transform result = FindFirstMatchingTransform(transform.GetChild(i), partialName); + if (result) return result; + } + return null; + } + } +} diff --git a/BetterVR/VRMenu.Hooks.cs b/BetterVR/VRMenu.Hooks.cs index 7d6bac8..4f99b62 100644 --- a/BetterVR/VRMenu.Hooks.cs +++ b/BetterVR/VRMenu.Hooks.cs @@ -1,9 +1,12 @@ using HarmonyLib; using HS2VR; using Manager; +using System.Reflection; +using UnityEngine; +using UnityEngine.UI; namespace BetterVR -{ +{ internal static class VRMenuHooks { @@ -22,18 +25,145 @@ public static void InitHooks(Harmony harmonyInstance, BetterVRPlugin _pluginInst /// [HarmonyPostfix, HarmonyPatch(typeof(VRSelectScene), "Start")] internal static void VRSelectScene_Start(VRSelectScene __instance) - { + { //If the pointer game object is active, then set the cursor angle - if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" VRSelectScene_Start "); + if (BetterVRPlugin.debugLog) BetterVRPlugin.Logger.LogInfo($" VRSelectScene_Start "); //Get the character card data VRSelectManager vrMgr = Singleton.Instance; //Add Random button to GUI, next to optional button - VRMenuRandom.AppendRandomButton(__instance); - VRMenuRandom.VRSelectSceneStart(); + VRMenuRandom.AppendRandomButton(__instance); + VRMenuRandom.VRSelectSceneStart(); + BetterVRPluginHelper.UpdatePrivacyScreen(Color.gray); + } + + [HarmonyPostfix, HarmonyPatch(typeof(GripMoveCrtl), "Start")] + internal static void FindVrOrigin(GripMoveCrtl __instance) + { + GameObject objVROrigin = (GameObject)typeof(GripMoveCrtl).GetField("objVROrigin", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); + if (objVROrigin) + { + BetterVRPluginHelper.Init(objVROrigin); + } + } + + static bool FirstHSceneAnimationPending = true; + + [HarmonyPrefix, HarmonyPatch(typeof(HS2.TitleScene), "Start")] + internal static void TitleSceneStartPatch() + { + BetterVRPluginHelper.UpdatePrivacyScreen(Color.black); + } + + [HarmonyPrefix, HarmonyPatch(typeof(HScene), nameof(HScene.Start))] + internal static void HSceneStartPatch() + { + FirstHSceneAnimationPending = true; } + [HarmonyPrefix, HarmonyPatch(typeof(HScene), nameof(HScene.ChangeAnimation))] + internal static void ChangeAnimationPatch() + { + if (FirstHSceneAnimationPending) + { + FirstHSceneAnimationPending = false; + // Allow the vanilla game to reset camera for the first animation. + return; + } + + if (Manager.Config.HData.InitCamera) return; + + var preventInitCamera = BetterVRPluginHelper.VROrigin?.GetOrAddComponent(); + if (preventInitCamera) preventInitCamera.enabled = true; + } + [HarmonyPostfix, HarmonyPatch(typeof(HScene), nameof(HScene.ChangeAnimation))] + internal static void ChangeAnimationPostfix() + { + VRControllerCollider.UpdateDynamicBoneColliders(); + } + + [HarmonyPrefix, HarmonyPatch(typeof(HScene), "SetClothStateStartMotion")] + internal static bool SetClothStateStartMotionPatch() + { + return false; + } + + [HarmonyPostfix, HarmonyPatch(typeof(VRSettingUI), "Start")] + internal static void FindResetViewButton(VRSettingUI __instance) + { + Button recenterButton = (Button)typeof(VRSettingUI).GetField("btnRecenter", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); + if (recenterButton != null) + { + BetterVRPluginHelper.recenterVR = recenterButton.onClick; + } + } + + private static bool hasStartedTitleSceneForFirstTime = false; + + [HarmonyPostfix, HarmonyPatch(typeof(HS2.TitleScene), "Start")] + internal static void TitleScenePatch(HS2.TitleScene __instance) + { + bool shouldPlay = BetterVRPlugin.SkipTitleScene.Value && !hasStartedTitleSceneForFirstTime; + hasStartedTitleSceneForFirstTime = true; + if (shouldPlay) __instance.OnPlay(); + BetterVRPluginHelper.UpdateControllersVisibilty(); + } + + + [HarmonyPostfix, HarmonyPatch(typeof(HSceneManager.HSceneTables), "LoadAnimationFileName")] + internal static void PositionUnlockPatch(HSceneManager.HSceneTables __instance) + { + if (!BetterVRPlugin.UnlockAllPositions.Value || __instance.lstAnimInfo == null) return; + foreach (var infos in __instance.lstAnimInfo) + { + if (infos == null) continue; + foreach (var info in infos) + { + if (info == null || info.nStatePtns == null) continue; + for (int i = 0; i < 7; i++) if (!info.nStatePtns.Contains(i)) info.nStatePtns.Add(i); + // BetterVRPlugin.Logger.LogInfo("Unlocked position: " + info.nameAnimation); + } + } + } + + [HarmonyPrefix, HarmonyPatch(typeof(HScene), "CheckStartBase")] + internal static void CheckStartBasePrefix() + { + PositionUnlockPatch(HSceneManager.HResourceTables); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(AIChara.ChaControl), "UpdateVisible")] + private static void UpdateVisible() + { + BetterVRPluginHelper.UpdatePrivacyScreen(Color.black); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(OpenUICrtl), "Start")] + private static void OpenUICrtlPatch(OpenUICrtl __instance) + { + __instance.GetOrAddComponent(); + } + + [HarmonyPrefix, HarmonyPatch(typeof(DynamicBone_Ver02), "Awake")] + internal static void SiriBoneRadiusFix(DynamicBone_Ver02 __instance) + { + foreach (var pattern in __instance.Patterns) + { + // Changing dynamic bone parameters is ineffective once it is active, so the must be changed before Awake() is called. + foreach (var param in pattern.Params) + { + // BetterVRPlugin.Logger.LogWarning("DBV2 " + __instance.name + " ptn " + pattern.Name + " param " + param.Name + " " + param.CollisionRadius + " " + param.NextBoneLength + " " + param.Stiffness); + if (param.Name.Contains("Siri") || param.Name.Contains("siri")) + { + // Increase siri collision radius since the vanilla radius is too small for motion control interaction. + param.CollisionRadius = Mathf.Max(param.CollisionRadius, 0.875f); + } + } + } + } } -} \ No newline at end of file +} diff --git a/BetterVR/VRMenu.Random.cs b/BetterVR/VRMenu.Random.cs index 02de599..aaaf23a 100644 --- a/BetterVR/VRMenu.Random.cs +++ b/BetterVR/VRMenu.Random.cs @@ -20,7 +20,6 @@ public enum FemaleIndex public static List females = new List(); public static List males = new List(); - /// /// When the Random button is presses, set a random female/male, and start the HScene /// @@ -36,6 +35,8 @@ public static void OnSelectRandomBtn() SetMales(); + BetterVRPluginHelper.UpdatePrivacyScreen(Color.white); + //Load the HScene Scene.LoadReserve(new Scene.Data { @@ -255,6 +256,8 @@ internal static void AppendRandomButton(VRSelectScene __instance) OnSelectRandomBtn(); }); + __instance.GetOrAddComponent().randomButton = btn; + // if (BetterVRPlugin.debugLog) DebugTools.LogChildrenComponents(btnGOCopy); //Set back to parent on canvas @@ -285,4 +288,20 @@ internal static void VRSelectSceneStart() } -} \ No newline at end of file + internal class StartRandomOnKeyEnter : MonoBehaviour + { + internal Button randomButton; + + void Update() + { + if (Singleton.Instance == null || Scene.IsNowLoading || randomButton == null) return; + + if (Input.GetKeyDown("enter") || Input.GetKeyDown("return")) + { + randomButton.onClick.Invoke(); + Destroy(this); + } + } + } + +} diff --git a/BetterVR/VrController.StripUpdater.cs b/BetterVR/VrController.StripUpdater.cs new file mode 100644 index 0000000..95fec8f --- /dev/null +++ b/BetterVR/VrController.StripUpdater.cs @@ -0,0 +1,472 @@ +using AIChara; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.UI; +using HTC.UnityPlugin.Vive; + +namespace BetterVR +{ + + public class StripUpdater + { + internal const int H_CAMERA_LAYER = 22; + internal static readonly Color[] STRIP_INDICATOR_COLORS = + new Color[] { Color.blue, Color.red, Color.cyan, Color.magenta, Color.yellow, Color.green, Color.white, Color.black }; + + private const float STRIP_START_RANGE = 0.5f; + private const float STRIP_MIN_DRAG_RANGE = 0.75f; + private Canvas clothIconCanvas; + private List clothIcons = new List(); + private List strippedClothIcons = new List(); + private bool finishedLoadingClothIcons = false; + private ViveRoleProperty handRole; + private Vector3 stripStartPos; + private bool canClothe; + private StripCollider grabbedStripCollider; + private MeshRenderer stripIndicator; + private bool grabbing; + + internal StripUpdater(ViveRoleProperty handRole) + { + this.handRole = handRole; + } + + internal void CheckStrip(bool enable) + { + if (BetterVRPluginHelper.VROrigin == null) return; + + Vector3 handPos = + BetterVRPluginHelper.VROrigin.transform.TransformPoint(VivePose.GetPose(handRole).pos); + + if (!enable || (!VRControllerInput.inHandTrackingMode && ViveInput.GetPress(handRole, ControllerButton.Grip))) + { + grabbedStripCollider = null; + canClothe = false; + stripIndicator?.gameObject.SetActive(false); + grabbing = false; + return; + } + + if (!VRControllerInput.inHandTrackingMode && ViveInput.GetPressUp(handRole, ControllerButton.Trigger)) + { + if (grabbedStripCollider != null && canClothe) + { + if (Vector3.Distance(handPos, stripStartPos) > STRIP_MIN_DRAG_RANGE * BetterVRPlugin.PlayerScale) + { + grabbedStripCollider.Clothe(); + if (BetterVRPlugin.HapticFeedbackIntensity.Value > 0) + { + ViveInput.TriggerHapticVibration(handRole, amplitude: BetterVRPlugin.HapticFeedbackIntensity.Value); + } + } + } + canClothe = false; + grabbedStripCollider = null; + stripIndicator?.gameObject.SetActive(false); + grabbing = false; + return; + } + + grabbing = VRControllerInput.inHandTrackingMode? + (VRControllerInput.CanStripUsingGesture(handRole, wasGrabbing: grabbing)) : ViveInput.GetPress(handRole, ControllerButton.Trigger); + if (grabbing) + { + if (canClothe) + { + grabbedStripCollider = FindClosestStripCollider(handPos, STRIP_START_RANGE * BetterVRPlugin.PlayerScale, 1, 2); + UpdateStripIndicator(); + } + else + { + if (finishedLoadingClothIcons) + { + for (int i = 0; i < clothIcons.Count; i++) + { + if (clothIcons[i] == null || strippedClothIcons[i] == null) + { + finishedLoadingClothIcons = false; + break; + } + if (clothIcons[i].activeSelf) + { + clothIcons[i].SetActive(false); + strippedClothIcons[i].SetActive(true); + break; + } + } + } + + if (grabbedStripCollider == null || Vector3.Distance(handPos, stripStartPos) < STRIP_MIN_DRAG_RANGE * BetterVRPlugin.PlayerScale) + { + return; + } + + if (Vector3.Angle(handPos - stripStartPos, grabbedStripCollider.transform.position - stripStartPos) > 90) + { + if (grabbedStripCollider.StripMore() && BetterVRPlugin.HapticFeedbackIntensity.Value > 0) + { + ViveInput.TriggerHapticVibration(handRole, amplitude: BetterVRPlugin.HapticFeedbackIntensity.Value); + } + } + else + { + if (grabbedStripCollider.StripLess() && BetterVRPlugin.HapticFeedbackIntensity.Value > 0) + { + ViveInput.TriggerHapticVibration(handRole, amplitude: BetterVRPlugin.HapticFeedbackIntensity.Value); + } + } + stripStartPos = handPos; + } + return; + } + + grabbedStripCollider = FindClosestStripCollider(handPos, STRIP_START_RANGE * BetterVRPlugin.PlayerScale, 0, VRControllerInput.inHandTrackingMode ? (byte) 2 : (byte) 1); + UpdateStripIndicator(); + stripStartPos = handPos; + canClothe = (grabbedStripCollider == null); + } + + private void LoadClothIconsIfNeeded() + { + if (finishedLoadingClothIcons || stripIndicator == null) + { + return; + } + + HSceneSprite hSceneSprite = Singleton.Instance; + if (!hSceneSprite || !hSceneSprite.objCloth || hSceneSprite.objCloth.objs == null) + { + return; + } + + while (clothIcons.Count < 8) clothIcons.Add(null); + while (strippedClothIcons.Count < 8) strippedClothIcons.Add(null); + + if (!clothIconCanvas) clothIconCanvas = new GameObject("ClothIconCanvas").AddComponent(); + clothIconCanvas.transform.SetParent(stripIndicator.transform, false); + clothIconCanvas.transform.localPosition = Vector3.zero; + clothIconCanvas.transform.localRotation = Quaternion.Euler(120, 0, 0); + clothIconCanvas.transform.localScale = Vector3.one * 6; + + bool waitingForIcons = false; + for (int i = 0; i < 8; i++) + { + if (clothIcons[i] != null && strippedClothIcons[i] != null) continue; + + var allClothButtons = hSceneSprite.objCloth.objs; + if (allClothButtons == null || allClothButtons.Count <= i) + { + waitingForIcons = true; + continue; + } + var buttons = allClothButtons[i].buttons; + if (buttons.Length < 2) + { + waitingForIcons = true; + continue; + } + + GameObject clothIcon = GameObject.Instantiate(buttons[0].gameObject); + GameObject strippedClothIcon = GameObject.Instantiate(buttons[1].gameObject); + Object.Destroy(clothIcon.GetComponent