diff --git a/CHANGELOG.md b/CHANGELOG.md index 647df8c..5228398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +# 1.1.0 + +## Detached Arms + +You can now detach your arms from your body, giving you more control over your aim, not being constrained by the maximum length of the Semibot's arms anymore. You can change this setting at any time, no restarts or rejoins needed. + +## Eye Tracking + +RepoXR v1.1.0 adds support for eye tracking (for the three people that have it). + +#### Singleplayer + +Players that use eye tracking will have enhanced immersion when it comes to "looking at" things. A few of the enemies in R.E.P.O. behave differently depending on if you're looking at them or not. This detection now factors in where you are looking with your eyes! + +Discovering valuables, or your lost friends' heads will also make use of eye tracking. Just look at the items and the game will discover them without you having to move your head. + +#### Multiplayer + +When playing with other people, other people will see your pupils move based on your real eye movement. Looking down? People will see you look down. Looking straight through your friend's soul because they broke something? Yup, they'll see that! + +#### Configuration + +Eye tracking can be enabled and disabled mid-game, there's no need to restart. If your headset does not support eye tracking, it will be considered disabled (even when eye tracking is enabled in the config), so no need to change the settings if you don't have eye tracking. + +## Hotswap + +You can now swap between VR mode and flatscreen mode by pressing the F8 button on your keyboard while you are in the main menu or in the lobby menu. This works even when you are the host (though the lobby will reload for everybody). + +**Additions**: +- Added eye tracking support +- Added an option to detach arms from body +- Added support for the new climbing mechanic +- Added support for spectating your death head +- Added support for the monster update +- Added hotswapping in the main menu (F8) + +**Changes**: +- The ceiling eye now darkens the world except for where the eye is (no more cheating hihi) +- Removed the performance tab and replaced it with UI in the settings +- Replaced the valuable discover overlay with a new 3D graphic (supports custom colors) +- You now look at the enemy/object that killed you while the death animation plays (if possible) +- Slightly optimized the custom camera by adding a frame rate limiter (disabled by default) +- Optimized framerate by forcibly disabling ambient occlusion (20%-40% less render time) +- Renamed "Dynamic Smooth Speed" to "Analog Smooth Turn" +- Changed the minimum possible HUD height value to account for detached hands +- The map tool can now also be grabbed from behind your head (near the shoulders) +- You can now unbind controls at your leisure + +**Removals**: +- Removed support for REPO v0.2.x + # 1.0.3 **Additions**: diff --git a/Preload/RepoXR.Preload.csproj b/Preload/RepoXR.Preload.csproj index fccda4f..3f3e826 100644 --- a/Preload/RepoXR.Preload.csproj +++ b/Preload/RepoXR.Preload.csproj @@ -4,7 +4,7 @@ netstandard2.1 RepoXR Preloader DaXcess - 1.0.3 + 1.1.0 true latest RepoXR.Preload diff --git a/README.md b/README.md index 85be51a..7093afa 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Here is a list of RepoXR versions and which version(s) of R.E.P.O. it supports | RepoXR | R.E.P.O. Version | |--------|------------------| +| v1.1.0 | v0.3.0 | | v1.0.3 | v0.2.1 | | v1.0.2 | v0.2.1 | | v1.0.1 | v0.2.1 | diff --git a/RepoXR.csproj b/RepoXR.csproj index 37d45e9..f6013f4 100644 --- a/RepoXR.csproj +++ b/RepoXR.csproj @@ -2,7 +2,7 @@ Collecting Valuables in VR - 1.0.3 + 1.1.0 DaXcess true latest @@ -28,13 +28,14 @@ - + + - - + + diff --git a/Source/Assets/AssetCollection.cs b/Source/Assets/AssetCollection.cs index 45d300c..c423dd4 100644 --- a/Source/Assets/AssetCollection.cs +++ b/Source/Assets/AssetCollection.cs @@ -11,6 +11,8 @@ internal static class AssetCollection { private static AssetBundle assetBundle; + public static OpenXRFeaturePack OpenXRFeatures; + public static RemappableControls RemappableControls; public static GameObject RebindHeader; @@ -21,6 +23,8 @@ internal static class AssetCollection public static GameObject VRTumble; public static GameObject Keyboard; public static GameObject ExpressionWheel; + public static GameObject ValuableDiscover; + public static GameObject FocusSphere; public static GameObject MenuSettings; public static GameObject MenuSettingsCategory; @@ -47,6 +51,8 @@ internal static class AssetCollection public static AnimationCurveData HurtHapticCurve; public static AnimationCurveData EyeAttachHapticCurve; public static AnimationCurveData KeyboardAnimation; + + public static GameObject Cube; public static bool LoadAssets() { @@ -59,6 +65,8 @@ public static bool LoadAssets() return false; } + OpenXRFeatures = assetBundle.LoadAsset("OpenXRFeatures"); + RemappableControls = assetBundle.LoadAsset("RemappableControls").GetComponent(); RebindHeader = assetBundle.LoadAsset("Rebind Header"); @@ -69,6 +77,8 @@ public static bool LoadAssets() VRTumble = assetBundle.LoadAsset("VRTumble"); Keyboard = assetBundle.LoadAsset("NonNativeKeyboard"); ExpressionWheel = assetBundle.LoadAsset("Expression Radial"); + ValuableDiscover = assetBundle.LoadAsset("Valuable Discover"); + FocusSphere = assetBundle.LoadAsset("Focus Sphere"); MenuSettings = assetBundle.LoadAsset("VR Settings Page"); MenuSettingsCategory = assetBundle.LoadAsset("VR Settings Page - Category"); @@ -96,6 +106,8 @@ public static bool LoadAssets() EyeAttachHapticCurve = assetBundle.LoadAsset("EyeAttachHapticCurve"); KeyboardAnimation = assetBundle.LoadAsset("KeyboardAnimation"); + Cube = assetBundle.LoadAsset("JustACube"); + if (RemappableControls?.controls == null) { Logger.LogError( diff --git a/Source/Compat.cs b/Source/Compat.cs index 199acdf..feb9f0c 100644 --- a/Source/Compat.cs +++ b/Source/Compat.cs @@ -5,6 +5,7 @@ namespace RepoXR; public static class Compat { public const string UnityExplorer = "com.sinai.unityexplorer"; + public const string CustomDiscoverStateLib = "Kistras-CustomDiscoverStateLib"; public static bool IsLoaded(string modId) { diff --git a/Source/Config.cs b/Source/Config.cs index 39ef093..330a252 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -38,6 +38,10 @@ public class Config(string assemblyPath, ConfigFile file) public ConfigEntry LeftHandDominant { get; } = file.Bind("Gameplay", nameof(LeftHandDominant), false, "Whether to use the left or right hand as dominant hand (the hand used to pick up items)"); + [ConfigDescriptor(customName: "Arms", falseText: "Attached", trueText: "Detached")] + public ConfigEntry DetachedArms { get; } = file.Bind("Gameplay", nameof(DetachedArms), false, + "Whether your arms are attached to your body, or if they are separate"); + [ConfigDescriptor] public ConfigEntry HapticFeedback { get; } = file.Bind("Gameplay", nameof(HapticFeedback), HapticFeedbackOption.All, @@ -45,29 +49,28 @@ public class Config(string assemblyPath, ConfigFile file) "Controls how much haptic feedback you will experience while playing with the VR mod.", new AcceptableValueEnum())); - [ConfigDescriptor(pointerSize: 0.01f, stepSize: 0.05f)] - public ConfigEntry HUDPlaneOffset { get; } = file.Bind("Gameplay", nameof(HUDPlaneOffset), -0.45f, - new ConfigDescription("The default height offset for the HUD", new AcceptableValueRange(-0.6f, 0.5f))); + [ConfigDescriptor(customName: "Eye Tracking", trueText: "Enabled", falseText: "Disabled")] + public ConfigEntry EnableEyeTracking { get; } = file.Bind("Gameplay", nameof(EnableEyeTracking), true, + "If supported by the headset, use eye tracking to move your characters pupils for other players and for checking line of sight with enemies."); + + // UI configuration + + [ConfigDescriptor(customName: "HUD Height", pointerSize: 0.01f, stepSize: 0.05f)] + public ConfigEntry HUDPlaneOffset { get; } = file.Bind("UI", nameof(HUDPlaneOffset), -0.45f, + new ConfigDescription("The default height offset for the HUD", new AcceptableValueRange(-1f, 0.5f))); - [ConfigDescriptor(pointerSize: 0.01f, stepSize: 0.05f)] - public ConfigEntry HUDGazePlaneOffset { get; } = file.Bind("Gameplay", nameof(HUDGazePlaneOffset), -0.25f, - new ConfigDescription("The height offset for the HUD when looking at it", new AcceptableValueRange(-0.6f, 0.5f))); + [ConfigDescriptor(customName: "HUD Secondary Height", pointerSize: 0.01f, stepSize: 0.05f)] + public ConfigEntry HUDGazePlaneOffset { get; } = file.Bind("UI", nameof(HUDGazePlaneOffset), -0.25f, + new ConfigDescription("The height offset for the HUD when looking at it", + new AcceptableValueRange(-1f, 0.5f))); [ConfigDescriptor(pointerSize: 0.05f, stepSize: 0.25f)] - public ConfigEntry SmoothCanvasDistance { get; } = file.Bind("Gameplay", nameof(SmoothCanvasDistance), 1.5f, + public ConfigEntry SmoothCanvasDistance { get; } = file.Bind("UI", nameof(SmoothCanvasDistance), 1.5f, new ConfigDescription("The distance that the smooth canvas should be away from the main camera", new AcceptableValueRange(1.25f, 3))); - // Performance configuration - - [ConfigDescriptor(stepSize: 5f, suffix: "%")] - public ConfigEntry CameraResolution { get; } = file.Bind("Performance", nameof(CameraResolution), 100, - new ConfigDescription( - "This setting configures the resolution scale of the game, lower values are more performant, but will make the game look worse.", - new AcceptableValueRange(5, 200))); - // Input configuration - + [ConfigDescriptor(enumDisableBar: true)] public ConfigEntry TurnProvider { get; } = file.Bind("Input", nameof(TurnProvider), TurnProviderOption.Smooth, @@ -81,7 +84,7 @@ public class Config(string assemblyPath, ConfigFile file) new AcceptableValueRange(0.25f, 5))); [ConfigDescriptor] - public ConfigEntry DynamicSmoothSpeed { get; } = file.Bind("Input", nameof(DynamicSmoothSpeed), true, + public ConfigEntry AnalogSmoothTurn { get; } = file.Bind("Input", nameof(AnalogSmoothTurn), true, "When enabled, makes the speed of the smooth turning dependent on how far the analog stick is pushed."); [ConfigDescriptor(stepSize: 5, suffix: "°")] @@ -92,6 +95,12 @@ public class Config(string assemblyPath, ConfigFile file) // Rendering configuration + [ConfigDescriptor(stepSize: 5f, suffix: "%")] + public ConfigEntry CameraResolution { get; } = file.Bind("Rendering", nameof(CameraResolution), 100, + new ConfigDescription( + "This setting configures the resolution scale of the game, lower values are more performant, but will make the game look worse.", + new AcceptableValueRange(5, 200))); + [ConfigDescriptor] public ConfigEntry Vignette { get; } = file.Bind("Rendering", nameof(Vignette), true, "Enables the vignette shader used in certain scenarios and levels in the game."); @@ -101,6 +110,13 @@ public class Config(string assemblyPath, ConfigFile file) file.Bind("Rendering", nameof(CustomCamera), false, "Adds a second camera mounted on top of the VR camera that will render separately from the VR camera to the display. This requires extra GPU power!"); + [ConfigDescriptor(stepSize: 15, pointerSize: 5)] + public ConfigEntry CustomCameraFramerate { get; } = file.Bind("Rendering", nameof(CustomCameraFramerate), + 144f, + new ConfigDescription( + "The maximum frequency that the custom camera can render at. The custom camera framerate is limited to the VR headset's refresh rate, so setting this higher won't have any effect.", + new AcceptableValueRange(15, 144))); + [ConfigDescriptor(stepSize: 5)] public ConfigEntry CustomCameraFOV { get; } = file.Bind("Rendering", nameof(CustomCameraFOV), 75f, new ConfigDescription("The field of view that the custom camera should have.", @@ -124,15 +140,12 @@ public class Config(string assemblyPath, ConfigFile file) "FOR INTERNAL USE ONLY, DO NOT EDIT"); private static bool leftHandedWarningShown; - + /// /// Create persistent callbacks that persist for the entire duration of the application /// public void SetupGlobalCallbacks() { - if (!VRSession.InVR) - return; - CameraResolution.SettingChanged += (_, _) => { XRSettings.eyeTextureResolutionScale = CameraResolution.Value / 100f; @@ -140,6 +153,9 @@ public void SetupGlobalCallbacks() CustomCamera.SettingChanged += (_, _) => { + if (!VRSession.InVR) + return; + if (CustomCamera.Value) Object.Instantiate(AssetCollection.CustomCamera, Camera.main!.transform.parent); else diff --git a/Source/Data/OpenXRFeaturePack.cs b/Source/Data/OpenXRFeaturePack.cs new file mode 100644 index 0000000..ff5009f --- /dev/null +++ b/Source/Data/OpenXRFeaturePack.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.XR.OpenXR.Features; + +namespace RepoXR.Data; + +public class OpenXRFeaturePack : ScriptableObject +{ + [SerializeReference] private List features = []; + + public IReadOnlyList Features => features; +} \ No newline at end of file diff --git a/Source/Entrypoint.cs b/Source/Entrypoint.cs index 6ac78ef..cd99b22 100644 --- a/Source/Entrypoint.cs +++ b/Source/Entrypoint.cs @@ -4,10 +4,9 @@ using RepoXR.Managers; using RepoXR.Networking; using RepoXR.Patches; +using RepoXR.Player.Camera; using RepoXR.UI; using UnityEngine; -using UnityEngine.InputSystem; -using UnityEngine.InputSystem.XR; using UnityEngine.Rendering.PostProcessing; using UnityEngine.UI; @@ -16,18 +15,10 @@ namespace RepoXR; [RepoXRPatch] internal static class Entrypoint { - public static void OnSceneLoad(string _) - { - if (Plugin.Flags.HasFlag(Flags.VR)) - SetupDefaultSceneVR(); - - SetupDefaultSceneUniversal(); - } - /// /// The default setup for VR for every scene /// - private static void SetupDefaultSceneVR() + internal static void SetupDefaultSceneVR() { // We grab all these references manually as most of the instances aren't set yet // Since most of them run in the "Start" lifetime function @@ -47,10 +38,7 @@ private static void SetupDefaultSceneVR() var mainCamera = Camera.main!; // Add tracking to camera - var poseDriver = mainCamera.gameObject.AddComponent(); - poseDriver.positionAction = Actions.Instance.HeadPosition; - poseDriver.rotationAction = Actions.Instance.HeadRotation; - poseDriver.trackingStateInput = new InputActionProperty(Actions.Instance.HeadTrackingState); + mainCamera.gameObject.AddComponent(); // Parent overlay to main camera overlayCamera.transform.SetParent(mainCamera.transform, false); @@ -131,22 +119,6 @@ private static void SetupDefaultSceneVR() new GameObject("Data Manager").AddComponent(); } - private static bool hasShownErrorMessage; - - /// - /// The default setup for every scene (including for non-vr players) - /// - private static void SetupDefaultSceneUniversal() - { - new GameObject("RepoXR Network System").AddComponent(); - - ShowVRFailedWarning(); - -#if DEBUG - ShowEarlyAccessWarning(); -#endif - } - /// /// is always present in the `Main` scene, so we use it as a base entrypoint /// @@ -154,7 +126,7 @@ private static void SetupDefaultSceneUniversal() [HarmonyPostfix] private static void OnStartup(GameDirector __instance) { - VRInputSystem.instance.ActivateInput(); + VRInputSystem.Instance.ActivateInput(); if (RunManager.instance.levelCurrent == RunManager.instance.levelMainMenu || RunManager.instance.levelCurrent == RunManager.instance.levelSplashScreen) @@ -205,7 +177,49 @@ private static void OnStartupInGame() { GameDirector.instance.gameObject.AddComponent(); } +} +[RepoXRPatch(RepoXRPatchTarget.Universal)] +internal static class UniversalEntrypoint +{ + private static bool hasShownErrorMessage; + + public static void OnSceneLoad(string _) + { + if (Plugin.Flags.HasFlag(Flags.VR)) + Entrypoint.SetupDefaultSceneVR(); + + SetupDefaultSceneUniversal(); + } + + /// + /// Enable hotswapping while in the main menu + /// + [HarmonyPatch(typeof(GameDirector), nameof(GameDirector.Start))] + [HarmonyPostfix] + private static void OnStartup(GameDirector __instance) + { + if (RunManager.instance.levelCurrent != RunManager.instance.levelMainMenu && + RunManager.instance.levelCurrent != RunManager.instance.levelLobbyMenu) + return; + + new GameObject("VR Hotswapper").AddComponent(); + } + + /// + /// The default setup for every scene (including for non-vr players) + /// + private static void SetupDefaultSceneUniversal() + { + new GameObject("RepoXR Network System").AddComponent(); + + ShowVRFailedWarning(); + +#if DEBUG + ShowEarlyAccessWarning(); +#endif + } + private static void ShowVRFailedWarning() { if (!Plugin.Flags.HasFlag(Flags.StartupFailed) || diff --git a/Source/Experiments.cs b/Source/Experiments.cs index c230555..1018abb 100644 --- a/Source/Experiments.cs +++ b/Source/Experiments.cs @@ -1,50 +1,63 @@ -using HarmonyLib; +// ReSharper disable UnusedVariable + +using System.Collections.Generic; +using System.Reflection.Emit; +using HarmonyLib; +using UnityEngine; namespace RepoXR; #if DEBUG internal static class Experiments { - [HarmonyPatch(typeof(EnemyDirector), nameof(EnemyDirector.Awake))] + [HarmonyPatch(typeof(PlayerController), nameof(PlayerController.FixedUpdate))] [HarmonyPostfix] - private static void FuckLolEnemy(EnemyDirector __instance) + private static void InfiniteSprintPatch(PlayerController __instance) { - // Only allow eyeyeyeyeyeye spawning - var enemy = __instance.enemiesDifficulty1[0]; - - // Only allow mouth spawning - // var enemy = __instance.enemiesDifficulty1[4]; - - // Only allow thin-man spawning - // var enemy = __instance.enemiesDifficulty1[1]; - - // Only allow upSCREAM! spawning - // var enemy = __instance.enemiesDifficulty2[2]; + __instance.EnergyCurrent = __instance.EnergyStart; - // Only allow beamer spawning - // var enemy = __instance.enemiesDifficulty3[4]; + var script = PlayerController.instance?.playerAvatarScript; + if (script != null) script.upgradeTumbleClimb = 100; + } - __instance.enemiesDifficulty1.Clear(); - __instance.enemiesDifficulty2.Clear(); - __instance.enemiesDifficulty3.Clear(); + [HarmonyPatch(typeof(SpectateCamera), nameof(SpectateCamera.HeadEnergyLogic))] + [HarmonyTranspiler] + private static IEnumerable FastRechargeHead(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, new CodeMatch(OpCodes.Ldc_R4, 100f)) + .SetOperandAndAdvance(0.5f) + .InstructionEnumeration(); + } - __instance.enemiesDifficulty1.Add(enemy); - __instance.enemiesDifficulty2.Add(enemy); - __instance.enemiesDifficulty3.Add(enemy); + [HarmonyPatch(typeof(SemiFunc), nameof(SemiFunc.DebugTester))] + [HarmonyPostfix] + private static void IAmASurgeonIMeanTester(ref bool __result) + { + __result = true; } - [HarmonyPatch(typeof(PlayerController), nameof(PlayerController.FixedUpdate))] + [HarmonyPatch(typeof(SemiFunc), nameof(SemiFunc.DebugDev))] [HarmonyPostfix] - private static void InfiniteSprintPatch(PlayerController __instance) + private static void IAmASurgeonIMeanDeveloper(ref bool __result) { - __instance.EnergyCurrent = __instance.EnergyStart; + __result = true; } - [HarmonyPatch(typeof(PlayerHealth), nameof(PlayerHealth.Hurt))] - [HarmonyPrefix] - private static bool NoDamage() + [HarmonyPatch(typeof(DebugConsoleUI), nameof(DebugConsoleUI.Update))] + [HarmonyTranspiler] + private static IEnumerable KeepEnterKeyThing(IEnumerable instructions) { - return false; + return new CodeMatcher(instructions) + .MatchForward(false, new CodeMatch(OpCodes.Ldc_I4_S, (sbyte)9)) + .SetOperandAndAdvance((sbyte)KeyCode.Return) + .SetOperandAndAdvance(AccessTools.Method(typeof(UnityEngine.Input), nameof(UnityEngine.Input.GetKeyDown), + [typeof(KeyCode)])) + .MatchForward(false, new CodeMatch(OpCodes.Ldc_I4_S, (sbyte)13)) + .SetOperandAndAdvance((sbyte)KeyCode.Backspace) + .SetOperandAndAdvance(AccessTools.Method(typeof(UnityEngine.Input), nameof(UnityEngine.Input.GetKeyDown), + [typeof(KeyCode)])) + .InstructionEnumeration(); } } #endif \ No newline at end of file diff --git a/Source/Input/Actions.cs b/Source/Input/Actions.cs index e7fe355..b8089c7 100644 --- a/Source/Input/Actions.cs +++ b/Source/Input/Actions.cs @@ -18,6 +18,9 @@ public class Actions public InputAction RightHandPosition { get; private set; } public InputAction RightHandRotation { get; private set; } public InputAction RightHandTrackingState { get; private set; } + + public InputAction EyeGazePosition { get; private set; } + public InputAction EyeGazeRotation { get; private set; } private Actions() { @@ -32,9 +35,12 @@ private Actions() RightHandPosition = AssetCollection.DefaultXRActions.FindAction("Right/Position"); RightHandRotation = AssetCollection.DefaultXRActions.FindAction("Right/Rotation"); RightHandTrackingState = AssetCollection.DefaultXRActions.FindAction("Right/Tracking State"); - + + EyeGazePosition = AssetCollection.DefaultXRActions.FindAction("Eye Gaze/Position"); + EyeGazeRotation = AssetCollection.DefaultXRActions.FindAction("Eye Gaze/Rotation"); + AssetCollection.DefaultXRActions.Enable(); } - public InputAction this[string name] => VRInputSystem.instance.Actions[name]; + public InputAction this[string name] => VRInputSystem.Instance.Actions[name]; } \ No newline at end of file diff --git a/Source/Input/RebindManager.cs b/Source/Input/RebindManager.cs index 41df47c..7cb3110 100644 --- a/Source/Input/RebindManager.cs +++ b/Source/Input/RebindManager.cs @@ -52,7 +52,7 @@ public class RebindManager : MonoBehaviour private void Awake() { Instance = this; - playerInput = VRInputSystem.instance.GetPlayerInput(); + playerInput = VRInputSystem.Instance.GetPlayerInput(); DestroyOldUI(); CreateUI(); @@ -73,8 +73,7 @@ private void OnDestroy() private void OnControlsChanged(PlayerInput input) { Logger.LogDebug($"New control scheme: {input.currentControlScheme}"); - // TODO: Change some text somewhere - + ReloadBindings(); } diff --git a/Source/Input/TrackingInput.cs b/Source/Input/TrackingInput.cs index ad03013..61d8c39 100644 --- a/Source/Input/TrackingInput.cs +++ b/Source/Input/TrackingInput.cs @@ -9,7 +9,8 @@ namespace RepoXR.Input; /// public class TrackingInput : MonoBehaviour { - public static TrackingInput instance; + public static TrackingInput Instance => _instance ?? InputManager.instance.gameObject.AddComponent(); + private static TrackingInput? _instance; public Transform HeadTransform { get; private set; } public Transform LeftHandTransform { get; private set; } @@ -17,13 +18,13 @@ public class TrackingInput : MonoBehaviour private void Awake() { - if (instance != null) + if (_instance != null) { Destroy(gameObject); return; } - instance = this; + _instance = this; DontDestroyOnLoad(gameObject); CreateTrackingOrigins(); diff --git a/Source/Input/VRInputSystem.cs b/Source/Input/VRInputSystem.cs index 8d723bf..bbd12a6 100644 --- a/Source/Input/VRInputSystem.cs +++ b/Source/Input/VRInputSystem.cs @@ -10,7 +10,8 @@ namespace RepoXR.Input; public class VRInputSystem : MonoBehaviour { - public static VRInputSystem instance; + public static VRInputSystem Instance => _instance ?? InputManager.instance.gameObject.AddComponent(); + private static VRInputSystem? _instance; private PlayerInput playerInput; @@ -21,7 +22,7 @@ public class VRInputSystem : MonoBehaviour private void Awake() { - instance = this; + _instance = this; playerInput = gameObject.AddComponent(); playerInput.actions = AssetCollection.VRInputs; diff --git a/Source/Managers/HapticManager.cs b/Source/Managers/HapticManager.cs index 05e628a..ba1187a 100644 --- a/Source/Managers/HapticManager.cs +++ b/Source/Managers/HapticManager.cs @@ -33,6 +33,13 @@ private void Awake() private void Update() { + // Destroy object if we toggled out of VR + if (SemiFunc.FPSImpulse1() && !VRSession.InVR) + { + Destroy(gameObject); + return; + } + if (priorityTimer > 0) { priorityTimer -= Time.deltaTime; diff --git a/Source/Managers/HotswapManager.cs b/Source/Managers/HotswapManager.cs new file mode 100644 index 0000000..282b0d8 --- /dev/null +++ b/Source/Managers/HotswapManager.cs @@ -0,0 +1,68 @@ +using Photon.Pun; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.SceneManagement; + +namespace RepoXR.Managers; + +public class HotswapManager : MonoBehaviour +{ + private readonly InputAction swapAction = new(binding: "/F8"); + + private void Awake() + { + swapAction.performed += SwapActionPerformed; + swapAction.Enable(); + } + + private void OnDestroy() + { + swapAction.performed -= SwapActionPerformed; + } + + private static void SwapActionPerformed(InputAction.CallbackContext context) + { + if (!context.performed) + return; + + if (VRSession.InVR) + HotswapDisableVR(); + else + HotswapEnableVR(); + } + + private static void HotswapDisableVR() + { + Plugin.ToggleVR(); + + RestartScene(); + } + + private static void HotswapEnableVR() + { + Plugin.ToggleVR(); + + if (VRSession.InVR) + RestartScene(); + else + { + // Close existing popup if one is open + if (MenuPagePopUp.instance != null) + MenuPagePopUp.instance.ButtonEvent(); + + MenuManager.instance.PagePopUp("VR Startup Failed", Color.red, + "RepoXR tried to swap the game to VR, however an error occured during initialization.\n\nYou can update your settings and press F8 to try again.", + "Darn it", + true); + } + } + + private static void RestartScene() + { + if (SemiFunc.IsMultiplayer() && !PhotonNetwork.IsMasterClient) + // RestartScene is not allowed when not the host, so we just re-join the lobby + SceneManager.LoadSceneAsync("LobbyJoin"); + else + RunManager.instance.RestartScene(); + } +} \ No newline at end of file diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs index ee0f7ae..da16a35 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -1,10 +1,8 @@ -using RepoXR.Input; +using RepoXR.Assets; using RepoXR.Player; using RepoXR.UI; using UnityEngine; -using UnityEngine.InputSystem; using UnityEngine.InputSystem.UI; -using UnityEngine.InputSystem.XR; namespace RepoXR.Managers; @@ -25,6 +23,7 @@ public class VRSession : MonoBehaviour public Camera MainCamera { get; private set; } public VRPlayer Player { get; private set; } public GameHud HUD { get; private set; } + public FocusSphere FocusSphere { get; private set; } private void Awake() { @@ -49,12 +48,6 @@ private void InitializeVRSession() MainCamera = CameraUtils.Instance.MainCamera; MainCamera.targetTexture = null; MainCamera.depth = 0; - - // Setup camera tracking - var cameraPoseDriver = MainCamera.gameObject.AddComponent(); - cameraPoseDriver.positionAction = Actions.Instance.HeadPosition; - cameraPoseDriver.rotationAction = Actions.Instance.HeadRotation; - cameraPoseDriver.trackingStateInput = new InputActionProperty(Actions.Instance.HeadTrackingState); // Setup "on top" camera var topCamera = MainCamera.transform.Find("Camera Top").GetComponent(); @@ -69,5 +62,8 @@ private void InitializeVRSession() // Initialize Handheld Map (if it wasn't created yet) VRMapTool.Create(); + + // Initialize Focus Sphere + FocusSphere = Instantiate(AssetCollection.FocusSphere, MainCamera.transform.parent).GetComponent(); } } \ No newline at end of file diff --git a/Source/Networking/Frames/EyeGaze.cs b/Source/Networking/Frames/EyeGaze.cs new file mode 100644 index 0000000..cfa02da --- /dev/null +++ b/Source/Networking/Frames/EyeGaze.cs @@ -0,0 +1,20 @@ +using Photon.Pun; +using UnityEngine; + +namespace RepoXR.Networking.Frames; + +[Frame(FrameHelper.FrameEyeGaze)] +public class EyeGaze : IFrame +{ + public Vector3 GazePoint; + + public void Serialize(PhotonStream stream) + { + stream.SendNext(GazePoint); + } + + public void Deserialize(PhotonStream stream) + { + GazePoint = (Vector3)stream.ReceiveNext(); + } +} \ No newline at end of file diff --git a/Source/Networking/Frames/Frame.cs b/Source/Networking/Frames/Frame.cs index 3db28f0..f1f56b1 100644 --- a/Source/Networking/Frames/Frame.cs +++ b/Source/Networking/Frames/Frame.cs @@ -14,6 +14,7 @@ public static class FrameHelper public const int FrameMaptool = 3; public const int FrameHeadlamp = 4; public const int FrameDominantHand = 5; + public const int FrameEyeGaze = 6; private static Dictionary cachedTypes = []; diff --git a/Source/Networking/NetworkPlayer.cs b/Source/Networking/NetworkPlayer.cs index eb9d8ea..7e3331c 100644 --- a/Source/Networking/NetworkPlayer.cs +++ b/Source/Networking/NetworkPlayer.cs @@ -37,6 +37,10 @@ public class NetworkPlayer : MonoBehaviour private bool isLeftHanded; private bool isMapLeftHanded; private bool isHeadlampEnabled; + + // Eye tracking + public bool EyeTracking { get; private set; } + public Vector3 EyeGazePoint { get; private set; } private void Start() { @@ -206,4 +210,17 @@ public void UpdateDominantHand(bool leftHanded) playerRightArm.physGrabBeam.PhysGrabPointOrigin.SetParent(isLeftHanded ? leftHandAnchor : rightHandAnchor); playerRightArm.physGrabBeam.PhysGrabPointOrigin.localPosition = Vector3.zero; } + + public void UpdateEyeTracking(Vector3 gazePoint) + { + // (0, -1000, 0) is sent whenever eye tracking is disabled (or stopped working) during a session + if (gazePoint == Vector3.down * 1000) + { + EyeTracking = false; + return; + } + + EyeTracking = true; + EyeGazePoint = gazePoint; + } } \ No newline at end of file diff --git a/Source/Networking/NetworkSystem.cs b/Source/Networking/NetworkSystem.cs index 096e1c2..7056f68 100644 --- a/Source/Networking/NetworkSystem.cs +++ b/Source/Networking/NetworkSystem.cs @@ -92,6 +92,19 @@ public void UpdateDominantHand(bool leftHanded) }); } + public void UpdateEyeTracking(Vector3 gazePoint) + { + EnqueueFrame(new EyeGaze + { + GazePoint = gazePoint + }); + } + + public void DisableEyeTracking() + { + UpdateEyeTracking(Vector3.down * 1000); + } + /// /// Enqueues a frame to be sent next serialization sequence. This function contains an optimization that removes /// duplicate frames to reduce network usage, which reduces server costs. @@ -170,6 +183,17 @@ private void HandleFrame(PlayerAvatar player, IFrame frame) break; } + + case FrameHelper.FrameEyeGaze: + { + var eyeGazeFrame = (EyeGaze)frame; + if (!networkPlayers.TryGetValue(player.photonView.controllerActorNr, out var networkPlayer)) + return; + + networkPlayer.UpdateEyeTracking(eyeGazeFrame.GazePoint); + + break; + } } } catch (Exception ex) @@ -198,6 +222,10 @@ internal void OnPlayerLeave(int actorNumber) internal void WriteAdditionalData(PhotonStream stream) { + // Just don't send anything if we have nothing to say + if (scheduledFrames.Count == 0) + return; + stream.SendNext(REPOXR_MAGIC); stream.SendNext(PROTOCOL_VERSION); @@ -244,23 +272,23 @@ internal void ReadAdditionalData(PlayerAvatar playerAvatar, PhotonStream stream) [RepoXRPatch(RepoXRPatchTarget.Universal)] internal static class NetworkingPatches { - // The reason that this code is injected on PhysGrabber is that it's the last observed component on the - // player avatar controller's PhotonView, meaning no additional data is available on the PhotonStream. - // If we started injecting data too early, it would cause vanilla clients to no longer be able - // to understand our photon data, and that breaks multiplayer. + // The reason that this code is injected on PlayerLocalCamera is that it's still enabled + // even after the player dies. Previous versions of this code would make the NetworkSystem + // stop functioning after the player died, which was fine before, but now the game + // has features that VR needs some special interactions with even after death /// /// Inject additional code when serializing/deserializing a network component /// - [HarmonyPatch(typeof(PhysGrabber), nameof(PhysGrabber.OnPhotonSerializeView))] + [HarmonyPatch(typeof(PlayerLocalCamera), nameof(PlayerLocalCamera.OnPhotonSerializeView))] [HarmonyPostfix] - private static void OnAfterSerializeView(PhysGrabber __instance, PhotonStream stream) + private static void OnAfterSerializeView(PlayerLocalCamera __instance, PhotonStream stream) { if (stream.IsWriting) NetworkSystem.instance.WriteAdditionalData(stream); else NetworkSystem.instance.ReadAdditionalData( - __instance.playerAvatar ?? __instance.GetComponent(), stream); + __instance.transform.parent.GetComponentInChildren(true), stream); } [HarmonyPatch(typeof(NetworkManager), nameof(NetworkManager.OnPlayerLeftRoom))] diff --git a/Source/OpenXR.cs b/Source/OpenXR.cs index 9acc39d..5c909a4 100644 --- a/Source/OpenXR.cs +++ b/Source/OpenXR.cs @@ -8,12 +8,12 @@ using BepInEx.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using RepoXR.Assets; using Steamworks; using UnityEngine; using UnityEngine.XR; using UnityEngine.XR.Management; using UnityEngine.XR.OpenXR; -using UnityEngine.XR.OpenXR.Features.Interactions; namespace RepoXR; @@ -371,6 +371,12 @@ public static bool InitializeXR() return false; } + public static void DeinitializeXR() + { + xrManagerSettings?.DeinitializeLoader(); + xrGeneralSettings?.StopXRSDK(); + } + private static bool InitializeXR(Runtime? runtime) { if (xrManagerSettings == null || xrGeneralSettings == null || xrLoader == null) @@ -421,36 +427,7 @@ private static void InitializeScripts() OpenXRSettings.Instance.renderMode = OpenXRSettings.RenderMode.MultiPass; OpenXRSettings.Instance.depthSubmissionMode = OpenXRSettings.DepthSubmissionMode.None; - - if (OpenXRSettings.Instance.features.Length != 0) - return; - - var valveIndex = ScriptableObject.CreateInstance(); - var hpReverb = ScriptableObject.CreateInstance(); - var htcVive = ScriptableObject.CreateInstance(); - var mmController = ScriptableObject.CreateInstance(); - var khrSimple = ScriptableObject.CreateInstance(); - var metaQuestTouch = ScriptableObject.CreateInstance(); - var oculusTouch = ScriptableObject.CreateInstance(); - - valveIndex.enabled = true; - hpReverb.enabled = true; - htcVive.enabled = true; - mmController.enabled = true; - khrSimple.enabled = true; - metaQuestTouch.enabled = true; - oculusTouch.enabled = true; - - OpenXRSettings.Instance.features = - [ - valveIndex, - hpReverb, - htcVive, - mmController, - khrSimple, - metaQuestTouch, - oculusTouch - ]; + OpenXRSettings.Instance.features = AssetCollection.OpenXRFeatures.Features.ToArray(); } } } \ No newline at end of file diff --git a/Source/Patches/CameraPatches.cs b/Source/Patches/CameraPatches.cs index 53ede4f..53af970 100644 --- a/Source/Patches/CameraPatches.cs +++ b/Source/Patches/CameraPatches.cs @@ -1,4 +1,6 @@ using HarmonyLib; +using Photon.Pun; +using RepoXR.Managers; using UnityEngine; namespace RepoXR.Patches; @@ -13,6 +15,10 @@ internal static class CameraPatches [HarmonyPrefix] private static void DisableTargetTextureOverride(Camera __instance, ref RenderTexture? value) { + // We make an exception for our manually rendered custom camera + if (__instance.name.StartsWith("Custom Camera")) + return; + value = null; } @@ -76,4 +82,42 @@ private static bool OnScreenVR(Vector3 position, float padWidth, float padHeight return screenPoint.x > -padWidth && screenPoint.x < 1 + padWidth && screenPoint.y > -padHeight && screenPoint.y < 1 + padHeight; } + + /// + /// Assign the transform the same values as our VR camera + /// + [HarmonyPatch(typeof(PlayerLocalCamera), nameof(PlayerLocalCamera.Update))] + [HarmonyPostfix] + private static void AlignWithVRCameraPatch(PlayerLocalCamera __instance) + { + if (SemiFunc.IsMultiplayer() && !__instance.photonView.IsMine) + return; + + if (VRSession.Instance is not { } session) + return; + + __instance.transform.position = session.MainCamera.transform.position; + __instance.transform.rotation = session.MainCamera.transform.rotation; + } + + /// + /// Make sure to synchronize the VR camera transforms instead of only the aim transforms + /// + [HarmonyPatch(typeof(PlayerLocalCamera), nameof(PlayerLocalCamera.OnPhotonSerializeView))] + [HarmonyPrefix] + private static bool CameraSerializeVRParams(PlayerLocalCamera __instance, PhotonStream stream, + PhotonMessageInfo info) + { + if (!SemiFunc.MasterAndOwnerOnlyRPC(info, __instance.photonView)) + return false; + + if (!stream.IsWriting) + return true; + + stream.SendNext(__instance.transform.position); + stream.SendNext(__instance.transform.rotation); + stream.SendNext(__instance.teleported); + + return false; + } } \ No newline at end of file diff --git a/Source/Patches/Enemy/EnemyCeilingEyePatches.cs b/Source/Patches/Enemy/EnemyCeilingEyePatches.cs index ada6d28..c3e118c 100644 --- a/Source/Patches/Enemy/EnemyCeilingEyePatches.cs +++ b/Source/Patches/Enemy/EnemyCeilingEyePatches.cs @@ -5,6 +5,7 @@ using RepoXR.Assets; using RepoXR.Managers; using RepoXR.Player.Camera; +using RepoXR.UI; using static HarmonyLib.AccessTools; namespace RepoXR.Patches.Enemy; @@ -32,29 +33,27 @@ private static IEnumerable SetCameraSoftRotationPatch(IEnumerab .InsertAndAdvance(new CodeInstruction(OpCodes.Callvirt, PropertyGetter(typeof(ConfigEntry), nameof(ConfigEntry.Value)))) .SetOperandAndAdvance(Method(typeof(VRCameraAim), nameof(VRCameraAim.SetAimTargetSoft))) - .InstructionEnumeration(); - } + // Set focus sphere target to look at the ceiling eye + .Insert( + // VRSession.Instance.FocusSphere + new CodeInstruction(OpCodes.Call, PropertyGetter(typeof(VRSession), nameof(VRSession.Instance))), + new CodeInstruction(OpCodes.Callvirt, PropertyGetter(typeof(VRSession), nameof(VRSession.FocusSphere))), - /// - /// Replace with - /// - [HarmonyPatch(typeof(EnemyCeilingEye), nameof(EnemyCeilingEye.UpdateStateRPC))] - [HarmonyTranspiler] - private static IEnumerable SetCameraRotationPatch(IEnumerable instructions) - { - return new CodeMatcher(instructions) - .MatchForward(false, - new CodeMatch(OpCodes.Callvirt, Method(typeof(CameraAim), nameof(CameraAim.AimTargetSet)))) - .Advance(-10) - .SetOperandAndAdvance(Field(typeof(VRCameraAim), nameof(VRCameraAim.instance))) - .Advance(9) - // Make the rotation less severe if reduced aim impact is enabled - .InsertAndAdvance(new CodeInstruction(OpCodes.Call, Plugin.GetConfigGetter())) - .InsertAndAdvance(new CodeInstruction(OpCodes.Callvirt, - PropertyGetter(typeof(Config), nameof(Config.ReducedAimImpact)))) - .InsertAndAdvance(new CodeInstruction(OpCodes.Callvirt, - PropertyGetter(typeof(ConfigEntry), nameof(ConfigEntry.Value)))) - .SetOperandAndAdvance(Method(typeof(VRCameraAim), nameof(VRCameraAim.SetAimTarget))) + // target: this.enemy.CenterTransform + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Ldfld, Field(typeof(EnemyCeilingEye), nameof(EnemyCeilingEye.enemy))), + new CodeInstruction(OpCodes.Ldfld, Field(typeof(global::Enemy), nameof(global::Enemy.CenterTransform))), + + // time: 1f + new CodeInstruction(OpCodes.Ldc_R4, 0.5f), + + // speed: 2f + new CodeInstruction(OpCodes.Ldc_R4, 2f), + + // strength: 1f (100%) + new CodeInstruction(OpCodes.Ldc_R4, 1f), + new CodeInstruction(OpCodes.Callvirt, Method(typeof(FocusSphere), nameof(FocusSphere.SetLookAtTarget))) + ) .InstructionEnumeration(); } @@ -67,7 +66,7 @@ private static void EyeAttachHapticFeedback(EnemyCeilingEye __instance) { if (__instance.currentState != EnemyCeilingEye.State.HasTarget) return; - + if (!__instance.targetPlayer || !__instance.targetPlayer.isLocal) return; diff --git a/Source/Patches/Enemy/EnemyHeartHuggerPatches.cs b/Source/Patches/Enemy/EnemyHeartHuggerPatches.cs new file mode 100644 index 0000000..44d391b --- /dev/null +++ b/Source/Patches/Enemy/EnemyHeartHuggerPatches.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Reflection.Emit; +using BepInEx.Configuration; +using HarmonyLib; +using RepoXR.Player.Camera; + +using static HarmonyLib.AccessTools; + +namespace RepoXR.Patches.Enemy; + +[RepoXRPatch] +internal static class EnemyHeartHuggerPatches +{ + /// + /// Force the VR camera to look at the heart hugger + /// + [HarmonyPatch(typeof(EnemyHeartHugger), nameof(EnemyHeartHugger.JumpScareAtChompStartForceLookAtHead))] + [HarmonyTranspiler] + private static IEnumerable LookAtPatch(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, new CodeMatch(OpCodes.Ldsfld, Field(typeof(CameraAim), nameof(CameraAim.Instance)))) + .SetOperandAndAdvance(Field(typeof(VRCameraAim), nameof(VRCameraAim.instance))) + .MatchForward(false, + new CodeMatch(OpCodes.Callvirt, Method(typeof(CameraAim), nameof(CameraAim.AimTargetSoftSet)))) + .SetAndAdvance(OpCodes.Call, Plugin.GetConfigGetter()) + .InsertAndAdvance( + new CodeInstruction(OpCodes.Callvirt, PropertyGetter(typeof(Config), nameof(Config.ReducedAimImpact))), + new CodeInstruction(OpCodes.Callvirt, + PropertyGetter(typeof(ConfigEntry), nameof(ConfigEntry.Value))), + new CodeInstruction(OpCodes.Callvirt, Method(typeof(VRCameraAim), nameof(VRCameraAim.SetAimTargetSoft))) + ) + .InstructionEnumeration(); + } +} \ No newline at end of file diff --git a/Source/Patches/Enemy/EnemyOnScreenPatches.cs b/Source/Patches/Enemy/EnemyOnScreenPatches.cs new file mode 100644 index 0000000..7d7bbf5 --- /dev/null +++ b/Source/Patches/Enemy/EnemyOnScreenPatches.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Reflection.Emit; +using HarmonyLib; +using RepoXR.Player; + +using static HarmonyLib.AccessTools; + +namespace RepoXR.Patches.Enemy; + +[RepoXRPatch] +internal static class EnemyOnScreenPatches +{ + /// + /// Replace the "on screen" detection in enemies with custom detection that is better suited for VR and supports + /// eye tracking + /// + [HarmonyPatch(typeof(EnemyOnScreen), nameof(EnemyOnScreen.Logic), MethodType.Enumerator)] + [HarmonyTranspiler] + private static IEnumerable LogicPatch(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, new CodeMatch(OpCodes.Call, Method(typeof(SemiFunc), nameof(SemiFunc.OnScreen)))) + .SetOperandAndAdvance(Method(typeof(VREyeTracking), nameof(VREyeTracking.LookingAt))) + .InstructionEnumeration(); + } +} \ No newline at end of file diff --git a/Source/Patches/Enemy/EnemyOoglyPatches.cs b/Source/Patches/Enemy/EnemyOoglyPatches.cs new file mode 100644 index 0000000..e1758b9 --- /dev/null +++ b/Source/Patches/Enemy/EnemyOoglyPatches.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Reflection.Emit; +using BepInEx.Configuration; +using HarmonyLib; +using RepoXR.Player.Camera; + +using static HarmonyLib.AccessTools; + +namespace RepoXR.Patches.Enemy; + +[RepoXRPatch] +internal static class EnemyOoglyPatches +{ + /// + /// Oh you don't wanna know... oh the horror (look at oogly while being attached) + /// + [HarmonyPatch(typeof(EnemyOogly), nameof(EnemyOogly.UpdateEvilEyesTimer))] + [HarmonyTranspiler] + private static IEnumerable LookAtPatch(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, new CodeMatch(OpCodes.Ldsfld, Field(typeof(CameraAim), nameof(CameraAim.Instance)))) + .SetOperandAndAdvance(Field(typeof(VRCameraAim), nameof(VRCameraAim.instance))) + .MatchForward(false, + new CodeMatch(OpCodes.Callvirt, Method(typeof(CameraAim), nameof(CameraAim.AimTargetSet)))) + .SetAndAdvance(OpCodes.Call, Plugin.GetConfigGetter()) + .InsertAndAdvance( + new CodeInstruction(OpCodes.Callvirt, PropertyGetter(typeof(Config), nameof(Config.ReducedAimImpact))), + new CodeInstruction(OpCodes.Callvirt, + PropertyGetter(typeof(ConfigEntry), nameof(ConfigEntry.Value))), + new CodeInstruction(OpCodes.Callvirt, Method(typeof(VRCameraAim), nameof(VRCameraAim.SetAimTarget))) + ) + .InstructionEnumeration(); + } +} \ No newline at end of file diff --git a/Source/Patches/Enemy/EnemySpinnyPatches.cs b/Source/Patches/Enemy/EnemySpinnyPatches.cs new file mode 100644 index 0000000..0b7dc03 --- /dev/null +++ b/Source/Patches/Enemy/EnemySpinnyPatches.cs @@ -0,0 +1,22 @@ +using HarmonyLib; +using RepoXR.Player.Camera; + +namespace RepoXR.Patches.Enemy; + +[RepoXRPatch] +internal static class EnemySpinnyPatches +{ + /// + /// Make sure to always look at the little gambling machine + /// + [HarmonyPatch(typeof(EnemySpinny), nameof(EnemySpinny.OverrideTargetPlayerCameraAim))] + [HarmonyPrefix] + private static bool OverrideVRCameraAim(EnemySpinny __instance, float _strenght, float _strenghtNoAim) + { + // Always low impact, there's not really a need to force up-down look here + VRCameraAim.instance.SetAimTargetSoft(__instance.spinnyWheel.position, 0.1f, _strenght, _strenghtNoAim, + __instance.gameObject, 100, true); + + return false; + } +} \ No newline at end of file diff --git a/Source/Patches/HarmonyPatcher.cs b/Source/Patches/HarmonyPatcher.cs index 8b4d948..51005c0 100644 --- a/Source/Patches/HarmonyPatcher.cs +++ b/Source/Patches/HarmonyPatcher.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using HarmonyLib; using JetBrains.Annotations; @@ -27,6 +28,11 @@ public static void PatchVR() Patch(VRPatcher, RepoXRPatchTarget.VROnly); } + public static void UnpatchVR() + { + VRPatcher.UnpatchSelf(); + } + public static void PatchClass(Type type) { UniversalPatcher.CreateClassProcessor(type, true).Patch(); @@ -76,7 +82,9 @@ internal enum RepoXRPatchTarget } /// -/// Fixes a bug in older BepInEx versions (shame on you TS for using a 2-year-old BepInEx) +/// To keep RepoXR compatible with older BepInEx versions, this patch will not be removed +/// +/// Fixes a bug in older BepInEx versions /// /// https://github.com/BepInEx/HarmonyX/blob/master/Harmony/Internal/Patching/ILManipulator.cs#L322 /// Licensed under MIT: https://github.com/BepInEx/HarmonyX/blob/master/LICENSE @@ -198,4 +206,19 @@ private static void Emit(this CecilILGenerator il, SRE.OpCode opcode, object ope method.Invoke(null, [il, opcode, operand]); } +} + +[RepoXRPatch(RepoXRPatchTarget.Universal)] +internal static class HarmonyLibPatches +{ + /// + /// Ironically, patching Harmony like this fixes some issues with *un*patching + /// + [HarmonyPatch(typeof(MethodBaseExtensions), nameof(MethodBaseExtensions.HasMethodBody))] + [HarmonyPostfix] + private static void OnUnpatch(MethodBase member, ref bool __result) + { + if (new StackTrace().GetFrame(2)?.GetMethod().Name == "UnpatchConditional") + __result = true; + } } \ No newline at end of file diff --git a/Source/Patches/InputPatches.cs b/Source/Patches/InputPatches.cs index 811bf26..64f0e01 100644 --- a/Source/Patches/InputPatches.cs +++ b/Source/Patches/InputPatches.cs @@ -23,20 +23,23 @@ private static void AllowBackgroundTracking(ref InputSettings.BackgroundBehavior value = InputSettings.BackgroundBehavior.IgnoreFocus; } + /// + /// Add additional mapping tags during startup + /// [HarmonyPatch(typeof(InputManager), nameof(InputManager.Start))] [HarmonyPostfix] private static void OnInputManagerStart(InputManager __instance) { var offset = Enum.GetNames(typeof(InputKey)).Length; - + for (var i = 0; i < AssetCollection.RemappableControls.additionalBindings.Length; i++) { var binding = AssetCollection.RemappableControls.additionalBindings[i]; - + __instance.tagDictionary.Add($"[{binding.action.name}]", (InputKey)(i + offset)); } } - + /// /// Create a custom component on the , allowing the use of s /// @@ -44,8 +47,6 @@ private static void OnInputManagerStart(InputManager __instance) [HarmonyPostfix] private static void OnInitializeInputManager(InputManager __instance) { - __instance.gameObject.AddComponent(); - new GameObject("VR Tracking Input").AddComponent(); } @@ -54,12 +55,20 @@ private static void OnInitializeInputManager(InputManager __instance) private static bool GetAction(ref InputKey key, ref InputAction __result) { var bindings = Enum.GetNames(typeof(InputKey)).Length; - - __result = (int)key >= bindings - ? AssetCollection.RemappableControls.additionalBindings[(int)key - bindings] - : Actions.Instance[key.ToString()]; - return false; + try + { + __result = (int)key >= bindings + ? AssetCollection.RemappableControls.additionalBindings[(int)key - bindings] + : Actions.Instance[key.ToString()]; + + return false; + } + catch + { + // If no key was found, fall back to vanilla keybind (likely won't work with VR controllers though) + return true; + } } [HarmonyPatch(typeof(InputManager), nameof(InputManager.GetMovement))] @@ -70,7 +79,7 @@ private static bool GetMovement(InputManager __instance, ref Vector2 __result) return true; __result = Actions.Instance["Movement"].ReadValue(); - + return false; } @@ -152,7 +161,7 @@ private static bool KeyUp(InputManager __instance, ref InputKey key, ref bool __ return true; __result = __instance.GetAction(key).WasReleasedThisFrame(); - + return false; } @@ -181,11 +190,8 @@ private static bool KeyPullAndPush(ref float __result) var pull = Actions.Instance["Pull"].ReadValue(); if (pull > 0) - { __result = -pull; - return false; - } - + return false; } @@ -200,14 +206,14 @@ private static bool InputDisplayGet(InputManager __instance, InputKey _inputKey, if (action == null) { __result = "Unassigned"; - + return false; } - var index = action.GetBindingIndex(VRInputSystem.instance.CurrentControlScheme); + var index = action.GetBindingIndex(VRInputSystem.Instance.CurrentControlScheme); __result = __instance.InputDisplayGetString(action, index); - + return false; } @@ -220,7 +226,7 @@ private static bool InputDisplayGetString(InputAction action, int bindingIndex, { var binding = action.bindings[bindingIndex].effectivePath; __result = Utils.GetControlSpriteString(binding); - + return false; } @@ -231,7 +237,7 @@ private static bool InputDisplayGetString(InputAction action, int bindingIndex, [HarmonyPrefix] private static bool InputToggleGet(ref InputKey key, ref bool __result) { - __result = VRInputSystem.instance.InputToggleGet(key.ToString()); + __result = VRInputSystem.Instance.InputToggleGet(key.ToString()); return false; } @@ -260,7 +266,7 @@ private static bool NoUnderlinePatch(InputManager __instance, ref string __resul private static bool ResetVRControls() { RebindManager.Instance.ResetControls(); - + return false; } diff --git a/Source/Patches/Item/ItemBoomboxPatches.cs b/Source/Patches/Item/ItemBoomboxPatches.cs index 2d5dab3..4c7da8b 100644 --- a/Source/Patches/Item/ItemBoomboxPatches.cs +++ b/Source/Patches/Item/ItemBoomboxPatches.cs @@ -21,8 +21,8 @@ private static void BoomboxAimPatch(ValuableBoombox __instance) var bopSpeed = Plugin.Config.ReducedAimImpact.Value ? 5 : 15; var bopMultiplier = Plugin.Config.ReducedAimImpact.Value ? 0.15f : 0.5f; - var cameraPosition = PhysGrabber.instance.playerAvatar.localCameraPosition; - var cameraForward = PhysGrabber.instance.playerAvatar.localCameraTransform.forward * 2; + var cameraPosition = PhysGrabber.instance.playerAvatar.localCamera.transform.position; + var cameraForward = PhysGrabber.instance.playerAvatar.localCamera.transform.forward * 2; var upOffset = Vector3.up * Mathf.Sin(Time.time * bopSpeed) * bopMultiplier; var lookAtPosition = cameraPosition + cameraForward + upOffset; diff --git a/Source/Patches/PhysGrabObjectPatches.cs b/Source/Patches/PhysGrabObjectPatches.cs index c21d6e5..ec11dee 100644 --- a/Source/Patches/PhysGrabObjectPatches.cs +++ b/Source/Patches/PhysGrabObjectPatches.cs @@ -16,31 +16,35 @@ internal static class PhysGrabObjectPatches private static Transform GetTargetTransform(PlayerAvatar player) { if (player.isLocal) - return VRSession.Instance is { } session ? session.Player.MainHand : player.localCameraTransform; + return VRSession.Instance is { } session ? session.Player.MainHand : player.localCamera.transform; return NetworkSystem.instance.GetNetworkPlayer(player, out var networkPlayer) ? networkPlayer.PrimaryHand - : player.localCameraTransform; + : player.localCamera.transform; } private static Quaternion GetTargetRotation(PlayerAvatar player) { if (player.isLocal) - return VRSession.Instance is { } session ? session.Player.MainHand.rotation : player.localCameraRotation; + return VRSession.Instance is { } session + ? session.Player.MainHand.rotation + : player.localCamera.transform.rotation; return NetworkSystem.instance.GetNetworkPlayer(player, out var networkPlayer) ? networkPlayer.PrimaryHand.rotation - : player.localCameraRotation; + : player.localCamera.transform.rotation; } private static Vector3 GetTargetPosition(PlayerAvatar player) { if (player.isLocal) - return VRSession.Instance is { } session ? session.Player.MainHand.position : player.localCameraPosition; + return VRSession.Instance is { } session + ? session.Player.MainHand.position + : player.localCamera.transform.position; return NetworkSystem.instance.GetNetworkPlayer(player, out var networkPlayer) ? networkPlayer.PrimaryHand.position - : player.localCameraPosition; + : player.localCamera.transform.position; } private static Transform GetCartSteerTransform(PhysGrabber grabber) @@ -62,7 +66,7 @@ private static IEnumerable HandRelativeMovementPatch(IEnumerabl { return new CodeMatcher(instructions) .MatchForward(false, - new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraTransform)))) + new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera)))) .Repeat(matcher => matcher.SetInstruction(new CodeInstruction(OpCodes.Call, ((Func)GetTargetTransform).Method))) @@ -106,14 +110,17 @@ private static IEnumerable HandRelativeCartCannonPatch(IEnumera { return new CodeMatcher(instructions) .MatchForward(false, - new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraRotation)))) - .Set(OpCodes.Call, ((Func)GetTargetRotation).Method) + new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera)))) + .SetAndAdvance(OpCodes.Call, ((Func)GetTargetRotation).Method) + .RemoveInstructions(2) .MatchForward(false, - new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraRotation)))) - .Set(OpCodes.Call, ((Func)GetTargetRotation).Method) + new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera)))) + .SetAndAdvance(OpCodes.Call, ((Func)GetTargetRotation).Method) + .RemoveInstructions(2) .MatchForward(false, - new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraPosition)))) - .Set(OpCodes.Call, ((Func)GetTargetPosition).Method) + new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera)))) + .SetAndAdvance(OpCodes.Call, ((Func)GetTargetPosition).Method) + .RemoveInstructions(2) .InstructionEnumeration(); } @@ -126,8 +133,9 @@ private static IEnumerable RotationTargetHandRelative(IEnumerab { return new CodeMatcher(instructions) .MatchForward(false, - new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraRotation)))) - .Set(OpCodes.Call, ((Func)GetTargetRotation).Method) + new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera)))) + .SetAndAdvance(OpCodes.Call, ((Func)GetTargetRotation).Method) + .RemoveInstructions(2) .InstructionEnumeration(); } } \ No newline at end of file diff --git a/Source/Patches/PhysGrabberPatches.cs b/Source/Patches/PhysGrabberPatches.cs index 12a19ba..a1372b9 100644 --- a/Source/Patches/PhysGrabberPatches.cs +++ b/Source/Patches/PhysGrabberPatches.cs @@ -149,7 +149,7 @@ private static IEnumerable RayCheckPatches(IEnumerable)CalculateNewForward).Method), new CodeInstruction(OpCodes.Stloc_1), @@ -158,14 +158,11 @@ private static IEnumerable RayCheckPatches(IEnumerable matcher.Advance(-1).ReplaceCameraWithHand()) - .Start() - .MatchForward(false, new CodeMatch(OpCodes.Call, PropertyGetter(typeof(Camera), nameof(Camera.main)))) - .Repeat(matcher => matcher.ReplaceCameraWithHand()) .InstructionEnumeration(); static Vector3 CalculateNewForward(PhysGrabber grabber) { - if (grabber.overrideGrab && grabber.overrideGrabTarget) + if (grabber.overrideGrabTarget) return (grabber.overrideGrabTarget.transform.position - VRSession.Instance.Player.MainHand.position) .normalized; @@ -272,38 +269,27 @@ private static void OnReleaseObject(PhysGrabber __instance) session.Player.Rig.inventoryController.TryEquipItem(item); } - private static float forceGrabTimer; - /// - /// Every time a grab override is triggered, reset the timer - /// - [HarmonyPatch(typeof(PhysGrabber), nameof(PhysGrabber.OverrideGrab))] - [HarmonyPostfix] - private static void OnOverrideGrab(PhysGrabber __instance) - { - forceGrabTimer = 0.1f; - } - - /// - /// If the is above zero, do not allow the grabber to let go + /// Make sure the force grab timer also works in VR, where the grab key is the same as the "take from inventory" key /// [HarmonyPatch(typeof(PhysGrabber), nameof(PhysGrabber.Update))] [HarmonyTranspiler] - private static IEnumerable ForceOverrideGrabPatch(IEnumerable instructions) + private static IEnumerable ForceHoldPatch(IEnumerable instructions) { return new CodeMatcher(instructions) .MatchForward(false, - new CodeMatch(OpCodes.Stfld, Field(typeof(PhysGrabber), nameof(PhysGrabber.overrideGrabTarget)))) - .Advance(-13) - .SetInstruction(new CodeInstruction(OpCodes.Call, ((Func)CheckAndUpdate).Method)) + new CodeMatch(OpCodes.Ldfld, + Field(typeof(GameplayManager), nameof(GameplayManager.itemUnequipAutoHold)))) + .Advance(-2) + .InsertAndAdvance( + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Ldfld, Field(typeof(PhysGrabber), nameof(PhysGrabber.overrideGrabTimer))), + new CodeInstruction(OpCodes.Call, ((Func)ShouldToggleGrabOff).Method) + ) .InstructionEnumeration(); - static bool CheckAndUpdate(PhysGrabber grabber) - { - forceGrabTimer -= Time.deltaTime; - - return grabber.overrideGrab && forceGrabTimer <= 0; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool ShouldToggleGrabOff(bool grabHeld, float grabTimer) => grabHeld && grabTimer <= 0; } } @@ -313,32 +299,14 @@ internal static class PhysGrabberUniversalPatches private static Transform GetHandTransform(PhysGrabber grabber) { if (grabber.playerAvatar.isLocal) - return VRSession.InVR ? VRSession.Instance.Player.MainHand : grabber.playerAvatar.localCameraTransform; + return VRSession.InVR ? VRSession.Instance.Player.MainHand : grabber.playerAvatar.localCamera.transform; if (!NetworkSystem.instance) - { - Logger.LogError("NetworkSystem is null?"); - return grabber.playerAvatar.localCameraTransform; - } - - if (NetworkSystem.instance.GetNetworkPlayer(grabber.playerAvatar, out var networkPlayer)) - { - if (!networkPlayer) - { - Logger.LogError("NetworkPlayer is null?"); - return grabber.playerAvatar.localCameraTransform; - } - - if (!networkPlayer.PrimaryHand) - { - Logger.LogError("GrabberHand is null?"); - return grabber.playerAvatar.localCameraTransform; - } - - return networkPlayer.PrimaryHand; - } + return grabber.playerAvatar.localCamera.transform; - return grabber.playerAvatar.localCameraTransform; + return NetworkSystem.instance.GetNetworkPlayer(grabber.playerAvatar, out var networkPlayer) + ? networkPlayer.PrimaryHand + : grabber.playerAvatar.localCamera.transform; } /// @@ -363,17 +331,48 @@ private static IEnumerable ObjectTurningPatches(IEnumerable)GetHandTransform).Method)) // Replace camera transform with hand transform (remote player) .MatchForward(false, - new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraTransform)))) + new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera)))) .Advance(-1) - .RemoveInstructions(2) + .RemoveInstructions(3) .Insert(new CodeInstruction(OpCodes.Call, ((Func)GetHandTransform).Method)) .InstructionEnumeration(); } + + /// + /// Make our tumble climb follow our hand rotation instead of the camera rotation + /// + [HarmonyPatch(typeof(PhysGrabber), nameof(PhysGrabber.GrabStateClimb))] + [HarmonyTranspiler] + private static IEnumerable HandBasedTumbleClimbPatch(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, + new CodeMatch(OpCodes.Stloc_2)) + .Advance(-2) + .SetAndAdvance(OpCodes.Call, ((Func)GetRotation).Method) + .RemoveInstruction() + .InstructionEnumeration(); + + static Quaternion GetRotation(PhysGrabber grabber) + { + if (grabber.playerAvatar.isLocal) + return VRSession.InVR + ? VRSession.Instance.Player.MainHand.rotation * Quaternion.Euler(0, 180, 0) + : grabber.climbStickTransform.rotation; + + if (!NetworkSystem.instance) + return grabber.climbStickTransform.rotation; + + return NetworkSystem.instance.GetNetworkPlayer(grabber.playerAvatar, out var networkPlayer) + ? networkPlayer.PrimaryHand.rotation * Quaternion.Euler(0, 180, 0) + : grabber.climbStickTransform.rotation; + } + } } \ No newline at end of file diff --git a/Source/Patches/Player/InventoryPatches.cs b/Source/Patches/Player/InventoryPatches.cs index 8f1071a..e7e9aef 100644 --- a/Source/Patches/Player/InventoryPatches.cs +++ b/Source/Patches/Player/InventoryPatches.cs @@ -140,6 +140,31 @@ static bool ShouldTeleport(PhysGrabObject @object) } } + /// + /// Prevent items from being "hidden" when equipped in an inventory + /// + [HarmonyPatch(typeof(PhysGrabObject), nameof(PhysGrabObject.OverrideDeactivate))] + [HarmonyTranspiler] + private static IEnumerable DisableItemOverrideHiding(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, + new CodeMatch(OpCodes.Call, Method(typeof(SemiFunc), nameof(SemiFunc.IsMasterClientOrSingleplayer)))) + .Set(OpCodes.Call, ((Func)ShouldMoveItem).Method) + .Insert(new CodeInstruction(OpCodes.Ldarg_0)) + .InstructionEnumeration(); + + static bool ShouldMoveItem(PhysGrabObject @object) + { + var result = SemiFunc.IsMasterClientOrSingleplayer(); + + if (!@object.TryGetComponent(out var item)) + return result; + + return result && !ItemIsMine(item); + } + } + [HarmonyPatch(typeof(InventorySpot), nameof(InventorySpot.EquipItem))] [HarmonyPrefix] private static void OnItemEquip(InventorySpot __instance, ItemEquippable item) @@ -187,8 +212,16 @@ internal static class UniversalInventoryPatches [HarmonyPrefix] private static bool DontMeleeWhenEquipped(ItemMelee __instance) { - return !(__instance.itemEquippable.currentState == ItemEquippable.ItemState.Equipped && - (!SemiFunc.IsMultiplayer() || PhotonView.Find(__instance.itemEquippable.ownerPlayerId) - .GetComponent().IsVRPlayer())); + var isEquipped = + __instance.itemEquippable.currentState is ItemEquippable.ItemState.Equipped + or ItemEquippable.ItemState.Equipping; + + if (!isEquipped) // Return early, as `ownerPlayerId` isn't set here yet + return true; + + var isVRPlayer = (SemiFunc.IsMultiplayer() && PhotonView.Find(__instance.itemEquippable.ownerPlayerId) + .GetComponent().IsVRPlayer()) || (!SemiFunc.IsMultiplayer() && VRSession.InVR); + + return !isVRPlayer; } } \ No newline at end of file diff --git a/Source/Patches/Player/MapToolPatches.cs b/Source/Patches/Player/MapToolPatches.cs index fc9f39a..e3d1b66 100644 --- a/Source/Patches/Player/MapToolPatches.cs +++ b/Source/Patches/Player/MapToolPatches.cs @@ -32,8 +32,8 @@ private static void OnMapToolCreated(MapToolController __instance) private static IEnumerable MapToolDisableInput(IEnumerable instructions) { return new CodeMatcher(instructions) - .Advance(1) - .RemoveInstructions(87) + .Start() + .RemoveInstructions(89) .InstructionEnumeration(); } diff --git a/Source/Patches/Player/PlayerAvatarPatches.cs b/Source/Patches/Player/PlayerAvatarPatches.cs index 957e973..bf037d3 100644 --- a/Source/Patches/Player/PlayerAvatarPatches.cs +++ b/Source/Patches/Player/PlayerAvatarPatches.cs @@ -1,7 +1,12 @@ -using HarmonyLib; +using System.Collections.Generic; +using System.Reflection.Emit; +using HarmonyLib; using RepoXR.Managers; +using RepoXR.Player.Camera; using UnityEngine; +using static HarmonyLib.AccessTools; + namespace RepoXR.Patches.Player; [RepoXRPatch] @@ -14,7 +19,7 @@ internal static class PlayerAvatarPatches [HarmonyPostfix] private static void OnPlayerDeath(PlayerAvatar __instance) { - if (!__instance.isLocal || VRSession.Instance is not {} session) + if (!__instance.isLocal || VRSession.Instance is not { } session) return; session.Player.Rig.SetVisible(false); @@ -29,7 +34,7 @@ private static void OnPlayerRevive(PlayerAvatar __instance) { if (!__instance.isLocal || VRSession.Instance is not { } session) return; - + session.Player.Rig.SetVisible(true); // Reset CameraAimOffset (for when revived during the top-down death sequence) @@ -37,4 +42,21 @@ private static void OnPlayerRevive(PlayerAvatar __instance) offsetTransform.localRotation = Quaternion.identity; offsetTransform.localPosition = Vector3.zero; } + + /// + /// Look at the enemy that killed you (if possible) + /// + [HarmonyPatch(typeof(PlayerAvatar), nameof(PlayerAvatar.Update))] + [HarmonyTranspiler] + private static IEnumerable PlayerDeathLookAtEnemyPatch(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, + new CodeMatch(OpCodes.Ldsfld, Field(typeof(CameraAim), nameof(CameraAim.Instance)))) + .SetOperandAndAdvance(Field(typeof(VRCameraAim), nameof(VRCameraAim.instance))) + .Advance(9) + .InsertAndAdvance(new CodeInstruction(OpCodes.Ldc_I4_1)) + .SetOperandAndAdvance(Method(typeof(VRCameraAim), nameof(VRCameraAim.SetAimTarget))) + .InstructionEnumeration(); + } } \ No newline at end of file diff --git a/Source/Patches/Player/PlayerDeathHeadPatches.cs b/Source/Patches/Player/PlayerDeathHeadPatches.cs new file mode 100644 index 0000000..a07c3f3 --- /dev/null +++ b/Source/Patches/Player/PlayerDeathHeadPatches.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Reflection.Emit; +using HarmonyLib; +using RepoXR.Player; + +using static HarmonyLib.AccessTools; + +namespace RepoXR.Patches.Player; + +[RepoXRPatch] +internal static class PlayerDeathHeadPatches +{ + /// + /// Replace the "on screen" detection with custom detection that is better suited for VR and supports + /// eye tracking + /// + [HarmonyPatch(typeof(PlayerDeathHead), nameof(PlayerDeathHead.Update))] + [HarmonyTranspiler] + private static IEnumerable LookAtHeadPatch(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, new CodeMatch(OpCodes.Call, Method(typeof(SemiFunc), nameof(SemiFunc.OnScreen)))) + .SetOperandAndAdvance(Method(typeof(VREyeTracking), nameof(VREyeTracking.LookingAt))) + .InstructionEnumeration(); + } +} \ No newline at end of file diff --git a/Source/Patches/Player/PlayerEyesPatches.cs b/Source/Patches/Player/PlayerEyesPatches.cs new file mode 100644 index 0000000..1b6d439 --- /dev/null +++ b/Source/Patches/Player/PlayerEyesPatches.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Emit; +using HarmonyLib; +using RepoXR.Networking; + +namespace RepoXR.Patches.Player; + +[RepoXRPatch(RepoXRPatchTarget.Universal)] +internal static class PlayerEyesPatches +{ + /// + /// Make players that use eye tracking have their eyes controller by their *real* eyes + /// + [HarmonyPatch(typeof(PlayerEyes), nameof(PlayerEyes.LookAtTransform))] + [HarmonyPostfix] + private static void LookAtTransformEyeTracking(PlayerEyes __instance) + { + if (!__instance.playerAvatar || __instance.playerAvatar.isLocal) + return; + + if (!NetworkSystem.instance.GetNetworkPlayer(__instance.playerAvatar, out var player) || !player.EyeTracking) + return; + + __instance.lookAtActive = true; + __instance.lookAt.transform.position = player.EyeGazePoint; + } + + /// + /// Make sure eye tracked players don't move their heads by merely looking around + /// + [HarmonyPatch(typeof(PlayerAvatarVisuals), nameof(PlayerAvatarVisuals.Update))] + [HarmonyTranspiler] + private static IEnumerable KeepHeadRotationPatch(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, new CodeMatch(OpCodes.Ldc_R4, 40f)) + .SetAndAdvance(OpCodes.Ldarg_0, null) + .Insert(new CodeInstruction(OpCodes.Call, ((Func)GetMaxAngle).Method)) + .InstructionEnumeration(); + + static float GetMaxAngle(PlayerAvatarVisuals visuals) + { + if (visuals.isMenuAvatar) + return 40; + + if (!NetworkSystem.instance.GetNetworkPlayer(visuals.playerAvatar, out var player) || !player.EyeTracking) + return 40; + + // Do not angle the head with the eyes if eye tracking is enabled + return 0; + } + } +} \ No newline at end of file diff --git a/Source/Patches/SpectatePatches.cs b/Source/Patches/SpectatePatches.cs index e988fdf..66eff18 100644 --- a/Source/Patches/SpectatePatches.cs +++ b/Source/Patches/SpectatePatches.cs @@ -3,6 +3,7 @@ using System.Reflection.Emit; using HarmonyLib; using RepoXR.Input; +using RepoXR.Player.Camera; using UnityEngine; using static HarmonyLib.AccessTools; @@ -153,7 +154,7 @@ private static void CameraTurnPatch(SpectateCamera __instance) break; case Config.TurnProviderOption.Smooth: - if (!Plugin.Config.DynamicSmoothSpeed.Value) + if (!Plugin.Config.AnalogSmoothTurn.Value) value = value == 0 ? 0 : Math.Sign(value); spectateTurnAmount += 180 * Time.deltaTime * Plugin.Config.SmoothTurnSpeedModifier.Value * value; @@ -163,4 +164,64 @@ private static void CameraTurnPatch(SpectateCamera __instance) break; } } + + /// + /// Allow snap/smooth turning in the spectator head camera + /// + [HarmonyPatch(typeof(SpectateCamera), nameof(SpectateCamera.StateHead))] + [HarmonyPostfix] + private static void HeadCameraTurnPatch(SpectateCamera __instance) + { + var value = Actions.Instance["Turn"].ReadValue(); + + switch (Plugin.Config.TurnProvider.Value) + { + case Config.TurnProviderOption.Snap: + var should = Mathf.Abs(value) > 0.75f; + var snapSize = Plugin.Config.SnapTurnSize.Value; + + if (!turnedLastInput && should) + if (value > 0) + VRCameraAim.instance.TurnAimNow(snapSize); + else + VRCameraAim.instance.TurnAimNow(-snapSize); + + turnedLastInput = should; + + break; + + case Config.TurnProviderOption.Smooth: + if (!Plugin.Config.AnalogSmoothTurn.Value) + value = value == 0 ? 0 : Math.Sign(value); + + VRCameraAim.instance.TurnAimNow(180 * Time.deltaTime * Plugin.Config.SmoothTurnSpeedModifier.Value * value); + break; + + case Config.TurnProviderOption.Disabled: + break; + } + } + + /// + /// Make sure the VR camera is always centered to the player's death head (3-DoF) + /// + [HarmonyPatch(typeof(SpectateCamera), nameof(SpectateCamera.StateHead))] + [HarmonyTranspiler] + private static IEnumerable AlignCameraWithHeadPatch(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, new CodeMatch(OpCodes.Call, PropertyGetter(typeof(Time), nameof(Time.deltaTime)))) + .Advance(-3) + .InsertAndAdvance(new CodeInstruction(OpCodes.Ldarg_0)) + .SetAndAdvance(OpCodes.Call, ((Func)GetTargetPoint).Method) + .RemoveInstruction() + .InstructionEnumeration(); + + static Vector3 GetTargetPoint(PlayerDeathHead head, SpectateCamera camera) + { + var cameraPos = camera.transform.InverseTransformPoint(Camera.main!.transform.position); + + return head.physGrabObject.centerPoint - camera.transform.rotation * cameraPos; + } + } } diff --git a/Source/Patches/UI/ChatPatches.cs b/Source/Patches/UI/ChatPatches.cs index 2b4e360..5822121 100644 --- a/Source/Patches/UI/ChatPatches.cs +++ b/Source/Patches/UI/ChatPatches.cs @@ -29,7 +29,7 @@ private static IEnumerable ChatOpenButtonPatch(IEnumerable ChatCloseButtonPatch(IEnumerable + /// Reset position when loading UI is shown + /// + [HarmonyPatch(typeof(LoadingUI), nameof(LoadingUI.StartLoading))] + [HarmonyPostfix] + private static void OnStartLoading() + { + Object.FindObjectOfType()?.ResetPosition(); + } + /// /// Fix the controller binding icon on the stuck text and mask it away when it's not shown /// diff --git a/Source/Patches/UI/TutorialPatches.cs b/Source/Patches/UI/TutorialPatches.cs index b76aa62..961d2ef 100644 --- a/Source/Patches/UI/TutorialPatches.cs +++ b/Source/Patches/UI/TutorialPatches.cs @@ -115,4 +115,14 @@ private static void TruckForceRotate(TutorialTruckTrigger __instance) VRCameraAim.instance.SetAimTarget(__instance.lookTarget.position + Vector3.down, 0.1f, 5, __instance.gameObject, 90, true); } + + /// + /// Make the spectate head prompt UI have the correct control sprites + /// + [HarmonyPatch(typeof(SpectateHeadUI), nameof(SpectateHeadUI.Awake))] + [HarmonyPostfix] + private static void OnSpectateHeadUICreate(SpectateHeadUI __instance) + { + __instance.promptText.spriteAsset = AssetCollection.TMPInputsSpriteAsset; + } } \ No newline at end of file diff --git a/Source/Patches/UI/UIPatches.cs b/Source/Patches/UI/UIPatches.cs index 303ba11..e183df8 100644 --- a/Source/Patches/UI/UIPatches.cs +++ b/Source/Patches/UI/UIPatches.cs @@ -219,9 +219,10 @@ private static void HandleVRScrollLogic(MenuScrollBox __instance) __instance.scrollHandleTargetPosition = pos; } - if (manager.GetUIScrollY() != 0) + if (manager.GetUIScrollY() != 0 && SemiFunc.NoTextInputsActive()) { __instance.scrollHandleTargetPosition += manager.GetUIScrollY() * 20 / (__instance.scrollHeight * 0.01f); + if (__instance.scrollHandleTargetPosition < __instance.scrollHandle.sizeDelta.y / 2f) __instance.scrollHandleTargetPosition = __instance.scrollHandle.sizeDelta.y / 2f; if (__instance.scrollHandleTargetPosition > @@ -276,8 +277,7 @@ private static IEnumerable ScrollDisableInputs(IEnumerable diff --git a/Source/Patches/UI/ValuableDiscoverGraphicPatches.cs b/Source/Patches/UI/ValuableDiscoverGraphicPatches.cs deleted file mode 100644 index 5885369..0000000 --- a/Source/Patches/UI/ValuableDiscoverGraphicPatches.cs +++ /dev/null @@ -1,142 +0,0 @@ -using HarmonyLib; -using RepoXR.Managers; -using UnityEngine; - -namespace RepoXR.Patches.UI; - -[RepoXRPatch] -internal static class ValuableDiscoverGraphicPatches -{ - [HarmonyPatch(typeof(ValuableDiscoverGraphic), nameof(ValuableDiscoverGraphic.Start))] - [HarmonyPostfix] - private static void OnValuableDiscovered(ValuableDiscoverGraphic __instance) - { - // Create canvas for rendering in world space - var canvas = new GameObject("World Space Valuable Graphic") { layer = 5 }.AddComponent(); - canvas.renderMode = RenderMode.WorldSpace; - - // Move graphic to canvas - __instance.transform.SetParent(canvas.transform, false); - __instance.transform.localPosition = Vector3.zero; - __instance.transform.localEulerAngles = Vector3.zero; - __instance.transform.localScale = Vector3.one; - - __instance.canvasRect = canvas.GetComponent(); - - // Anchor images - var container = __instance.middle.parent.GetComponent(); - var middle = __instance.middle.GetComponent(); - var topLeft = __instance.topLeft.GetComponent(); - var topRight = __instance.topRight.GetComponent(); - var bottomLeft = __instance.botLeft.GetComponent(); - var bottomRight = __instance.botRight.GetComponent(); - - const float offset = 0.024f; - - container.anchorMax = new Vector2(1, 1); - container.anchorMin = new Vector2(0, 0); - container.offsetMax = Vector2.zero; - container.offsetMin = Vector2.zero; - container.anchoredPosition = Vector2.zero; - - middle.anchorMax = new Vector2(1, 1); - middle.anchorMin = new Vector2(0, 0); - middle.offsetMin = new Vector2(0, 0); - middle.offsetMax = new Vector2(0, 0); - middle.anchoredPosition = Vector2.zero; - middle.sizeDelta = Vector2.zero; - - topLeft.anchorMax = new Vector2(0, 1); - topLeft.anchorMin = new Vector2(0, 1); - topLeft.anchoredPosition = new Vector2(offset, -offset); - topLeft.localScale = Vector3.one * 0.0006f; - - topRight.anchorMax = new Vector2(1, 1); - topRight.anchorMin = new Vector2(1, 1); - topRight.anchoredPosition = new Vector2(-offset, -offset); - topRight.localScale = Vector3.one * 0.0006f; - - bottomLeft.anchorMax = new Vector2(0, 0); - bottomLeft.anchorMin = new Vector2(0, 0); - bottomLeft.anchoredPosition = new Vector2(offset, offset); - bottomLeft.localScale = Vector3.one * 0.0006f; - - bottomRight.anchorMax = new Vector2(1, 0); - bottomRight.anchorMin = new Vector2(1, 0); - bottomRight.anchoredPosition = new Vector2(-offset, offset); - bottomRight.localScale = Vector3.one * 0.0006f; - } - - /// - /// A replacement for the original Update method that makes the graphic show in world space - /// - [HarmonyPatch(typeof(ValuableDiscoverGraphic), nameof(ValuableDiscoverGraphic.Update))] - [HarmonyPrefix] - private static bool ValuableDiscoverGraphicUpdate(ValuableDiscoverGraphic __instance) - { - var mainCamera = VRSession.Instance.MainCamera.transform; - var canvas = __instance.transform.parent.GetComponent(); - - if (__instance.target) - { - var bounds = new Bounds(__instance.target.centerPoint, Vector3.zero); - - foreach (var meshRenderer in __instance.target.GetComponentsInChildren()) - bounds.Encapsulate(meshRenderer.bounds); - - canvas.position = bounds.center; - canvas.sizeDelta = new Vector2(bounds.size.x + 0.05f, bounds.size.y + 0.05f); - canvas.LookAt(mainCamera.position); - canvas.eulerAngles = new Vector3(0, canvas.eulerAngles.y, 0); - - if (SemiFunc.OnScreen(bounds.center, 0.5f, 0.5f)) - { - if (__instance.first) - { - if (__instance.state == ValuableDiscoverGraphic.State.Reminder) - __instance.sound.Play(__instance.target.centerPoint, 0.3f); - else - __instance.sound.Play(__instance.target.centerPoint); - - __instance.middle.gameObject.SetActive(true); - __instance.topLeft.gameObject.SetActive(true); - __instance.topRight.gameObject.SetActive(true); - __instance.botLeft.gameObject.SetActive(true); - __instance.botRight.gameObject.SetActive(true); - - __instance.first = false; - } - } - } - else - __instance.waitTimer = 0; - - if (__instance.waitTimer > 0) - { - __instance.animLerp = Mathf.Clamp01(__instance.animLerp + __instance.introSpeed * Time.deltaTime); - canvas.localScale = Vector3.LerpUnclamped(Vector3.zero, Vector3.one, - __instance.introCurve.Evaluate(__instance.animLerp)); - - if (__instance.animLerp >= 1) - { - __instance.waitTimer -= Time.deltaTime; - if (__instance.waitTimer <= 0) - { - __instance.animLerp = 0; - return false; - } - } - } - else - { - __instance.animLerp = Mathf.Clamp01(__instance.animLerp + __instance.outroSpeed * Time.deltaTime); - canvas.localScale = Vector3.LerpUnclamped(Vector3.one, Vector3.zero, - __instance.outroCurve.Evaluate(__instance.animLerp)); - - if (__instance.animLerp >= 1) - Object.Destroy(__instance.transform.parent.gameObject); - } - - return false; - } -} \ No newline at end of file diff --git a/Source/Patches/UI/ValuableDiscoverPatches.cs b/Source/Patches/UI/ValuableDiscoverPatches.cs new file mode 100644 index 0000000..41e3c44 --- /dev/null +++ b/Source/Patches/UI/ValuableDiscoverPatches.cs @@ -0,0 +1,73 @@ +using System.Runtime.CompilerServices; +using CustomDiscoverStateLib; +using HarmonyLib; +using RepoXR.Assets; +using UnityEngine; + +namespace RepoXR.Patches.UI; + +[RepoXRPatch] +internal static class ValuableDiscoverPatches +{ + /// + /// Replace the in-game with a custom one that renders a 3d cube instead of + /// a canvas element + /// + [HarmonyPatch(typeof(ValuableDiscover), nameof(ValuableDiscover.New))] + [HarmonyPrefix] + private static bool OnValuableDiscovered(ValuableDiscover __instance, PhysGrabObject _target, + ValuableDiscoverGraphic.State _state) + { + var component = Object.Instantiate(AssetCollection.ValuableDiscover) + .GetComponent(); + component.target = _target; + + if (Compat.IsLoaded(Compat.CustomDiscoverStateLib) && + AttemptSetupCustom(component, __instance, _target, _state)) + return false; // Return early if CustomDiscoverState is applicable to this valuable + + if (_state == ValuableDiscoverGraphic.State.Reminder) + component.ReminderSetup(); + + if (_state == ValuableDiscoverGraphic.State.Bad) + component.BadSetup(); + + return false; + } + + /// + /// Attempt to set up custom colors provided by CustomDiscoverStateLib if possible + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool AttemptSetupCustom(RepoXR.UI.ValuableDiscoverGraphic graphic, ValuableDiscover valuable, + PhysGrabObject target, ValuableDiscoverGraphic.State state) + { + if (CustomDiscoverState.customStates.TryGetValue(state, out var customState)) + { + graphic.CustomSetup(state, customState.ColorMiddle, customState.ColorCorner); + return true; + } + + foreach (var (condState, condDelegate) in CustomDiscoverState.conditionalStates) + { + if (!condDelegate(valuable, target) || CustomDiscoverState.customStates[condState] == null) + continue; + + customState = CustomDiscoverState.customStates[condState]; + graphic.CustomSetup(condState, customState.ColorMiddle, customState.ColorCorner); + + return true; + } + + foreach (var (dynState, dynDelegate) in CustomDiscoverState.dynamicStates) + { + if (!dynDelegate(valuable, target, out var middle, out var corner)) + continue; + + graphic.CustomSetup(dynState, middle!.Value, corner!.Value); + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/Source/Player/Camera/VRCameraAim.cs b/Source/Player/Camera/VRCameraAim.cs index 7bcce00..fb560dc 100644 --- a/Source/Player/Camera/VRCameraAim.cs +++ b/Source/Player/Camera/VRCameraAim.cs @@ -5,18 +5,16 @@ namespace RepoXR.Player.Camera; -// KNOWN ISSUE: When the player is not near their play area center, the VR aim script will pivot around a far away point -// instead of at the camera itself. This is a known issue, no clue how to fix it yet. - public class VRCameraAim : MonoBehaviour { public static VRCameraAim instance; - + private CameraAim cameraAim; private Transform mainCamera; - - private Quaternion rotation; - + + public Quaternion rotationXZ = Quaternion.identity; + public Quaternion rotationY = Quaternion.identity; + // Aim fields private bool aimTargetActive; private GameObject? aimTargetObject; @@ -27,7 +25,7 @@ public class VRCameraAim : MonoBehaviour private bool aimTargetLowImpact; private float aimTargetLerp; - + // Soft aim fields private GameObject? aimTargetSoftObject; private Vector3 aimTargetSoftPosition; @@ -36,18 +34,18 @@ public class VRCameraAim : MonoBehaviour private float aimTargetSoftStrengthNoAim; private int aimTargetSoftPriority = 999; private bool aimTargetSoftLowImpact; - + private float aimTargetSoftStrengthCurrent; private Quaternion lastCameraRotation; private float playerAimingTimer; public bool IsActive => aimTargetActive; - + private void Awake() { instance = this; - + cameraAim = GetComponent(); mainCamera = GetComponentInChildren().transform; } @@ -55,7 +53,7 @@ private void Awake() private void Update() { // Detect head movement - + if (lastCameraRotation == Quaternion.identity) lastCameraRotation = mainCamera.localRotation; @@ -66,7 +64,7 @@ private void Update() lastCameraRotation = mainCamera.localRotation; // Perform forced rotations - + if (playerAimingTimer > 0) playerAimingTimer -= Time.deltaTime; @@ -75,33 +73,37 @@ private void Update() aimTargetTimer -= Time.deltaTime; aimTargetLerp += Time.deltaTime * aimTargetSpeed; aimTargetLerp = Mathf.Clamp01(aimTargetLerp); - } else if (aimTargetLerp > 0) + } + else if (aimTargetLerp > 0) { - cameraAim.ResetPlayerAim(mainCamera.rotation); + cameraAim.SetPlayerAim(mainCamera.rotation, false); aimTargetLerp = 0; aimTargetPriority = 999; aimTargetActive = false; } - var targetRotation = GetLookRotation(aimTargetPosition); + var (targetY, targetXZ) = GetLookRotation(aimTargetPosition); if (aimTargetLowImpact) - targetRotation = Quaternion.Euler(0, targetRotation.eulerAngles.y, 0); - - rotation = Quaternion.LerpUnclamped(rotation, targetRotation, cameraAim.AimTargetCurve.Evaluate(aimTargetLerp)); - + targetXZ = Quaternion.identity; + + rotationXZ = Quaternion.LerpUnclamped(rotationXZ, targetXZ, cameraAim.AimTargetCurve.Evaluate(aimTargetLerp)); + rotationY = Quaternion.LerpUnclamped(rotationY, targetY, cameraAim.AimTargetCurve.Evaluate(aimTargetLerp)); + if (aimTargetSoftTimer > 0 && aimTargetTimer <= 0) { var targetStrength = playerAimingTimer <= 0 ? aimTargetSoftStrengthNoAim : aimTargetSoftStrength; - aimTargetSoftStrengthCurrent = Mathf.Lerp(aimTargetSoftStrengthCurrent, targetStrength, 10 * Time.deltaTime); + aimTargetSoftStrengthCurrent = + Mathf.Lerp(aimTargetSoftStrengthCurrent, targetStrength, 10 * Time.deltaTime); - targetRotation = GetLookRotation(aimTargetSoftPosition); + (targetY, targetXZ) = GetLookRotation(aimTargetSoftPosition); if (aimTargetSoftLowImpact) - targetRotation = Quaternion.Euler(0, targetRotation.eulerAngles.y, 0); - - rotation = Quaternion.Lerp(rotation, targetRotation, aimTargetSoftStrengthCurrent * Time.deltaTime); + targetXZ = Quaternion.identity; + + rotationXZ = Quaternion.Lerp(rotationXZ, targetXZ, aimTargetSoftStrengthCurrent * Time.deltaTime); + rotationY = Quaternion.Lerp(rotationY, targetY, aimTargetSoftStrengthCurrent * Time.deltaTime); aimTargetSoftTimer -= Time.deltaTime; @@ -113,29 +115,29 @@ private void Update() } if (!aimTargetActive && aimTargetSoftTimer <= 0) - rotation = Quaternion.LerpUnclamped(rotation, Quaternion.Euler(0, rotation.eulerAngles.y, 0), 5 * Time.deltaTime); - - var lastRotation = transform.localRotation; - - transform.localPosition = Vector3.zero; - transform.localRotation = Quaternion.Euler(0, transform.localEulerAngles.y, 0); - - var cameraPos = mainCamera.transform.position; - - transform.localRotation = Quaternion.Lerp(lastRotation, rotation, 33 * Time.deltaTime); - transform.localPosition = cameraPos - mainCamera.transform.position; - + rotationXZ = Quaternion.Lerp(rotationXZ, Quaternion.identity, 5 * Time.deltaTime); + + if (SpectateCamera.instance && SpectateCamera.instance.CheckState(SpectateCamera.State.Death)) + transform.localRotation = Quaternion.identity; + else + transform.localRotation = Quaternion.Lerp(transform.localRotation, rotationY, 33 * Time.deltaTime); + // Finally, reset the player aim - - cameraAim.ResetPlayerAim(mainCamera.rotation); + + cameraAim.SetPlayerAim(mainCamera.rotation, false); } - private Quaternion GetLookRotation(Vector3 position) + private (Quaternion, Quaternion) GetLookRotation(Vector3 position) { - var desired = Quaternion.LookRotation(position - mainCamera.transform.position, Vector3.up); - var camDelta = desired * Quaternion.Inverse(mainCamera.transform.rotation); + var finalWorldRot = Quaternion.LookRotation(position - mainCamera.position, Vector3.up); + var localRot = finalWorldRot * Quaternion.Inverse(TrackingInput.Instance.HeadTransform.rotation); + var localFwd = localRot * Vector3.forward; + var localYawFwd = new Vector3(localFwd.x, 0, localFwd.z).normalized; + + var qY = Quaternion.LookRotation(localYawFwd, Vector3.up); + var qXZ = Quaternion.Inverse(qY) * localRot; - return camDelta * transform.rotation; + return (qY, qXZ); } /// @@ -143,37 +145,41 @@ private Quaternion GetLookRotation(Vector3 position) /// public void TurnAimNow(float degrees) { - var rot = Quaternion.Euler(transform.eulerAngles + Vector3.up * degrees); + var rot = Quaternion.Euler(rotationY.eulerAngles + Vector3.up * degrees); transform.localRotation = rot; - rotation = rot; + rotationY = rot; } /// /// Instantly change the aim rotation without any interpolation or smoothing /// - public void ForceSetRotation(Vector3 newAngles) + public void SetAimNow(float degrees) { - var rot = Quaternion.Euler(newAngles); - + var rot = Quaternion.Euler(0, degrees, 0); + transform.localRotation = rot; - rotation = rot; + rotationY = rot; } + private bool setInitialAim; + /// - /// Set spawn rotation, which takes into account the current Y rotation of the headset + /// Set current aim rotation, which takes into account the current Y rotation of the headset /// - public void SetSpawnRotation(float yRot) + public void SetPlayerAim(float yRot, bool forceInitial = false) { - if (CameraNoPlayerTarget.instance) + if (CameraNoPlayerTarget.instance && (!setInitialAim || forceInitial)) + { yRot = CameraNoPlayerTarget.instance.transform.eulerAngles.y; - - var angle = new Vector3(0, yRot - TrackingInput.instance.HeadTransform.localEulerAngles.y, 0); - - ForceSetRotation(angle); + setInitialAim = true; + } + + SetAimNow(yRot - TrackingInput.Instance.HeadTransform.localEulerAngles.y); } - public void SetAimTarget(Vector3 position, float time, float speed, GameObject obj, int priority, bool lowImpact = false) + public void SetAimTarget(Vector3 position, float time, float speed, GameObject obj, int priority, + bool lowImpact = false) { if (priority > aimTargetPriority) return; @@ -197,8 +203,8 @@ public void SetAimTargetSoft(Vector3 position, float time, float strength, float return; if (aimTargetSoftObject && obj != aimTargetSoftObject) - return; - + return; + if (obj != aimTargetSoftObject) playerAimingTimer = 0; @@ -228,11 +234,12 @@ private static void OnCameraAimAwake(CameraAim __instance) /// /// Set initial rotation on game start /// - [HarmonyPatch(typeof(CameraAim), nameof(CameraAim.CameraAimSpawn))] + [HarmonyPatch(typeof(CameraAim), nameof(CameraAim.SetPlayerAim))] [HarmonyPostfix] - private static void OnCameraAimSpawn(float _rotation) + private static void OnCameraAimSpawn(ref Quaternion _rotation, bool _setRotation) { - VRCameraAim.instance.SetSpawnRotation(_rotation); + if (_setRotation) + VRCameraAim.instance.SetPlayerAim(_rotation.eulerAngles.y); } /// @@ -244,4 +251,14 @@ private static bool DisableCameraAim(CameraAim __instance) { return false; } + + /// + /// Disable this method as it modifies the camera aim transform + /// + [HarmonyPatch(typeof(CameraAim), nameof(CameraAim.OverridePlayerAimDisable))] + [HarmonyPrefix] + private static bool DisableAimDisableOverride() + { + return false; + } } diff --git a/Source/Player/Camera/VRCameraTracker.cs b/Source/Player/Camera/VRCameraTracker.cs new file mode 100644 index 0000000..5ac407c --- /dev/null +++ b/Source/Player/Camera/VRCameraTracker.cs @@ -0,0 +1,28 @@ +using RepoXR.Input; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.XR; + +namespace RepoXR.Player.Camera; + +public class VRCameraTracker : TrackedPoseDriver +{ + public override void Awake() + { + base.Awake(); + + positionAction = Actions.Instance.HeadPosition; + rotationAction = Actions.Instance.HeadRotation; + trackingStateInput = new InputActionProperty(Actions.Instance.HeadTrackingState); + } + + public override void SetLocalTransform(Vector3 newPosition, Quaternion newRotation) + { + var rotation = newRotation; + + if (VRCameraAim.instance is { } aim) + rotation = aim.rotationXZ * rotation; + + base.SetLocalTransform(newPosition, rotation); + } +} \ No newline at end of file diff --git a/Source/Player/Camera/VRCustomCamera.cs b/Source/Player/Camera/VRCustomCamera.cs index 07db1fa..752534a 100644 --- a/Source/Player/Camera/VRCustomCamera.cs +++ b/Source/Player/Camera/VRCustomCamera.cs @@ -1,6 +1,7 @@ using System; using RepoXR.Input; using UnityEngine; +using UnityEngine.Experimental.Rendering; using UnityEngine.UI; namespace RepoXR.Player.Camera; @@ -12,40 +13,58 @@ namespace RepoXR.Player.Camera; public class VRCustomCamera : MonoBehaviour { public static VRCustomCamera instance; - + + private RenderTexture targetTexture; + [SerializeField] protected UnityEngine.Camera mainCamera; [SerializeField] protected UnityEngine.Camera topCamera; [SerializeField] protected UnityEngine.Camera uiCamera; [SerializeField] protected Image overlayImage; - + private Transform gameplayCamera; + private int lastWidth; + private int lastHeight; + + private float frameTimer; + private void Awake() { instance = this; - + var fov = Plugin.Config.CustomCameraFOV.Value; mainCamera.fieldOfView = fov; topCamera.fieldOfView = fov; uiCamera.fieldOfView = fov; - + gameplayCamera = UnityEngine.Camera.main!.transform; - - transform.localPosition = TrackingInput.instance.HeadTransform.localPosition; - transform.localRotation = TrackingInput.instance.HeadTransform.localRotation; - + + transform.localPosition = TrackingInput.Instance.HeadTransform.localPosition; + transform.localRotation = TrackingInput.Instance.HeadTransform.localRotation; + Plugin.Config.CustomCameraFOV.SettingChanged += OnFOVChanged; + + Application.onBeforeRender += OnBeforeRender; + + UpdateRenderTexture(); } private void OnDestroy() { instance = null!; - + Plugin.Config.CustomCameraFOV.SettingChanged -= OnFOVChanged; + + Application.onBeforeRender -= OnBeforeRender; } - + + private void OnBeforeRender() + { + transform.localPosition = gameplayCamera.localPosition; + } + private void OnFOVChanged(object sender, EventArgs e) { var fov = Plugin.Config.CustomCameraFOV.Value; @@ -71,12 +90,58 @@ private void Update() SemiFunc.MenuLevel() || SemiFunc.RunIsShop() || SemiFunc.RunIsLobby() ? 0.015f : 0.15f; } - private void LateUpdate() { + transform.localPosition = gameplayCamera.localPosition; + // Since we override the FadeOverlay image color in a LateUpdate, we need to read it back in a late update as well // Also this script needs to execute *after* the override, hence the [DefaultExecutionOrder(100)] overlayImage.color = FadeOverlay.Instance.Image.color; + + if (lastWidth != Screen.width || lastHeight != Screen.height) + UpdateRenderTexture(); + + frameTimer += Time.unscaledDeltaTime; + + var interval = 1f / Plugin.Config.CustomCameraFramerate.Value; + if (frameTimer < interval) + return; + + frameTimer -= interval; + + mainCamera.Render(); + topCamera.Render(); + uiCamera.Render(); + } + + private void UpdateRenderTexture() + { + lastWidth = Screen.width; + lastHeight = Screen.height; + + if (targetTexture != null) + targetTexture.Release(); + + targetTexture = new RenderTexture(lastWidth, lastHeight, GraphicsFormat.R8G8B8A8_UNorm, + GraphicsFormat.D32_SFloat_S8_UInt) + { + name = "Custom Camera RT", + antiAliasing = 1, + useMipMap = false, + autoGenerateMips = false, + }; + + mainCamera.targetTexture = targetTexture; + topCamera.targetTexture = targetTexture; + uiCamera.targetTexture = targetTexture; + } + + private void OnRenderImage(RenderTexture source, RenderTexture destination) + { + if (targetTexture != null) + Graphics.Blit(targetTexture, destination); + else + Graphics.Blit(Texture2D.blackTexture, destination); } } diff --git a/Source/Player/VREyeTracking.cs b/Source/Player/VREyeTracking.cs new file mode 100644 index 0000000..8ad506a --- /dev/null +++ b/Source/Player/VREyeTracking.cs @@ -0,0 +1,130 @@ +using System; +using RepoXR.Assets; +using RepoXR.Input; +using RepoXR.Managers; +using RepoXR.Networking; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace RepoXR.Player; + +public class VREyeTracking : MonoBehaviour +{ + private Transform debugCube; + + public bool Enabled => supported && Plugin.Config.EnableEyeTracking.Value; + public Ray Gaze { get; private set; } + + private Vector3 gazePosition; + private Quaternion gazeRotation; + + private bool supported; + private float lastHardwareInput; + + private void Awake() + { + Actions.Instance.EyeGazePosition.performed += OnEyeGazePosition; + Actions.Instance.EyeGazeRotation.performed += OnEyeGazeRotation; + + Plugin.Config.EnableEyeTracking.SettingChanged += OnEyeTrackingSettingChanged; + + if (!Plugin.Flags.HasFlag(Flags.EyeTrackingDebug)) + return; + + debugCube = Instantiate(AssetCollection.Cube).transform; + debugCube.GetComponent().material.color = Color.blue; + debugCube.GetComponent().enabled = false; + debugCube.position = Vector3.down * 1000; + debugCube.gameObject.layer = 5; + debugCube.localScale *= 2; + } + + private void OnDestroy() + { + Actions.Instance.EyeGazePosition.performed -= OnEyeGazePosition; + Actions.Instance.EyeGazeRotation.performed -= OnEyeGazeRotation; + + Plugin.Config.EnableEyeTracking.SettingChanged -= OnEyeTrackingSettingChanged; + } + + private void OnEyeGazePosition(InputAction.CallbackContext ctx) + { + gazePosition = ctx.ReadValue(); + + // Sometimes the OpenXR runtime misfires and triggers eye tracking callbacks even when it doesn't support it + // In that case the data is always 0, so we can just discard the event if we didn't already have data before + if (!supported && gazePosition == Vector3.zero) + return; + + supported = true; + lastHardwareInput = Time.realtimeSinceStartup; + } + + private void OnEyeGazeRotation(InputAction.CallbackContext ctx) + { + gazeRotation = ctx.ReadValue(); + + // Sometimes the OpenXR runtime misfires and triggers eye tracking callbacks even when it doesn't support it + // In that case the data is always 0, so we can just discard the event if we didn't already have data before + if (!supported && gazeRotation == Quaternion.identity) + return; + + supported = true; + lastHardwareInput = Time.realtimeSinceStartup; + } + + private static void OnEyeTrackingSettingChanged(object sender, EventArgs e) + { + if (!Plugin.Config.EnableEyeTracking.Value) + NetworkSystem.instance.DisableEyeTracking(); + } + + private void Update() + { + if (!Enabled) + return; + + // Assume eye tracking is no longer enabled if no data has been received for over 5 seconds + if (Time.realtimeSinceStartup - lastHardwareInput > 5) + { + supported = false; + NetworkSystem.instance.DisableEyeTracking(); + return; + } + + var ray = new Ray(transform.parent.TransformPoint(gazePosition), + transform.parent.TransformDirection(gazeRotation * Vector3.forward)); + + var position = Physics.Raycast(ray, out var hit, 10, SemiFunc.LayerMaskGetShouldHits()) + ? hit.point + : ray.origin + ray.direction * 10; + + Gaze = ray; + + NetworkSystem.instance.UpdateEyeTracking(position); + + if (!debugCube) + return; + + debugCube.position = position; + debugCube.rotation = Quaternion.LookRotation(ray.direction); + } + + public static bool LookingAt(Vector3 position, float padWidth, float padHeight) + { + // Fall back if session is not initialized for some reason + if (VRSession.Instance is not { } session) + return SemiFunc.OnScreen(position, padWidth, padHeight); + + var eyeTracking = session.Player.EyeTracking.Enabled; + + var gaze = eyeTracking + ? session.Player.EyeTracking.Gaze + : new Ray(session.MainCamera.transform.position, session.MainCamera.transform.forward); + var coneAngle = (eyeTracking ? 15f : 25f) + (padWidth + padHeight) * 2.5f; + var direction = position - gaze.origin; + var angle = Vector3.Angle(gaze.direction, direction.normalized); + + return angle <= coneAngle; + } +} \ No newline at end of file diff --git a/Source/Player/VRInventory.cs b/Source/Player/VRInventory.cs index 66fa4c8..09e82dc 100644 --- a/Source/Player/VRInventory.cs +++ b/Source/Player/VRInventory.cs @@ -160,11 +160,16 @@ public void UnequipItem(ItemEquippable item) var slot = slots[item.equippedSpot.inventorySpotIndex]; slot.heldItem = null; + + item.transform.localPosition = Vector3.zero; + item.transform.localRotation = Quaternion.identity; item.transform.parent = GameObject.Find("Level Generator/Items")?.transform; item.rb.interpolation = RigidbodyInterpolation.Interpolate; item.gameObject.SetLayerRecursively(16); item.enabled = true; - + + PhysGrabber.instance.OverrideGrab(item.physGrabObject, 0.25f); + // Re-enable shadows item.GetComponentsInChildren().Do(mesh => mesh.shadowCastingMode = ShadowCastingMode.On); } diff --git a/Source/Player/VRPlayer.cs b/Source/Player/VRPlayer.cs index 7b3c3c0..65b2ea4 100644 --- a/Source/Player/VRPlayer.cs +++ b/Source/Player/VRPlayer.cs @@ -19,18 +19,21 @@ public class VRPlayer : MonoBehaviour // Tracking stuff private Transform mainCamera; + private Transform handContainer; private Transform leftHand; private Transform rightHand; // Reference stuff private PlayerController localController; private VRRig localRig; + private VREyeTracking eyeTracking; // Public accessors public Transform MainHand => VRSession.IsLeftHanded ? localRig.leftHandTip : localRig.rightHandTip; public Transform SecondaryHand => VRSession.IsLeftHanded ? localRig.rightHandTip : localRig.leftHandTip; public Transform MapParent => localRig.map; public VRRig Rig => localRig; + public VREyeTracking EyeTracking => eyeTracking; // Public state public float disableRotateTimer; @@ -57,11 +60,14 @@ private void Awake() // Set up hands and stuff localRig = Instantiate(AssetCollection.VRRig).GetComponent(); + + handContainer = new GameObject("Hand Container").transform; + handContainer.transform.SetParent(mainCamera.transform.parent, false); leftHand = new GameObject("Left Hand").transform; rightHand = new GameObject("Right Hand").transform; - leftHand.transform.parent = rightHand.transform.parent = mainCamera.transform.parent; + leftHand.transform.parent = rightHand.transform.parent = handContainer; var leftHandTracker = leftHand.gameObject.AddComponent(); var rightHandTracker = rightHand.gameObject.AddComponent(); @@ -78,6 +84,8 @@ private void Awake() localRig.leftArmTarget = leftHand; localRig.rightArmTarget = rightHand; + eyeTracking = mainCamera.gameObject.AddComponent(); + Actions.Instance["ResetHeight"].performed += OnResetHeight; } @@ -152,7 +160,7 @@ private void HandleMovement() movementRelative += movement.sqrMagnitude; - if (movementRelative > 0.00025f) + if (movementRelative > 0.0003f) { PlayerController.instance.movingResetTimer = 0.1f; PlayerController.instance.moving = true; @@ -235,13 +243,15 @@ private void HandleTurning() break; case Config.TurnProviderOption.Smooth: - if (!Plugin.Config.DynamicSmoothSpeed.Value) + if (!Plugin.Config.AnalogSmoothTurn.Value) value = value == 0 ? 0 : Math.Sign(value); if (PlayerController.instance.overrideTimeScaleTimer > 0) value *= PlayerController.instance.overrideTimeScaleMultiplier; - - cameraAim.TurnAimNow(180 * Time.deltaTime * Plugin.Config.SmoothTurnSpeedModifier.Value * value); + + if (value != 0) + cameraAim.TurnAimNow(180 * Time.deltaTime * Plugin.Config.SmoothTurnSpeedModifier.Value * value); + break; case Config.TurnProviderOption.Disabled: diff --git a/Source/Player/VRRig.cs b/Source/Player/VRRig.cs index 5fb1520..c7ce38f 100644 --- a/Source/Player/VRRig.cs +++ b/Source/Player/VRRig.cs @@ -28,6 +28,8 @@ public class VRRig : MonoBehaviour public Transform head; public Transform leftArm; public Transform rightArm; + public Transform leftArmCenter; + public Transform rightArmCenter; public Transform leftArmTarget; public Transform rightArmTarget; public Transform leftHandAnchor; @@ -46,6 +48,7 @@ public class VRRig : MonoBehaviour public Collider rightHandCollider; public Collider mapPickupCollider; public Collider lampTriggerCollider; + public Collider[] shoulderMapPickupColliders; public VRInventory inventoryController; @@ -53,7 +56,9 @@ public class VRRig : MonoBehaviour public Vector3 mapRightPosition; public Vector3 mapLeftPosition; - + + private bool armsDetached; + private Transform leftArmMesh; private Transform rightArmMesh; @@ -80,11 +85,16 @@ private void Awake() headlampEnabled = DataManager.instance.headlampEnabled; Plugin.Config.LeftHandDominant.SettingChanged += OnDominantHandChanged; + Plugin.Config.DetachedArms.SettingChanged += OnDetachedArmsChanged; + + // Update on load + OnDetachedArmsChanged(null!, null!); } private void OnDestroy() { Plugin.Config.LeftHandDominant.SettingChanged -= OnDominantHandChanged; + Plugin.Config.DetachedArms.SettingChanged -= OnDetachedArmsChanged; } private IEnumerator Start() @@ -156,8 +166,21 @@ private void UpdateDominantTransforms() NetworkSystem.instance.UpdateDominantHand(Plugin.Config.LeftHandDominant.Value); } - + private void UpdateArms() + { + if (armsDetached) + UpdateArmsDetached(); + else + UpdateArmsAttached(); + + // Synchronize multiplayer rig + if (SemiFunc.IsMultiplayer()) + NetworkSystem.instance.SendRigData(leftHandTip.position, rightHandTip.position, leftHandTip.rotation, + rightHandTip.rotation); + } + + private void UpdateArmsAttached() { leftArm.localPosition = new Vector3(leftArm.localPosition.x, leftArm.localPosition.y, 0); rightArm.localPosition = new Vector3(rightArm.localPosition.x, rightArm.localPosition.y, 0); @@ -191,11 +214,15 @@ private void UpdateArms() leftHandTip.rotation = leftArmTarget.rotation; rightHandTip.rotation = rightArmTarget.rotation; + } - // Synchronize multiplayer rig - if (SemiFunc.IsMultiplayer()) - NetworkSystem.instance.SendRigData(leftHandTip.position, rightHandTip.position, leftHandTip.rotation, - rightHandTip.rotation); + private void UpdateArmsDetached() + { + leftArm.position = leftArmTarget.position - (leftArmCenter.position - leftArm.position); + leftArm.rotation = leftArmTarget.rotation; + + rightArm.position = rightArmTarget.position - (rightArmCenter.position - rightArm.position); + rightArm.rotation = rightArmTarget.rotation; } private void UpdateClaw() @@ -214,7 +241,7 @@ private void UpdateClaw() private Vector3 MapPrimaryPosition => VRSession.IsLeftHanded ? mapLeftPosition : mapRightPosition; private Vector3 MapSecondaryPosition => VRSession.IsLeftHanded ? mapRightPosition : mapLeftPosition; - + private bool mapHovered; private void MapToolLogic() @@ -249,8 +276,8 @@ private void MapToolLogic() return; } - var rightHandHovered = Utils.Collide(rightHandCollider, mapPickupCollider); - var leftHandHovered = Utils.Collide(leftHandCollider, mapPickupCollider); + var rightHandHovered = Utils.Collide(rightHandCollider, [mapPickupCollider, ..shoulderMapPickupColliders]); + var leftHandHovered = Utils.Collide(leftHandCollider, [mapPickupCollider, ..shoulderMapPickupColliders]); // Haptic touch logic if (!mapTool.Active && !mapHovered && leftHandHovered) @@ -267,18 +294,19 @@ private void MapToolLogic() } else if (mapTool.Active || (!leftHandHovered && !rightHandHovered)) mapHovered = false; - + // Flashlight hide logic (before picking up) if (!mapTool.Active && Utils.Collide(VRSession.IsLeftHanded ? rightHandCollider : leftHandCollider, - mapPickupCollider) && !PlayerController.instance.sprinting) + [mapPickupCollider, ..shoulderMapPickupColliders]) && !PlayerController.instance.sprinting) flashlight.hideFlashlight = !headlampEnabled; else if (!mapTool.Active) flashlight.hideFlashlight = false; // Right hand pickup logic if (!mapTool.Active && Actions.Instance["MapGrabRight"].WasPressedThisFrame() && - Utils.Collide(rightHandCollider, mapPickupCollider) && !PlayerController.instance.sprinting) + Utils.Collide(rightHandCollider, [mapPickupCollider, ..shoulderMapPickupColliders]) && + !PlayerController.instance.sprinting) if (mapTool.HideLerp >= 1) { mapTool.transform.parent.parent = rightHandTip; @@ -289,14 +317,15 @@ private void MapToolLogic() // Prevent picking up items while the map is opened if (!VRSession.IsLeftHanded) { - playerAvatar.physGrabber.ReleaseObject(); + playerAvatar.physGrabber.ReleaseObject(-1); playerAvatar.physGrabber.enabled = false; } } // Left hand pickup logic if (!mapTool.Active && Actions.Instance["MapGrabLeft"].WasPressedThisFrame() && - Utils.Collide(leftHandCollider, mapPickupCollider) && !PlayerController.instance.sprinting) + Utils.Collide(leftHandCollider, [mapPickupCollider, ..shoulderMapPickupColliders]) && + !PlayerController.instance.sprinting) if (mapTool.HideLerp >= 1) { mapTool.transform.parent.parent = leftHandTip; @@ -304,11 +333,11 @@ private void MapToolLogic() VRMapTool.instance.leftHanded = true; mapHeldLeftHand = true; flashlight.hideFlashlight = !headlampEnabled && !VRSession.IsLeftHanded; - + // Prevent picking up items while the map is opened if (VRSession.IsLeftHanded) { - playerAvatar.physGrabber.ReleaseObject(); + playerAvatar.physGrabber.ReleaseObject(-1); playerAvatar.physGrabber.enabled = false; } } @@ -347,7 +376,7 @@ private void WallClipLogic() else { // Not hit! - + Crosshair.instance.gameObject.SetActive(true); } } @@ -400,6 +429,26 @@ private void HeadLampLogic() headlampHovered = collided; } + + private void DetachArms() + { + armsDetached = true; + + leftArm.localScale = rightArm.localScale = Vector3.one * 0.3f; + leftHandCollider.transform.localScale = rightHandCollider.transform.localScale = Vector3.one * 3f; + + leftHandTip.localRotation = Quaternion.identity; + rightHandTip.localRotation = Quaternion.identity; + } + + private void AttachArms() + { + armsDetached = false; + + leftArm.localScale = rightArm.localScale = Vector3.one; + leftHandCollider.transform.localScale = rightHandCollider.transform.localScale = Vector3.one; + leftArm.localPosition = rightArm.localPosition = Vector3.zero; + } public void SetVisible(bool visible) { @@ -435,4 +484,12 @@ private void OnDominantHandChanged(object sender, EventArgs args) { UpdateDominantTransforms(); } + + private void OnDetachedArmsChanged(object sender, EventArgs args) + { + if (Plugin.Config.DetachedArms.Value) + DetachArms(); + else + AttachArms(); + } } \ No newline at end of file diff --git a/Source/Plugin.cs b/Source/Plugin.cs index b9b72d2..9f425ad 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -20,21 +20,22 @@ public class Plugin : BaseUnityPlugin { public const string PLUGIN_GUID = "io.daxcess.repoxr"; public const string PLUGIN_NAME = "RepoXR"; - public const string PLUGIN_VERSION = "1.0.2"; - - #if DEBUG + public const string PLUGIN_VERSION = "1.1.0"; + +#if DEBUG private const string SKIP_CHECKSUM_VAR = $"--repoxr-skip-checksum={PLUGIN_VERSION}-dev"; - #else +#else private const string SKIP_CHECKSUM_VAR = $"--repoxr-skip-checksum={PLUGIN_VERSION}"; - #endif +#endif - private const string HASHES_OVERRIDE_URL = "https://gist.githubusercontent.com/DaXcess/033e8ff514c505d2372e6f55a412dc00/raw/RepoXR%2520Game%2520Hashes"; + private const string HASHES_OVERRIDE_URL = + "https://gist.githubusercontent.com/DaXcess/033e8ff514c505d2372e6f55a412dc00/raw/RepoXR%2520Game%2520Hashes"; private readonly string[] GAME_ASSEMBLY_HASHES = [ - "E95BEFC4BD5206D9455BAA68C51A950ABAF0A99001012928B3E553D8D0E5CDB3" // v0.2.1 + "137D6E8475DEA976831CC95D7F56F4B7DA311E52A57B4C420591A5122F25589F" // v0.3.0 ]; - + public new static Config Config { get; private set; } = null!; public static Flags Flags { get; private set; } = 0; @@ -71,7 +72,7 @@ private void Awake() { Logger.LogError("Error: Unsupported game version, or corrupted game detected!"); Logger.LogError("RepoXR only supports legitimate Steam copies of R.E.P.O."); - Logger.LogError("R.E.P.O. might have been updated recently, which will also trigger this error."); + Logger.LogError("R.E.P.O. might have been updated recently, and RepoXR does not yet support this version."); Logger.LogDebug( $"To bypass this check, add the following flag to your launch options in Steam: {SKIP_CHECKSUM_VAR}"); @@ -105,14 +106,17 @@ private void Awake() } #endif + if (Environment.GetCommandLineArgs().Contains("--repoxr-debug-eyetracking", StringComparer.OrdinalIgnoreCase)) + Flags |= Flags.EyeTrackingDebug; + Native.BringGameWindowToFront(); Config.SetupGlobalCallbacks(); - SceneManager.sceneLoaded += (scene, _) => Entrypoint.OnSceneLoad(scene.name); + SceneManager.sceneLoaded += (scene, _) => UniversalEntrypoint.OnSceneLoad(scene.name); } public static string GetCommitHash() - { + { try { var attribute = Assembly.GetExecutingAssembly().GetCustomAttribute(); @@ -131,7 +135,7 @@ private bool VerifyGameVersion() { var location = Path.Combine(Paths.ManagedPath, "Assembly-CSharp.dll"); var hash = BitConverter.ToString(Utils.ComputeHash(File.ReadAllBytes(location))).Replace("-", ""); - + // Attempt local lookup first if (GAME_ASSEMBLY_HASHES.Contains(hash)) { @@ -139,9 +143,9 @@ private bool VerifyGameVersion() return true; } - + Logger.LogWarning("Failed to verify game version using local hashes, checking remotely for updated hashes..."); - + // Attempt to fetch a gist with known working assembly hashes // This allows me to keep RepoXR up and running if the game updates, without having to push an update out try @@ -150,7 +154,7 @@ private bool VerifyGameVersion() var hashes = Utils.ParseConfig(contents); var found = false; - + foreach (var versionedHash in hashes) { try @@ -194,10 +198,6 @@ private bool PreloadRuntimeDependencies() { var filename = Path.GetFileName(file); - // Ignore known unmanaged libraries - if (filename is "UnityOpenXR.dll" or "openxr_loader.dll") - continue; - try { Assembly.LoadFile(file); @@ -210,14 +210,33 @@ private bool PreloadRuntimeDependencies() } catch (Exception ex) { - Logger.LogError($"Unexpected error occured while preloading runtime dependencies (incorrect folder structure?): {ex.Message}"); - + Logger.LogError( + $"Unexpected error occured while preloading runtime dependencies (incorrect folder structure?): {ex.Message}"); + return false; } return true; } + public static void ToggleVR() + { + if (Flags.HasFlag(Flags.VR)) + { + OpenXR.Loader.DeinitializeXR(); + HarmonyPatcher.UnpatchVR(); + + Flags &= ~Flags.VR; + } + else + { + if (!InitializeVR()) + return; + + Flags |= Flags.VR; + } + } + private static bool InitializeVR() { RepoXR.Logger.LogInfo("Loading VR..."); @@ -231,21 +250,23 @@ private static bool InitializeVR() return false; } - - if (OpenXR.GetActiveRuntimeName(out var name) && OpenXR.GetActiveRuntimeVersion(out var major, out var minor, out var patch)) + + if (OpenXR.GetActiveRuntimeName(out var name) && + OpenXR.GetActiveRuntimeVersion(out var major, out var minor, out var patch)) RepoXR.Logger.LogInfo($"OpenXR runtime being used: {name} ({major}.{minor}.{patch})"); else RepoXR.Logger.LogError("Could not get OpenXR runtime info?"); HarmonyPatcher.PatchVR(); - + RepoXR.Logger.LogDebug("Inserted VR patches using Harmony"); - + // Change render pipeline settings if needed XRSettings.eyeTextureResolutionScale = Config.CameraResolution.Value / 100f; - + // Input settings - InputSystem.settings.backgroundBehavior = InputSettings.BackgroundBehavior.IgnoreFocus; // Prevent VR from getting disabled when losing focus + InputSystem.settings.backgroundBehavior = + InputSettings.BackgroundBehavior.IgnoreFocus; // Prevent VR from getting disabled when losing focus return true; } @@ -262,5 +283,6 @@ public static MethodInfo GetConfigGetter() public enum Flags { VR = 1 << 0, - StartupFailed = 1 << 1 + StartupFailed = 1 << 1, + EyeTrackingDebug = 1 << 2 } \ No newline at end of file diff --git a/Source/Rendering/CustomPostProcessing.cs b/Source/Rendering/CustomPostProcessing.cs index 69acd51..33e9d4c 100644 --- a/Source/Rendering/CustomPostProcessing.cs +++ b/Source/Rendering/CustomPostProcessing.cs @@ -1,4 +1,5 @@ using UnityEngine; +using UnityEngine.Rendering.PostProcessing; namespace RepoXR.Rendering; @@ -20,8 +21,11 @@ private void Start() postProcessing.volume.profile.AddSettings(vignette); - // Disable replaced shaders + // Disable original shaders postProcessing.vignette.enabled.value = false; + + // Disable ambient occlusion (big performance boost) + postProcessing.GetComponent().profile.GetSetting().enabled.value = false; } private void Update() diff --git a/Source/UI/ChatUI.cs b/Source/UI/ChatUI.cs index 727a057..843d805 100644 --- a/Source/UI/ChatUI.cs +++ b/Source/UI/ChatUI.cs @@ -71,7 +71,7 @@ private void Update() if (chatManager.chatState == ChatManager.ChatState.Active) { - PhysGrabber.instance.ReleaseObject(); // Drop items while chat is active + PhysGrabber.instance.ReleaseObject(-1); // Drop items while chat is active if (!keyboard.gameObject.activeSelf) keyboard.PresentKeyboard(); diff --git a/Source/UI/Controls/ControlOption.cs b/Source/UI/Controls/ControlOption.cs index 7918cac..0a93874 100644 --- a/Source/UI/Controls/ControlOption.cs +++ b/Source/UI/Controls/ControlOption.cs @@ -22,7 +22,7 @@ public class ControlOption : MonoBehaviour private void Awake() { - playerInput = VRInputSystem.instance.GetPlayerInput(); + playerInput = VRInputSystem.Instance.GetPlayerInput(); } public void StartRebind() @@ -30,9 +30,21 @@ public void StartRebind() manager.StartRebind(this, bindingIndex); } + public void DeleteBinding() + { + // Don't allow to unbind while no controller scheme is known + if (string.IsNullOrEmpty(playerInput.currentControlScheme)) + return; + + action.ApplyBindingOverride(bindingIndex, ""); + + ReloadBinding(); + manager.SaveBindings(); + } + public void SetBindToggle(bool toggle) { - VRInputSystem.instance.InputToggleRebind(action!.name, toggle); + VRInputSystem.Instance.InputToggleRebind(action!.name, toggle); } public void FetchToggle() @@ -41,7 +53,7 @@ public void FetchToggle() return; var menuTwoOptions = GetComponentInChildren(); - menuTwoOptions.startSettingFetch = VRInputSystem.instance.InputToggleGet(action.name); + menuTwoOptions.startSettingFetch = VRInputSystem.Instance.InputToggleGet(action.name); } public void Setup(RebindManager rebindManager, RemappableControl remappableControl) diff --git a/Source/UI/Controls/VRMenuKeybindToggle.cs b/Source/UI/Controls/VRMenuKeybindToggle.cs index 2727aaa..bac1ffc 100644 --- a/Source/UI/Controls/VRMenuKeybindToggle.cs +++ b/Source/UI/Controls/VRMenuKeybindToggle.cs @@ -9,16 +9,16 @@ public class VRMenuKeybindToggle: MonoBehaviour public void EnableToggle() { - VRInputSystem.instance.InputToggleRebind(inputAction, true); + VRInputSystem.Instance.InputToggleRebind(inputAction, true); } public void DisableToggle() { - VRInputSystem.instance.InputToggleRebind(inputAction, false); + VRInputSystem.Instance.InputToggleRebind(inputAction, false); } public void FetchSetting() { - GetComponent().startSettingFetch = VRInputSystem.instance.InputToggleGet(inputAction); + GetComponent().startSettingFetch = VRInputSystem.Instance.InputToggleGet(inputAction); } } \ No newline at end of file diff --git a/Source/UI/Expressions/ExpressionRadial.cs b/Source/UI/Expressions/ExpressionRadial.cs index 0f898cf..46b21aa 100644 --- a/Source/UI/Expressions/ExpressionRadial.cs +++ b/Source/UI/Expressions/ExpressionRadial.cs @@ -62,7 +62,7 @@ private void Update() if (closedLastPress) closedLastPress = false; - var pressed = VRInputSystem.instance.ExpressionPressed() || + var pressed = VRInputSystem.Instance.ExpressionPressed() || // Close radial menu when chat becomes active (isActive && SemiFunc.InputDown(InputKey.Chat)); switch (pressed) @@ -151,7 +151,7 @@ private void ReloadParts() private void UpdateBindingHand() { var chatAction = Actions.Instance["Chat"]; - var bindingIndex = Mathf.Max(chatAction.GetBindingIndex(VRInputSystem.instance.CurrentControlScheme), 0); + var bindingIndex = Mathf.Max(chatAction.GetBindingIndex(VRInputSystem.Instance.CurrentControlScheme), 0); var bindingPath = Actions.Instance["Chat"].bindings[bindingIndex].effectivePath; if (!Utils.GetControlHand(bindingPath, out var hand)) diff --git a/Source/UI/FocusSphere.cs b/Source/UI/FocusSphere.cs new file mode 100644 index 0000000..0c4bb45 --- /dev/null +++ b/Source/UI/FocusSphere.cs @@ -0,0 +1,65 @@ +using UnityEngine; + +namespace RepoXR.UI; + +public class FocusSphere : MonoBehaviour +{ + private static readonly int FadeStart = Shader.PropertyToID("_FadeStart"); + private static readonly int FadeEnd = Shader.PropertyToID("_FadeEnd"); + + [SerializeField] protected Renderer renderer; + [SerializeField] protected AnimationCurve animIn; + [SerializeField] protected AnimationCurve animOut; + + private Transform camera; + + private Transform? lookAtTarget; + private float lookAtTimer; + private float lookAtSpeed; + private float lookAtStrength; + + private float strengthLerp; + + private void Awake() + { + renderer.material.SetFloat(FadeStart, 0f); + renderer.material.SetFloat(FadeEnd, 0f); + + camera = Camera.main!.transform; + } + + private void Update() + { + transform.localPosition = camera.localPosition; + + var hasTarget = lookAtTarget != null; + var visible = hasTarget && lookAtTimer > 0; + + strengthLerp = Mathf.Clamp01(strengthLerp + (visible ? lookAtSpeed : -lookAtSpeed) * Time.deltaTime); + + if (lookAtTimer > 0) + { + lookAtTimer = Mathf.Max(0, lookAtTimer - Time.deltaTime); + + renderer.material.SetFloat(FadeStart, animIn.Evaluate(strengthLerp) * lookAtStrength * 0.92f); + renderer.material.SetFloat(FadeEnd, animIn.Evaluate(strengthLerp) * lookAtStrength); + } else if (strengthLerp > 0) + { + renderer.material.SetFloat(FadeStart, animOut.Evaluate(strengthLerp) * lookAtStrength * 0.92f); + renderer.material.SetFloat(FadeEnd, animOut.Evaluate(strengthLerp) * lookAtStrength); + } + + if (hasTarget && strengthLerp > 0) + transform.LookAt(lookAtTarget); + } + + public void SetLookAtTarget(Transform target, float time, float speed, float strength) + { + strength = Mathf.Clamp01(strength); + + lookAtTarget = target; + lookAtTimer = time; + lookAtSpeed = speed; + lookAtStrength = strength; + } +} \ No newline at end of file diff --git a/Source/UI/GameHud.cs b/Source/UI/GameHud.cs index 19cd746..5cf8831 100644 --- a/Source/UI/GameHud.cs +++ b/Source/UI/GameHud.cs @@ -49,7 +49,7 @@ private void LateUpdate() ? SpectateCamera.instance.transform.up : Vector3.up; - var fwd = camera.position + camera.forward * Plugin.Config.SmoothCanvasDistance.Value; + var fwd = camera.position + Vector3.down * 0.15f + camera.forward * Plugin.Config.SmoothCanvasDistance.Value; var rot = Quaternion.LookRotation(camera.forward, up); SmoothedCanvas.transform.position = Vector3.Slerp(SmoothedCanvas.transform.position, fwd, 0.1f); @@ -145,9 +145,11 @@ private void SetupSmoothedCanvas() // Dump all the game hud onto this smoothed canvas var gameHud = HUDCanvas.instance.transform.Find("HUD/Game Hud"); var chatLocal = HUDCanvas.instance.transform.Find("HUD/Chat Local"); - + var debugConsole = GameObject.Find("UI/UI/Canvas/DebugConsole")?.transform; + gameHud.SetParent(rect, false); chatLocal.SetParent(rect, false); + debugConsole?.SetParent(rect, false); } /// @@ -161,7 +163,8 @@ private void SetupPauseMenu() { parent = camera.transform.parent.parent, localPosition = Vector3.down * 3000, // Move very far away initially to prevent UI flashes during loading - localRotation = Quaternion.identity, localScale = Vector3.one * 0.01f + localRotation = Quaternion.identity, + localScale = Vector3.one * 0.01f }, layer = 5 }.AddComponent(); diff --git a/Source/UI/LoadingUI.cs b/Source/UI/LoadingUI.cs index 3df7baf..11f149c 100644 --- a/Source/UI/LoadingUI.cs +++ b/Source/UI/LoadingUI.cs @@ -1,8 +1,5 @@ using System.Collections; -using HarmonyLib; -using RepoXR.Patches; using UnityEngine; -using Object = UnityEngine.Object; namespace RepoXR.UI; @@ -62,15 +59,4 @@ private void RestorePosition() transform.localPosition = lastLocalPosition; transform.localEulerAngles = lastLocalRotation * Vector3.up; } -} - -[RepoXRPatch] -internal static class LoadingUIPatches -{ - [HarmonyPatch(typeof(global::LoadingUI), nameof(global::LoadingUI.StartLoading))] - [HarmonyPostfix] - private static void OnStartLoading() - { - Object.FindObjectOfType().ResetPosition(); - } } \ No newline at end of file diff --git a/Source/UI/MainMenu.cs b/Source/UI/MainMenu.cs index 546f1c4..45254ee 100644 --- a/Source/UI/MainMenu.cs +++ b/Source/UI/MainMenu.cs @@ -112,6 +112,6 @@ private static IEnumerator LobbyLinkCopy() private static void OnResetHeight(InputAction.CallbackContext obj) { - VRCameraAim.instance.SetSpawnRotation(0); + VRCameraAim.instance.SetPlayerAim(0, true); } } \ No newline at end of file diff --git a/Source/UI/PauseUI.cs b/Source/UI/PauseUI.cs index f17f010..bf846e9 100644 --- a/Source/UI/PauseUI.cs +++ b/Source/UI/PauseUI.cs @@ -7,9 +7,9 @@ namespace RepoXR.UI; public class PauseUI : MonoBehaviour { public static PauseUI? instance; - + public Vector3 positionOffset; - + private Vector3 targetPos; private Quaternion targetRot; @@ -18,7 +18,7 @@ public class PauseUI : MonoBehaviour private bool isOpen; private float darkness; - + private void Awake() { instance = this; @@ -75,7 +75,7 @@ public void Show() { isOpen = true; ResetPosition(true); - + interactor.SetVisible(true); } @@ -109,12 +109,12 @@ public void ResetPosition(bool instant = false) transform.localRotation = targetRot; } } - + private void OnResetHeight(InputAction.CallbackContext ctx) { if (!ctx.performed || !isOpen) return; - + ResetPosition(); } } \ No newline at end of file diff --git a/Source/UI/ValuableDiscoverGraphic.cs b/Source/UI/ValuableDiscoverGraphic.cs new file mode 100644 index 0000000..21f22ca --- /dev/null +++ b/Source/UI/ValuableDiscoverGraphic.cs @@ -0,0 +1,129 @@ +using RepoXR.Player; +using UnityEngine; + +namespace RepoXR.UI; + +// A rewrite of the original ValuableDiscoverGraphic that makes use of 3d models instead of a canvas renderer + +public class ValuableDiscoverGraphic : MonoBehaviour +{ + private static readonly int BaseColor = Shader.PropertyToID("_BaseColor"); + private static readonly int EdgeColor = Shader.PropertyToID("_EdgeColor"); + + public Renderer renderer; + + private global::ValuableDiscoverGraphic baseGraphic; + + internal PhysGrabObject target; + + private global::ValuableDiscoverGraphic.State state; + private bool discovered; + private float waitTimer; + private float animLerp; + + private Vector3 targetCenter; + private Vector3 targetSize; + + private void Awake() + { + baseGraphic = ValuableDiscover.instance.graphicPrefab.GetComponent(); + } + + private void Start() + { + transform.localScale = Vector3.zero; + + waitTimer = state switch + { + global::ValuableDiscoverGraphic.State.Reminder => 0.5f, + global::ValuableDiscoverGraphic.State.Bad => 3, + _ => 1 + }; + } + + private void Update() + { + if (target) + { + var bounds = new Bounds(target.centerPoint, Vector3.zero); + foreach (var meshRenderer in target.GetComponentsInChildren()) + bounds.Encapsulate(meshRenderer.bounds); + + bounds.Expand(0.05f); + + targetCenter = bounds.center; + targetSize = bounds.size; + + var lookingAt = VREyeTracking.LookingAt(bounds.center, 0.5f, 0.5f); + if (lookingAt && !discovered) + { + if (state == global::ValuableDiscoverGraphic.State.Reminder) + baseGraphic.sound.Play(target.centerPoint, 0.3f); + else + baseGraphic.sound.Play(target.centerPoint); + + renderer.enabled = true; + discovered = true; + } + } + else + waitTimer = 0; + + transform.position = targetCenter; + + if (waitTimer > 0) + { + animLerp = Mathf.Clamp01(animLerp + baseGraphic.introSpeed * Time.deltaTime); + transform.localScale = + Vector3.LerpUnclamped(Vector3.zero, targetSize, baseGraphic.introCurve.Evaluate(animLerp)); + + if (animLerp < 1) + return; + + waitTimer -= Time.deltaTime; + + // Wait timer has ended, outro will now start (so reset animLerp) + if (waitTimer <= 0) + animLerp = 0; + } + else + { + animLerp = Mathf.Clamp01(animLerp + baseGraphic.outroSpeed * Time.deltaTime); + transform.localScale = + Vector3.LerpUnclamped(targetSize, Vector3.zero, baseGraphic.outroCurve.Evaluate(animLerp)); + + if (animLerp >= 1) + Destroy(gameObject); + } + } + + public void ReminderSetup() + { + state = global::ValuableDiscoverGraphic.State.Reminder; + + var baseColor = baseGraphic.ColorReminderMiddle; + var edgeColor = baseGraphic.ColorReminderCorner; + + renderer.material.SetColor(BaseColor, baseColor); + renderer.material.SetColor(EdgeColor, edgeColor); + } + + public void BadSetup() + { + state = global::ValuableDiscoverGraphic.State.Bad; + + var baseColor = baseGraphic.ColorBadMiddle; + var edgeColor = baseGraphic.ColorBadCorner; + + renderer.material.SetColor(BaseColor, baseColor); + renderer.material.SetColor(EdgeColor, edgeColor); + } + + public void CustomSetup(global::ValuableDiscoverGraphic.State newState, Color baseColor, Color edgeColor) + { + state = newState; + + renderer.material.SetColor(BaseColor, baseColor); + renderer.material.SetColor(EdgeColor, edgeColor); + } +} \ No newline at end of file diff --git a/Source/Utils.cs b/Source/Utils.cs index f3bfc68..42707fd 100644 --- a/Source/Utils.cs +++ b/Source/Utils.cs @@ -139,10 +139,11 @@ public static bool GetControlHand(string controlPath, out HapticManager.Hand han } } - public static bool Collide(Collider lhs, Collider rhs) + public static bool Collide(Collider lhs, params Collider[] rhs) { - return Physics.ComputePenetration(lhs, lhs.transform.position, lhs.transform.rotation, rhs, - rhs.transform.position, rhs.transform.rotation, out _, out _); + return rhs.Any(collider => Physics.ComputePenetration(lhs, lhs.transform.position, lhs.transform.rotation, + collider, + collider.transform.position, collider.transform.rotation, out _, out _)); } public static void DisableScanlines(this SemiUI ui)