From 97a86228df2aba149003c3c843b65780b8a3f7a0 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 8 Sep 2025 15:39:57 +0200 Subject: [PATCH 01/20] Initial 1.1.0 commit --- CHANGELOG.md | 12 ++++++++++++ Preload/RepoXR.Preload.csproj | 2 +- RepoXR.csproj | 2 +- Source/OpenXR.cs | 5 ++++- Source/Plugin.cs | 2 +- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 647df8c..22c6368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 1.1.0 + +**Additions**: +- Added eye tracking support (for the three people that have it) +- Added an option to detach arms from body +- Added an overhauled spectating system +- Added support for the new climbing mechanic +- Added support for the monster update + +**Removals**: +- Removed support for REPO v0.2.1 + # 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/RepoXR.csproj b/RepoXR.csproj index 37d45e9..75d85ef 100644 --- a/RepoXR.csproj +++ b/RepoXR.csproj @@ -2,7 +2,7 @@ Collecting Valuables in VR - 1.0.3 + 1.1.0 DaXcess true latest diff --git a/Source/OpenXR.cs b/Source/OpenXR.cs index 9acc39d..636b629 100644 --- a/Source/OpenXR.cs +++ b/Source/OpenXR.cs @@ -432,6 +432,7 @@ private static void InitializeScripts() var khrSimple = ScriptableObject.CreateInstance(); var metaQuestTouch = ScriptableObject.CreateInstance(); var oculusTouch = ScriptableObject.CreateInstance(); + var eyeTracking = ScriptableObject.CreateInstance(); valveIndex.enabled = true; hpReverb.enabled = true; @@ -440,6 +441,7 @@ private static void InitializeScripts() khrSimple.enabled = true; metaQuestTouch.enabled = true; oculusTouch.enabled = true; + eyeTracking.enabled = true; OpenXRSettings.Instance.features = [ @@ -449,7 +451,8 @@ private static void InitializeScripts() mmController, khrSimple, metaQuestTouch, - oculusTouch + oculusTouch, + eyeTracking ]; } } diff --git a/Source/Plugin.cs b/Source/Plugin.cs index b9b72d2..819c0d7 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -20,7 +20,7 @@ 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"; + public const string PLUGIN_VERSION = "1.1.0"; #if DEBUG private const string SKIP_CHECKSUM_VAR = $"--repoxr-skip-checksum={PLUGIN_VERSION}-dev"; From bcd4ae589b42dd677618d1cbbf929b02075ed0bc Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 8 Sep 2025 15:42:21 +0200 Subject: [PATCH 02/20] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 85be51a..474cb85 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 | v.0.3.0 | | v1.0.3 | v0.2.1 | | v1.0.2 | v0.2.1 | | v1.0.1 | v0.2.1 | From 540216b9d40cfb041af0dc108c587fcd5d40a971 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 8 Sep 2025 15:42:44 +0200 Subject: [PATCH 03/20] Fuck --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 474cb85..7093afa 100644 --- a/README.md +++ b/README.md @@ -38,7 +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 | v.0.3.0 | +| v1.1.0 | v0.3.0 | | v1.0.3 | v0.2.1 | | v1.0.2 | v0.2.1 | | v1.0.1 | v0.2.1 | From 4492147229c1d398a0a3bc9e10a5ca91a9d31443 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 8 Sep 2025 16:13:35 +0200 Subject: [PATCH 04/20] Balls to the walls --- Source/Input/Actions.cs | 8 ++++++ Source/Patches/Player/PlayerEyesPatches.cs | 33 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 Source/Patches/Player/PlayerEyesPatches.cs diff --git a/Source/Input/Actions.cs b/Source/Input/Actions.cs index e7fe355..7063c8a 100644 --- a/Source/Input/Actions.cs +++ b/Source/Input/Actions.cs @@ -18,6 +18,10 @@ 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; } + public InputAction EyeGazeTracked { get; private set; } private Actions() { @@ -32,6 +36,10 @@ 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"); + EyeGazeTracked = AssetCollection.DefaultXRActions.FindAction("Eye Gaze/Is Tracked"); AssetCollection.DefaultXRActions.Enable(); } diff --git a/Source/Patches/Player/PlayerEyesPatches.cs b/Source/Patches/Player/PlayerEyesPatches.cs new file mode 100644 index 0000000..7e9609f --- /dev/null +++ b/Source/Patches/Player/PlayerEyesPatches.cs @@ -0,0 +1,33 @@ +using HarmonyLib; +using RepoXR.Input; +using UnityEngine; + +namespace RepoXR.Patches.Player; + +[RepoXRPatch] +internal static class PlayerEyesPatches +{ + [HarmonyPatch(typeof(PlayerEyes), nameof(PlayerEyes.LookAtTransform))] + [HarmonyPostfix] + private static void LookAtTransformEyeTracking(PlayerEyes __instance) + { + // TODO: Use synced values here if the remote player has eye tracking + if (!__instance.playerAvatar.isLocal) + return; + + // TODO: First check if setting is disabled, once setting is added + if (!Actions.Instance.EyeGazeTracked.IsPressed()) + return; + + // TODO: Determine if certain occurrences in the game should override eye gaze + // TODO: Network sync this stuff as well somehow + + var gazePosition = Actions.Instance.EyeGazePosition.ReadValue(); + var gazeRotation = Actions.Instance.EyeGazeRotation.ReadValue(); + + // Some magic raycasting bs here + + __instance.lookAtActive = true; + __instance.lookAt.transform.position = Vector3.zero; /* raycast using gazePosition and gazeRotation */ + } +} \ No newline at end of file From 5d0d77c78ce18a957ded662fe5e688eadae28982 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 22 Sep 2025 17:38:56 +0200 Subject: [PATCH 05/20] VR eye tracking teehee --- Source/Assets/AssetCollection.cs | 4 + Source/Networking/Frames/EyeGaze.cs | 20 ++++ Source/Networking/Frames/Frame.cs | 1 + Source/Networking/NetworkPlayer.cs | 10 ++ Source/Networking/NetworkSystem.cs | 19 ++++ Source/Patches/Player/PlayerEyesPatches.cs | 22 ++--- Source/Player/VREyeTracking.cs | 101 +++++++++++++++++++++ Source/Player/VRPlayer.cs | 5 + Source/Player/VRRig.cs | 2 +- 9 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 Source/Networking/Frames/EyeGaze.cs create mode 100644 Source/Player/VREyeTracking.cs diff --git a/Source/Assets/AssetCollection.cs b/Source/Assets/AssetCollection.cs index 45d300c..89ce867 100644 --- a/Source/Assets/AssetCollection.cs +++ b/Source/Assets/AssetCollection.cs @@ -47,6 +47,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() { @@ -96,6 +98,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/Networking/Frames/EyeGaze.cs b/Source/Networking/Frames/EyeGaze.cs new file mode 100644 index 0000000..bd07dfe --- /dev/null +++ b/Source/Networking/Frames/EyeGaze.cs @@ -0,0 +1,20 @@ +using Photon.Pun; +using UnityEngine; + +namespace RepoXR.Networking.Frames; + +[Frame(FrameHelper.FrameDominantHand)] +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..21f3ce2 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,10 @@ public void UpdateDominantHand(bool leftHanded) playerRightArm.physGrabBeam.PhysGrabPointOrigin.SetParent(isLeftHanded ? leftHandAnchor : rightHandAnchor); playerRightArm.physGrabBeam.PhysGrabPointOrigin.localPosition = Vector3.zero; } + + public void UpdateEyeTracking(Vector3 gazePoint) + { + EyeTracking = true; + EyeGazePoint = gazePoint; + } } \ No newline at end of file diff --git a/Source/Networking/NetworkSystem.cs b/Source/Networking/NetworkSystem.cs index 096e1c2..b999c6c 100644 --- a/Source/Networking/NetworkSystem.cs +++ b/Source/Networking/NetworkSystem.cs @@ -92,6 +92,14 @@ public void UpdateDominantHand(bool leftHanded) }); } + public void UpdateEyeTracking(Vector3 gazePoint) + { + EnqueueFrame(new EyeGaze + { + GazePoint = gazePoint + }); + } + /// /// 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 +178,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) diff --git a/Source/Patches/Player/PlayerEyesPatches.cs b/Source/Patches/Player/PlayerEyesPatches.cs index 7e9609f..a3bc40c 100644 --- a/Source/Patches/Player/PlayerEyesPatches.cs +++ b/Source/Patches/Player/PlayerEyesPatches.cs @@ -1,5 +1,8 @@ using HarmonyLib; +using RepoXR.Assets; using RepoXR.Input; +using RepoXR.Managers; +using RepoXR.Networking; using UnityEngine; namespace RepoXR.Patches.Player; @@ -11,23 +14,16 @@ internal static class PlayerEyesPatches [HarmonyPostfix] private static void LookAtTransformEyeTracking(PlayerEyes __instance) { - // TODO: Use synced values here if the remote player has eye tracking - if (!__instance.playerAvatar.isLocal) - return; - - // TODO: First check if setting is disabled, once setting is added - if (!Actions.Instance.EyeGazeTracked.IsPressed()) + // TODO: Check if we can just do this with the local player + if (__instance.playerAvatar.isLocal) return; // TODO: Determine if certain occurrences in the game should override eye gaze - // TODO: Network sync this stuff as well somehow - var gazePosition = Actions.Instance.EyeGazePosition.ReadValue(); - var gazeRotation = Actions.Instance.EyeGazeRotation.ReadValue(); - - // Some magic raycasting bs here - + if (!NetworkSystem.instance.GetNetworkPlayer(__instance.playerAvatar, out var player) && player.EyeTracking) + return; + __instance.lookAtActive = true; - __instance.lookAt.transform.position = Vector3.zero; /* raycast using gazePosition and gazeRotation */ + __instance.lookAt.transform.position = player.EyeGazePoint; } } \ No newline at end of file diff --git a/Source/Player/VREyeTracking.cs b/Source/Player/VREyeTracking.cs new file mode 100644 index 0000000..97d6fcf --- /dev/null +++ b/Source/Player/VREyeTracking.cs @@ -0,0 +1,101 @@ +using RepoXR.Assets; +using RepoXR.Input; +using RepoXR.Networking; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace RepoXR.Player; + +public class VREyeTracking : MonoBehaviour +{ + private Transform debugCubeHmdRel; + private Transform debugCubeSpaceRel; + + public bool Supported { get; private set; } + public Vector3 FocusPosition => debugCubeHmdRel.position; + public Vector3 GazeDirection { get; private set; } + + private void Awake() + { + Actions.Instance.EyeGazeTracked.performed += OnEyeTrackingDetected; + + debugCubeHmdRel = Instantiate(AssetCollection.Cube).transform; + debugCubeSpaceRel = Instantiate(AssetCollection.Cube).transform; + + debugCubeHmdRel.GetComponent().material.color = Color.blue; + debugCubeSpaceRel.GetComponent().material.color = Color.red; + + debugCubeHmdRel.GetComponent().enabled = false; + debugCubeSpaceRel.GetComponent().enabled = false; + + debugCubeHmdRel.position = Vector3.down * 1000; + debugCubeSpaceRel.position = Vector3.down * 1000; + } + + private void OnDestroy() + { + Actions.Instance.EyeGazeTracked.performed -= OnEyeTrackingDetected; + } + + private void OnEyeTrackingDetected(InputAction.CallbackContext obj) + { + if (!obj.performed) + return; + + Supported = true; + } + + private void Update() + { + if (!Supported) + return; + + if (!!false) // TODO: If disabled by config + return; + + var gazePosition = Actions.Instance.EyeGazePosition.ReadValue(); + var gazeRotation = Actions.Instance.EyeGazeRotation.ReadValue(); + + UpdateHmdRelative(gazePosition, gazeRotation); + UpdateSpaceRelative(gazePosition, gazeRotation); + + // var ray = new Ray(transform.TransformPoint(gazePosition), + // transform.TransformDirection(gazeRotation * Vector3.forward)); + // + // GazeDirection = ray.direction; + // + // if (Physics.Raycast(ray, out var hit, 5, SemiFunc.LayerMaskGetShouldHits())) + // debugCubeHmdRel.transform.position = hit.point; + // else + // debugCubeHmdRel.transform.position = ray.origin + ray.direction * 5; + // + // NetworkSystem.instance.UpdateEyeTracking(debugCubeHmdRel.transform.position); + // + // debugCubeHmdRel.transform.rotation = Quaternion.LookRotation(ray.direction); + } + + private void UpdateHmdRelative(Vector3 position, Quaternion rotation) + { + var ray = new Ray(transform.TransformPoint(position), transform.TransformDirection(rotation * Vector3.forward)); + + if (Physics.Raycast(ray, out var hit, 5, SemiFunc.LayerMaskGetShouldHits())) + debugCubeHmdRel.transform.position = hit.point; + else + debugCubeHmdRel.transform.position = ray.origin + ray.direction * 5; + + debugCubeHmdRel.transform.rotation = Quaternion.LookRotation(ray.direction); + } + + private void UpdateSpaceRelative(Vector3 position, Quaternion rotation) + { + var ray = new Ray(transform.parent.TransformPoint(position), + transform.parent.TransformDirection(rotation * Vector3.forward)); + + if (Physics.Raycast(ray, out var hit, 5, SemiFunc.LayerMaskGetShouldHits())) + debugCubeSpaceRel.transform.position = hit.point; + else + debugCubeSpaceRel.transform.position = ray.origin + ray.direction * 5; + + debugCubeSpaceRel.transform.rotation = Quaternion.LookRotation(ray.direction); + } +} \ No newline at end of file diff --git a/Source/Player/VRPlayer.cs b/Source/Player/VRPlayer.cs index 7b3c3c0..8e6903a 100644 --- a/Source/Player/VRPlayer.cs +++ b/Source/Player/VRPlayer.cs @@ -25,12 +25,14 @@ public class VRPlayer : MonoBehaviour // 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; @@ -78,6 +80,9 @@ private void Awake() localRig.leftArmTarget = leftHand; localRig.rightArmTarget = rightHand; + // TODO: Eye tracking + eyeTracking = mainCamera.gameObject.AddComponent(); + Actions.Instance["ResetHeight"].performed += OnResetHeight; } diff --git a/Source/Player/VRRig.cs b/Source/Player/VRRig.cs index 5fb1520..5fc16ae 100644 --- a/Source/Player/VRRig.cs +++ b/Source/Player/VRRig.cs @@ -347,7 +347,7 @@ private void WallClipLogic() else { // Not hit! - + Crosshair.instance.gameObject.SetActive(true); } } From 252a87cc093b87a273133128becb6a3ee87dbeee Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 22 Sep 2025 17:42:39 +0200 Subject: [PATCH 06/20] Hotswap --- CHANGELOG.md | 1 + Source/Entrypoint.cs | 71 +++++++++++++++++++------------ Source/Managers/HapticManager.cs | 7 +++ Source/Managers/HotswapManager.cs | 47 ++++++++++++++++++++ Source/OpenXR.cs | 6 +++ Source/Patches/HarmonyPatcher.cs | 21 +++++++++ Source/Patches/InputPatches.cs | 25 ++++++----- Source/Plugin.cs | 20 ++++++++- 8 files changed, 159 insertions(+), 39 deletions(-) create mode 100644 Source/Managers/HotswapManager.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c6368..2b73a1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added an overhauled spectating system - Added support for the new climbing mechanic - Added support for the monster update +- Added hotswapping in the main menu (F8) **Removals**: - Removed support for REPO v0.2.1 diff --git a/Source/Entrypoint.cs b/Source/Entrypoint.cs index 6ac78ef..79f8283 100644 --- a/Source/Entrypoint.cs +++ b/Source/Entrypoint.cs @@ -16,18 +16,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 @@ -131,22 +123,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 /// @@ -205,11 +181,52 @@ private static void OnStartupInGame() { GameDirector.instance.gameObject.AddComponent(); } +} - private static void ShowVRFailedWarning() +[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) + 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 + } + + public static void ShowVRFailedWarning(bool force = false) { if (!Plugin.Flags.HasFlag(Flags.StartupFailed) || - hasShownErrorMessage || RunManager.instance.levelCurrent != RunManager.instance.levelMainMenu) + (hasShownErrorMessage && !force) || RunManager.instance.levelCurrent != RunManager.instance.levelMainMenu) return; hasShownErrorMessage = true; 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..9751955 --- /dev/null +++ b/Source/Managers/HotswapManager.cs @@ -0,0 +1,47 @@ +using UnityEngine; +using UnityEngine.InputSystem; + +namespace RepoXR.Managers; + +public class HotswapManager : MonoBehaviour +{ + private readonly InputAction swapAction = new(binding: "/F8"); + + private void Awake() + { + swapAction.performed += SwapActionPerformed; + } + + 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(); + + GameDirector.instance.OutroStart(); // Reload scene + } + + private static void HotswapEnableVR() + { + Plugin.ToggleVR(); + + if (VRSession.InVR) + GameDirector.instance.OutroStart(); // Reload scene + else + UniversalEntrypoint.ShowVRFailedWarning(true); + } +} \ No newline at end of file diff --git a/Source/OpenXR.cs b/Source/OpenXR.cs index 636b629..3ed2092 100644 --- a/Source/OpenXR.cs +++ b/Source/OpenXR.cs @@ -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) diff --git a/Source/Patches/HarmonyPatcher.cs b/Source/Patches/HarmonyPatcher.cs index 8b4d948..4a7c0ed 100644 --- a/Source/Patches/HarmonyPatcher.cs +++ b/Source/Patches/HarmonyPatcher.cs @@ -27,6 +27,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(); @@ -198,4 +203,20 @@ private static void Emit(this CecilILGenerator il, SRE.OpCode opcode, object ope method.Invoke(null, [il, opcode, operand]); } +} + +internal static class HarmonyLibPatches +{ + /// + /// Ironically, patching Harmony like this fixes some issues with *un*patching + /// + [HarmonyPatch(typeof(MethodBaseExtensions), nameof(MethodBaseExtensions.HasMethodBody))] + [HarmonyPostfix] + private static bool OnUnpatch(MethodBase member, ref bool __result) + { + if (!__result) + Logger.LogDebug($"OnUnpatch: {member.FullDescription()}"); + + return true; + } } \ No newline at end of file diff --git a/Source/Patches/InputPatches.cs b/Source/Patches/InputPatches.cs index 811bf26..93c240a 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 /// @@ -54,7 +57,7 @@ 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()]; @@ -70,7 +73,7 @@ private static bool GetMovement(InputManager __instance, ref Vector2 __result) return true; __result = Actions.Instance["Movement"].ReadValue(); - + return false; } @@ -152,7 +155,7 @@ private static bool KeyUp(InputManager __instance, ref InputKey key, ref bool __ return true; __result = __instance.GetAction(key).WasReleasedThisFrame(); - + return false; } @@ -185,7 +188,7 @@ private static bool KeyPullAndPush(ref float __result) __result = -pull; return false; } - + return false; } @@ -200,14 +203,14 @@ private static bool InputDisplayGet(InputManager __instance, InputKey _inputKey, if (action == null) { __result = "Unassigned"; - + return false; } var index = action.GetBindingIndex(VRInputSystem.instance.CurrentControlScheme); __result = __instance.InputDisplayGetString(action, index); - + return false; } @@ -220,7 +223,7 @@ private static bool InputDisplayGetString(InputAction action, int bindingIndex, { var binding = action.bindings[bindingIndex].effectivePath; __result = Utils.GetControlSpriteString(binding); - + return false; } @@ -260,7 +263,7 @@ private static bool NoUnderlinePatch(InputManager __instance, ref string __resul private static bool ResetVRControls() { RebindManager.Instance.ResetControls(); - + return false; } diff --git a/Source/Plugin.cs b/Source/Plugin.cs index 819c0d7..3626bee 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -108,7 +108,7 @@ private void Awake() Native.BringGameWindowToFront(); Config.SetupGlobalCallbacks(); - SceneManager.sceneLoaded += (scene, _) => Entrypoint.OnSceneLoad(scene.name); + SceneManager.sceneLoaded += (scene, _) => UniversalEntrypoint.OnSceneLoad(scene.name); } public static string GetCommitHash() @@ -218,6 +218,24 @@ private bool PreloadRuntimeDependencies() 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..."); From c464b3a1c064c441b696a98f5501cef4434ee6b1 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 22 Sep 2025 19:34:01 +0200 Subject: [PATCH 07/20] Fixed hotswapping --- Source/Entrypoint.cs | 7 ++++--- Source/Managers/HotswapManager.cs | 23 +++++++++++++++++++---- Source/Networking/Frames/EyeGaze.cs | 2 +- Source/Patches/HarmonyPatcher.cs | 10 +++++----- Source/Patches/UI/LoadingUIPatches.cs | 10 ++++++++++ Source/UI/LoadingUI.cs | 14 -------------- 6 files changed, 39 insertions(+), 27 deletions(-) diff --git a/Source/Entrypoint.cs b/Source/Entrypoint.cs index 79f8283..20c4baf 100644 --- a/Source/Entrypoint.cs +++ b/Source/Entrypoint.cs @@ -203,7 +203,8 @@ public static void OnSceneLoad(string _) [HarmonyPostfix] private static void OnStartup(GameDirector __instance) { - if (RunManager.instance.levelCurrent != RunManager.instance.levelMainMenu) + if (RunManager.instance.levelCurrent != RunManager.instance.levelMainMenu && + RunManager.instance.levelCurrent != RunManager.instance.levelLobbyMenu) return; new GameObject("VR Hotswapper").AddComponent(); @@ -223,10 +224,10 @@ private static void SetupDefaultSceneUniversal() #endif } - public static void ShowVRFailedWarning(bool force = false) + private static void ShowVRFailedWarning() { if (!Plugin.Flags.HasFlag(Flags.StartupFailed) || - (hasShownErrorMessage && !force) || RunManager.instance.levelCurrent != RunManager.instance.levelMainMenu) + hasShownErrorMessage || RunManager.instance.levelCurrent != RunManager.instance.levelMainMenu) return; hasShownErrorMessage = true; diff --git a/Source/Managers/HotswapManager.cs b/Source/Managers/HotswapManager.cs index 9751955..688ea67 100644 --- a/Source/Managers/HotswapManager.cs +++ b/Source/Managers/HotswapManager.cs @@ -1,5 +1,7 @@ +using Photon.Pun; using UnityEngine; using UnityEngine.InputSystem; +using UnityEngine.SceneManagement; namespace RepoXR.Managers; @@ -10,6 +12,7 @@ public class HotswapManager : MonoBehaviour private void Awake() { swapAction.performed += SwapActionPerformed; + swapAction.Enable(); } private void OnDestroy() @@ -31,8 +34,8 @@ private static void SwapActionPerformed(InputAction.CallbackContext context) private static void HotswapDisableVR() { Plugin.ToggleVR(); - - GameDirector.instance.OutroStart(); // Reload scene + + RestartScene(); } private static void HotswapEnableVR() @@ -40,8 +43,20 @@ private static void HotswapEnableVR() Plugin.ToggleVR(); if (VRSession.InVR) - GameDirector.instance.OutroStart(); // Reload scene + RestartScene(); + else + MenuManager.instance.PagePopUp("VR Startup Failed", Color.red, + "RepoXR tried to launch the game in VR, however an error occured during initialization.\n\nYou can disable VR in the settings if you are not planning to play in VR.", + "Alright fam", + 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 - UniversalEntrypoint.ShowVRFailedWarning(true); + RunManager.instance.RestartScene(); } } \ No newline at end of file diff --git a/Source/Networking/Frames/EyeGaze.cs b/Source/Networking/Frames/EyeGaze.cs index bd07dfe..cfa02da 100644 --- a/Source/Networking/Frames/EyeGaze.cs +++ b/Source/Networking/Frames/EyeGaze.cs @@ -3,7 +3,7 @@ namespace RepoXR.Networking.Frames; -[Frame(FrameHelper.FrameDominantHand)] +[Frame(FrameHelper.FrameEyeGaze)] public class EyeGaze : IFrame { public Vector3 GazePoint; diff --git a/Source/Patches/HarmonyPatcher.cs b/Source/Patches/HarmonyPatcher.cs index 4a7c0ed..9d8798d 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; @@ -205,6 +206,7 @@ private static void Emit(this CecilILGenerator il, SRE.OpCode opcode, object ope } } +[RepoXRPatch(RepoXRPatchTarget.Universal)] internal static class HarmonyLibPatches { /// @@ -212,11 +214,9 @@ internal static class HarmonyLibPatches /// [HarmonyPatch(typeof(MethodBaseExtensions), nameof(MethodBaseExtensions.HasMethodBody))] [HarmonyPostfix] - private static bool OnUnpatch(MethodBase member, ref bool __result) + private static void OnUnpatch(MethodBase member, ref bool __result) { - if (!__result) - Logger.LogDebug($"OnUnpatch: {member.FullDescription()}"); - - return true; + if (new StackTrace().GetFrame(2)?.GetMethod().Name == "UnpatchConditional") + __result = true; } } \ No newline at end of file diff --git a/Source/Patches/UI/LoadingUIPatches.cs b/Source/Patches/UI/LoadingUIPatches.cs index 35a635a..5f766a5 100644 --- a/Source/Patches/UI/LoadingUIPatches.cs +++ b/Source/Patches/UI/LoadingUIPatches.cs @@ -8,6 +8,16 @@ namespace RepoXR.Patches.UI; [RepoXRPatch] internal static class LoadingUIPatches { + /// + /// 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/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 From 67e23faef5ea6c5c94447de2a5f8ebec286eefc5 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Tue, 23 Sep 2025 10:18:36 +0200 Subject: [PATCH 08/20] Messarge --- Source/Managers/HotswapManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Managers/HotswapManager.cs b/Source/Managers/HotswapManager.cs index 688ea67..b53d2b6 100644 --- a/Source/Managers/HotswapManager.cs +++ b/Source/Managers/HotswapManager.cs @@ -46,8 +46,8 @@ private static void HotswapEnableVR() RestartScene(); else MenuManager.instance.PagePopUp("VR Startup Failed", Color.red, - "RepoXR tried to launch the game in VR, however an error occured during initialization.\n\nYou can disable VR in the settings if you are not planning to play in VR.", - "Alright fam", + "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); } From 9a32e02c179e00f6aba40147542c85cac557ba3f Mon Sep 17 00:00:00 2001 From: DaXcess Date: Tue, 23 Sep 2025 12:56:25 +0200 Subject: [PATCH 09/20] Cnoncnfig --- RepoXR.csproj | 2 +- Source/Config.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/RepoXR.csproj b/RepoXR.csproj index 75d85ef..f9398e5 100644 --- a/RepoXR.csproj +++ b/RepoXR.csproj @@ -28,7 +28,7 @@ - + diff --git a/Source/Config.cs b/Source/Config.cs index 39ef093..783fe41 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -45,6 +45,14 @@ public class Config(string assemblyPath, ConfigFile file) "Controls how much haptic feedback you will experience while playing with the VR mod.", new AcceptableValueEnum())); + [ConfigDescriptor(falseText: "Disabled", trueText: "Enabled")] + public ConfigEntry EyeTracking { get; } = file.Bind("Gameplay", nameof(EyeTracking), true, + "Whether eye tracking should be used for certain gameplay elements (if supported by the hardware)"); + + [ConfigDescriptor(falseText: "Attached", trueText: "Separated")] + public ConfigEntry ArmsAttached { get; } = file.Bind("Gameplay", nameof(ArmsAttached), true, + "Whether locally the arms should be attached to your body, or they should be free floating \"hands\""); + [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))); From cf553ebdeee21c00d9cf47ce1a9af6532b7fca99 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 6 Oct 2025 19:02:17 +0200 Subject: [PATCH 10/20] Finish eye tracking and fix hotswapping again --- CHANGELOG.md | 22 +++- Source/Assets/AssetCollection.cs | 4 + Source/Config.cs | 4 + Source/Data/OpenXRFeaturePack.cs | 12 ++ Source/Entrypoint.cs | 2 +- Source/Experiments.cs | 52 +++++--- Source/Input/Actions.cs | 6 +- Source/Input/RebindManager.cs | 2 +- Source/Input/TrackingInput.cs | 7 +- Source/Input/VRInputSystem.cs | 5 +- Source/Managers/HotswapManager.cs | 5 +- Source/Networking/NetworkPlayer.cs | 7 + Source/OpenXR.cs | 36 +----- Source/Patches/Enemy/EnemyOnScreenPatches.cs | 26 ++++ Source/Patches/InputPatches.cs | 6 +- .../Patches/Player/PlayerDeathHeadPatches.cs | 26 ++++ Source/Patches/Player/PlayerEyesPatches.cs | 41 +++++- Source/Patches/UI/ChatPatches.cs | 4 +- .../UI/ValuableDiscoverGraphicPatches.cs | 3 +- Source/Player/Camera/VRCameraAim.cs | 2 +- Source/Player/Camera/VRCustomCamera.cs | 4 +- Source/Player/VREyeTracking.cs | 122 +++++++++--------- Source/Plugin.cs | 2 +- Source/UI/Controls/ControlOption.cs | 6 +- Source/UI/Controls/VRMenuKeybindToggle.cs | 6 +- Source/UI/Expressions/ExpressionRadial.cs | 4 +- 26 files changed, 266 insertions(+), 150 deletions(-) create mode 100644 Source/Data/OpenXRFeaturePack.cs create mode 100644 Source/Patches/Enemy/EnemyOnScreenPatches.cs create mode 100644 Source/Patches/Player/PlayerDeathHeadPatches.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b73a1b..32b5a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,25 @@ # 1.1.0 +## 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. + **Additions**: -- Added eye tracking support (for the three people that have it) +- Added eye tracking support - Added an option to detach arms from body - Added an overhauled spectating system - Added support for the new climbing mechanic @@ -9,7 +27,7 @@ - Added hotswapping in the main menu (F8) **Removals**: -- Removed support for REPO v0.2.1 +- Removed support for REPO v0.2.x # 1.0.3 diff --git a/Source/Assets/AssetCollection.cs b/Source/Assets/AssetCollection.cs index 89ce867..edc3e82 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; @@ -61,6 +63,8 @@ public static bool LoadAssets() return false; } + OpenXRFeatures = assetBundle.LoadAsset("OpenXRFeatures"); + RemappableControls = assetBundle.LoadAsset("RemappableControls").GetComponent(); RebindHeader = assetBundle.LoadAsset("Rebind Header"); diff --git a/Source/Config.cs b/Source/Config.cs index 39ef093..bcf8d1b 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -45,6 +45,10 @@ public class Config(string assemblyPath, ConfigFile file) "Controls how much haptic feedback you will experience while playing with the VR mod.", new AcceptableValueEnum())); + [ConfigDescriptor(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."); + [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))); 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 20c4baf..1a75156 100644 --- a/Source/Entrypoint.cs +++ b/Source/Entrypoint.cs @@ -130,7 +130,7 @@ internal static void SetupDefaultSceneVR() [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) diff --git a/Source/Experiments.cs b/Source/Experiments.cs index c230555..36e1618 100644 --- a/Source/Experiments.cs +++ b/Source/Experiments.cs @@ -1,4 +1,6 @@ -using HarmonyLib; +// ReSharper disable UnusedVariable + +using HarmonyLib; namespace RepoXR; @@ -9,28 +11,38 @@ internal static class Experiments [HarmonyPostfix] private static void FuckLolEnemy(EnemyDirector __instance) { - // Only allow eyeyeyeyeyeye spawning - var enemy = __instance.enemiesDifficulty1[0]; - - // Only allow mouth spawning - // var enemy = __instance.enemiesDifficulty1[4]; + // Difficulty 1 enemies + var ceileingEye = __instance.enemiesDifficulty1[0]; + var thinMan = __instance.enemiesDifficulty1[1]; + var gnome = __instance.enemiesDifficulty1[2]; + var duck = __instance.enemiesDifficulty1[3]; + var slowMouth = __instance.enemiesDifficulty1[4]; - // Only allow thin-man spawning - // var enemy = __instance.enemiesDifficulty1[1]; + // Difficulty 2 enemies + var valuableThrower = __instance.enemiesDifficulty2[0]; + var animal = __instance.enemiesDifficulty2[1]; + var upscream = __instance.enemiesDifficulty2[2]; + var hidden = __instance.enemiesDifficulty2[3]; + var tumbler = __instance.enemiesDifficulty2[4]; + var bowtie = __instance.enemiesDifficulty2[5]; + var floater = __instance.enemiesDifficulty2[6]; + var bang = __instance.enemiesDifficulty2[7]; - // Only allow upSCREAM! spawning - // var enemy = __instance.enemiesDifficulty2[2]; - - // Only allow beamer spawning - // var enemy = __instance.enemiesDifficulty3[4]; + // Difficulty 3 enemies + var head = __instance.enemiesDifficulty3[0]; + var robe = __instance.enemiesDifficulty3[1]; + var hunter = __instance.enemiesDifficulty3[2]; + var runner = __instance.enemiesDifficulty3[3]; + var beamer = __instance.enemiesDifficulty3[4]; + var slowWalker = __instance.enemiesDifficulty3[5]; __instance.enemiesDifficulty1.Clear(); __instance.enemiesDifficulty2.Clear(); __instance.enemiesDifficulty3.Clear(); - __instance.enemiesDifficulty1.Add(enemy); - __instance.enemiesDifficulty2.Add(enemy); - __instance.enemiesDifficulty3.Add(enemy); + __instance.enemiesDifficulty1.Add(robe); + __instance.enemiesDifficulty2.Add(robe); + __instance.enemiesDifficulty3.Add(robe); } [HarmonyPatch(typeof(PlayerController), nameof(PlayerController.FixedUpdate))] @@ -46,5 +58,13 @@ private static bool NoDamage() { return false; } + + [HarmonyPatch(typeof(EnemyThinMan), nameof(EnemyThinMan.TentacleLogic))] + [HarmonyPostfix] + private static void NoHurtMe(EnemyThinMan __instance) + { + if (__instance.tentacleLerp >= 1f) + __instance.tentacleLerp = 0.99f; + } } #endif \ No newline at end of file diff --git a/Source/Input/Actions.cs b/Source/Input/Actions.cs index 7063c8a..b8089c7 100644 --- a/Source/Input/Actions.cs +++ b/Source/Input/Actions.cs @@ -21,7 +21,6 @@ public class Actions public InputAction EyeGazePosition { get; private set; } public InputAction EyeGazeRotation { get; private set; } - public InputAction EyeGazeTracked { get; private set; } private Actions() { @@ -39,10 +38,9 @@ private Actions() EyeGazePosition = AssetCollection.DefaultXRActions.FindAction("Eye Gaze/Position"); EyeGazeRotation = AssetCollection.DefaultXRActions.FindAction("Eye Gaze/Rotation"); - EyeGazeTracked = AssetCollection.DefaultXRActions.FindAction("Eye Gaze/Is Tracked"); - + 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..6795746 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(); 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/HotswapManager.cs b/Source/Managers/HotswapManager.cs index 688ea67..d06a921 100644 --- a/Source/Managers/HotswapManager.cs +++ b/Source/Managers/HotswapManager.cs @@ -41,14 +41,17 @@ private static void HotswapDisableVR() private static void HotswapEnableVR() { Plugin.ToggleVR(); - + if (VRSession.InVR) RestartScene(); else + { + MenuManager.instance.PageCloseAll(); MenuManager.instance.PagePopUp("VR Startup Failed", Color.red, "RepoXR tried to launch the game in VR, however an error occured during initialization.\n\nYou can disable VR in the settings if you are not planning to play in VR.", "Alright fam", true); + } } private static void RestartScene() diff --git a/Source/Networking/NetworkPlayer.cs b/Source/Networking/NetworkPlayer.cs index 21f3ce2..c8cc83f 100644 --- a/Source/Networking/NetworkPlayer.cs +++ b/Source/Networking/NetworkPlayer.cs @@ -213,6 +213,13 @@ public void UpdateDominantHand(bool leftHanded) public void UpdateEyeTracking(Vector3 gazePoint) { + // (0, -1000, 0) is sent whenever eye tracking is disabled during a session + if (gazePoint == Vector3.down * 1000) + { + EyeTracking = false; + return; + } + EyeTracking = true; EyeGazePoint = gazePoint; } diff --git a/Source/OpenXR.cs b/Source/OpenXR.cs index 3ed2092..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; @@ -427,39 +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(); - var eyeTracking = ScriptableObject.CreateInstance(); - - valveIndex.enabled = true; - hpReverb.enabled = true; - htcVive.enabled = true; - mmController.enabled = true; - khrSimple.enabled = true; - metaQuestTouch.enabled = true; - oculusTouch.enabled = true; - eyeTracking.enabled = true; - - OpenXRSettings.Instance.features = - [ - valveIndex, - hpReverb, - htcVive, - mmController, - khrSimple, - metaQuestTouch, - oculusTouch, - eyeTracking - ]; + OpenXRSettings.Instance.features = AssetCollection.OpenXRFeatures.Features.ToArray(); } } } \ 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/InputPatches.cs b/Source/Patches/InputPatches.cs index 93c240a..373de4a 100644 --- a/Source/Patches/InputPatches.cs +++ b/Source/Patches/InputPatches.cs @@ -47,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(); } @@ -207,7 +205,7 @@ private static bool InputDisplayGet(InputManager __instance, InputKey _inputKey, return false; } - var index = action.GetBindingIndex(VRInputSystem.instance.CurrentControlScheme); + var index = action.GetBindingIndex(VRInputSystem.Instance.CurrentControlScheme); __result = __instance.InputDisplayGetString(action, index); @@ -234,7 +232,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; } 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 index a3bc40c..e1bfd74 100644 --- a/Source/Patches/Player/PlayerEyesPatches.cs +++ b/Source/Patches/Player/PlayerEyesPatches.cs @@ -1,29 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Emit; using HarmonyLib; -using RepoXR.Assets; -using RepoXR.Input; -using RepoXR.Managers; using RepoXR.Networking; -using UnityEngine; namespace RepoXR.Patches.Player; -[RepoXRPatch] +[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) { - // TODO: Check if we can just do this with the local player if (__instance.playerAvatar.isLocal) return; // TODO: Determine if certain occurrences in the game should override eye gaze - if (!NetworkSystem.instance.GetNetworkPlayer(__instance.playerAvatar, out var player) && player.EyeTracking) + 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/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 debugCubeHmdRel.position; - public Vector3 GazeDirection { get; private set; } + public bool Enabled => supported && Plugin.Config.EnableEyeTracking.Value; + public Ray Gaze { get; private set; } + + private Vector3 gazePosition; + private Quaternion gazeRotation; + + private bool supported; private void Awake() { - Actions.Instance.EyeGazeTracked.performed += OnEyeTrackingDetected; + Actions.Instance.EyeGazePosition.performed += OnEyeGazePosition; + Actions.Instance.EyeGazeRotation.performed += OnEyeGazeRotation; - debugCubeHmdRel = Instantiate(AssetCollection.Cube).transform; - debugCubeSpaceRel = Instantiate(AssetCollection.Cube).transform; + Plugin.Config.EnableEyeTracking.SettingChanged += OnEyeTrackingSettingChanged; - debugCubeHmdRel.GetComponent().material.color = Color.blue; - debugCubeSpaceRel.GetComponent().material.color = Color.red; + 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; + } - debugCubeHmdRel.GetComponent().enabled = false; - debugCubeSpaceRel.GetComponent().enabled = false; + private void OnDestroy() + { + Actions.Instance.EyeGazePosition.performed -= OnEyeGazePosition; + Actions.Instance.EyeGazeRotation.performed -= OnEyeGazeRotation; - debugCubeHmdRel.position = Vector3.down * 1000; - debugCubeSpaceRel.position = Vector3.down * 1000; + Plugin.Config.EnableEyeTracking.SettingChanged -= OnEyeTrackingSettingChanged; } - private void OnDestroy() + private void OnEyeGazePosition(InputAction.CallbackContext ctx) { - Actions.Instance.EyeGazeTracked.performed -= OnEyeTrackingDetected; + supported = true; + + gazePosition = ctx.ReadValue(); } - private void OnEyeTrackingDetected(InputAction.CallbackContext obj) + private void OnEyeGazeRotation(InputAction.CallbackContext ctx) { - if (!obj.performed) - return; + supported = true; - Supported = true; + gazeRotation = ctx.ReadValue(); + } + + private static void OnEyeTrackingSettingChanged(object sender, EventArgs e) + { + if (!Plugin.Config.EnableEyeTracking.Value) + NetworkSystem.instance.UpdateEyeTracking(Vector3.down * 1000); } private void Update() { - if (!Supported) + if (!Enabled) return; - if (!!false) // TODO: If disabled by config - return; + var ray = new Ray(transform.parent.TransformPoint(gazePosition), + transform.parent.TransformDirection(gazeRotation * Vector3.forward)); - var gazePosition = Actions.Instance.EyeGazePosition.ReadValue(); - var gazeRotation = Actions.Instance.EyeGazeRotation.ReadValue(); - - UpdateHmdRelative(gazePosition, gazeRotation); - UpdateSpaceRelative(gazePosition, gazeRotation); - - // var ray = new Ray(transform.TransformPoint(gazePosition), - // transform.TransformDirection(gazeRotation * Vector3.forward)); - // - // GazeDirection = ray.direction; - // - // if (Physics.Raycast(ray, out var hit, 5, SemiFunc.LayerMaskGetShouldHits())) - // debugCubeHmdRel.transform.position = hit.point; - // else - // debugCubeHmdRel.transform.position = ray.origin + ray.direction * 5; - // - // NetworkSystem.instance.UpdateEyeTracking(debugCubeHmdRel.transform.position); - // - // debugCubeHmdRel.transform.rotation = Quaternion.LookRotation(ray.direction); - } + var position = Physics.Raycast(ray, out var hit, 10, SemiFunc.LayerMaskGetShouldHits()) + ? hit.point + : ray.origin + ray.direction * 10; - private void UpdateHmdRelative(Vector3 position, Quaternion rotation) - { - var ray = new Ray(transform.TransformPoint(position), transform.TransformDirection(rotation * Vector3.forward)); + Gaze = ray; - if (Physics.Raycast(ray, out var hit, 5, SemiFunc.LayerMaskGetShouldHits())) - debugCubeHmdRel.transform.position = hit.point; - else - debugCubeHmdRel.transform.position = ray.origin + ray.direction * 5; + NetworkSystem.instance.UpdateEyeTracking(position); - debugCubeHmdRel.transform.rotation = Quaternion.LookRotation(ray.direction); + debugCube.position = position; + debugCube.rotation = Quaternion.LookRotation(ray.direction); } - private void UpdateSpaceRelative(Vector3 position, Quaternion rotation) + public static bool LookingAt(Vector3 position, float padWidth, float padHeight) { - var ray = new Ray(transform.parent.TransformPoint(position), - transform.parent.TransformDirection(rotation * Vector3.forward)); + // 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; - if (Physics.Raycast(ray, out var hit, 5, SemiFunc.LayerMaskGetShouldHits())) - debugCubeSpaceRel.transform.position = hit.point; - else - debugCubeSpaceRel.transform.position = ray.origin + ray.direction * 5; + 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); - debugCubeSpaceRel.transform.rotation = Quaternion.LookRotation(ray.direction); + return angle <= coneAngle; } } \ No newline at end of file diff --git a/Source/Plugin.cs b/Source/Plugin.cs index 3626bee..369a1f5 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -32,7 +32,7 @@ public class Plugin : BaseUnityPlugin private readonly string[] GAME_ASSEMBLY_HASHES = [ - "E95BEFC4BD5206D9455BAA68C51A950ABAF0A99001012928B3E553D8D0E5CDB3" // v0.2.1 + "E95BEFC4BD5206D9455BAA68C51A950ABAF0A99001012928B3E553D8D0E5CDB3" // v0.2.2 ]; public new static Config Config { get; private set; } = null!; diff --git a/Source/UI/Controls/ControlOption.cs b/Source/UI/Controls/ControlOption.cs index 7918cac..1bd749b 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() @@ -32,7 +32,7 @@ public void StartRebind() public void SetBindToggle(bool toggle) { - VRInputSystem.instance.InputToggleRebind(action!.name, toggle); + VRInputSystem.Instance.InputToggleRebind(action!.name, toggle); } public void FetchToggle() @@ -41,7 +41,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)) From eddcefb67f3e52cf2c1c68cf1789bac851a94b6a Mon Sep 17 00:00:00 2001 From: DaXcess Date: Fri, 10 Oct 2025 16:39:50 +0200 Subject: [PATCH 11/20] Separated arms yeay --- CHANGELOG.md | 11 +++++ Source/Config.cs | 33 ++++++++------- Source/Networking/NetworkSystem.cs | 5 +++ Source/Player/VREyeTracking.cs | 13 +++++- Source/Player/VRRig.cs | 66 +++++++++++++++++++++++++++--- 5 files changed, 107 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32b5a37..309e164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 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). @@ -18,6 +22,10 @@ When playing with other people, other people will see your pupils move based on 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 @@ -26,6 +34,9 @@ Eye tracking can be enabled and disabled mid-game, there's no need to restart. I - Added support for the monster update - Added hotswapping in the main menu (F8) +**Changed**: +- Removed the performance tab and replaced it with UI in the settings + **Removals**: - Removed support for REPO v0.2.x diff --git a/Source/Config.cs b/Source/Config.cs index bcf8d1b..892899c 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -26,6 +26,12 @@ public class Config(string assemblyPath, ConfigFile file) // Gameplay configuration + [ConfigDescriptor(stepSize: 5f, suffix: "%")] + public ConfigEntry CameraResolution { get; } = file.Bind("Gameplay", 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 ReducedAimImpact { get; } = file.Bind("Gameplay", nameof(ReducedAimImpact), false, "When enabled, lowers the severity of force-look events (like the ceiling eye), which can be helpful for people with motion sickness"); @@ -38,6 +44,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, @@ -49,29 +59,24 @@ public class Config(string assemblyPath, ConfigFile file) 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(pointerSize: 0.01f, stepSize: 0.05f)] - public ConfigEntry HUDPlaneOffset { get; } = file.Bind("Gameplay", nameof(HUDPlaneOffset), -0.45f, + public ConfigEntry HUDPlaneOffset { get; } = file.Bind("UI", nameof(HUDPlaneOffset), -0.45f, new ConfigDescription("The default height offset for the HUD", new AcceptableValueRange(-0.6f, 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))); + 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(-0.6f, 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, @@ -128,7 +133,7 @@ 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 /// diff --git a/Source/Networking/NetworkSystem.cs b/Source/Networking/NetworkSystem.cs index b999c6c..e0bc484 100644 --- a/Source/Networking/NetworkSystem.cs +++ b/Source/Networking/NetworkSystem.cs @@ -100,6 +100,11 @@ public void UpdateEyeTracking(Vector3 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. diff --git a/Source/Player/VREyeTracking.cs b/Source/Player/VREyeTracking.cs index 84fc414..244839b 100644 --- a/Source/Player/VREyeTracking.cs +++ b/Source/Player/VREyeTracking.cs @@ -19,6 +19,7 @@ public class VREyeTracking : MonoBehaviour private Quaternion gazeRotation; private bool supported; + private float lastHardwareInput; private void Awake() { @@ -46,6 +47,7 @@ private void OnDestroy() private void OnEyeGazePosition(InputAction.CallbackContext ctx) { supported = true; + lastHardwareInput = Time.realtimeSinceStartup; gazePosition = ctx.ReadValue(); } @@ -53,6 +55,7 @@ private void OnEyeGazePosition(InputAction.CallbackContext ctx) private void OnEyeGazeRotation(InputAction.CallbackContext ctx) { supported = true; + lastHardwareInput = Time.realtimeSinceStartup; gazeRotation = ctx.ReadValue(); } @@ -60,7 +63,7 @@ private void OnEyeGazeRotation(InputAction.CallbackContext ctx) private static void OnEyeTrackingSettingChanged(object sender, EventArgs e) { if (!Plugin.Config.EnableEyeTracking.Value) - NetworkSystem.instance.UpdateEyeTracking(Vector3.down * 1000); + NetworkSystem.instance.DisableEyeTracking(); } private void Update() @@ -68,6 +71,14 @@ 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)); diff --git a/Source/Player/VRRig.cs b/Source/Player/VRRig.cs index 5fc16ae..dfa1ca5 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; @@ -53,7 +55,9 @@ public class VRRig : MonoBehaviour public Vector3 mapRightPosition; public Vector3 mapLeftPosition; - + + private bool armsDetached; + private Transform leftArmMesh; private Transform rightArmMesh; @@ -80,11 +84,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 +165,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 +213,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() @@ -400,6 +426,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 +481,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 From 92c36a7385210191dbd61f2c20a7c2fd449a40fb Mon Sep 17 00:00:00 2001 From: DaXcess Date: Sat, 11 Oct 2025 14:37:18 +0200 Subject: [PATCH 12/20] New valuable discover graphic --- Source/Assets/AssetCollection.cs | 2 + Source/Config.cs | 20 +-- .../UI/ValuableDiscoverGraphicPatches.cs | 143 ------------------ Source/Patches/UI/ValuableDiscoverPatches.cs | 31 ++++ Source/UI/ValuableDiscoverGraphic.cs | 121 +++++++++++++++ 5 files changed, 164 insertions(+), 153 deletions(-) delete mode 100644 Source/Patches/UI/ValuableDiscoverGraphicPatches.cs create mode 100644 Source/Patches/UI/ValuableDiscoverPatches.cs create mode 100644 Source/UI/ValuableDiscoverGraphic.cs diff --git a/Source/Assets/AssetCollection.cs b/Source/Assets/AssetCollection.cs index edc3e82..9dbf390 100644 --- a/Source/Assets/AssetCollection.cs +++ b/Source/Assets/AssetCollection.cs @@ -23,6 +23,7 @@ internal static class AssetCollection public static GameObject VRTumble; public static GameObject Keyboard; public static GameObject ExpressionWheel; + public static GameObject ValuableDiscover; public static GameObject MenuSettings; public static GameObject MenuSettingsCategory; @@ -75,6 +76,7 @@ public static bool LoadAssets() VRTumble = assetBundle.LoadAsset("VRTumble"); Keyboard = assetBundle.LoadAsset("NonNativeKeyboard"); ExpressionWheel = assetBundle.LoadAsset("Expression Radial"); + ValuableDiscover = assetBundle.LoadAsset("Valuable Discover"); MenuSettings = assetBundle.LoadAsset("VR Settings Page"); MenuSettingsCategory = assetBundle.LoadAsset("VR Settings Page - Category"); diff --git a/Source/Config.cs b/Source/Config.cs index 892899c..5c05c31 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -26,12 +26,6 @@ public class Config(string assemblyPath, ConfigFile file) // Gameplay configuration - [ConfigDescriptor(stepSize: 5f, suffix: "%")] - public ConfigEntry CameraResolution { get; } = file.Bind("Gameplay", 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 ReducedAimImpact { get; } = file.Bind("Gameplay", nameof(ReducedAimImpact), false, "When enabled, lowers the severity of force-look events (like the ceiling eye), which can be helpful for people with motion sickness"); @@ -55,7 +49,7 @@ public class Config(string assemblyPath, ConfigFile file) "Controls how much haptic feedback you will experience while playing with the VR mod.", new AcceptableValueEnum())); - [ConfigDescriptor(trueText: "Enabled", falseText: "Disabled")] + [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."); @@ -101,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."); @@ -139,9 +139,6 @@ public class Config(string assemblyPath, ConfigFile file) /// public void SetupGlobalCallbacks() { - if (!VRSession.InVR) - return; - CameraResolution.SettingChanged += (_, _) => { XRSettings.eyeTextureResolutionScale = CameraResolution.Value / 100f; @@ -149,6 +146,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/Patches/UI/ValuableDiscoverGraphicPatches.cs b/Source/Patches/UI/ValuableDiscoverGraphicPatches.cs deleted file mode 100644 index 55d7891..0000000 --- a/Source/Patches/UI/ValuableDiscoverGraphicPatches.cs +++ /dev/null @@ -1,143 +0,0 @@ -using HarmonyLib; -using RepoXR.Managers; -using RepoXR.Player; -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 (VREyeTracking.LookingAt(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..e6ab552 --- /dev/null +++ b/Source/Patches/UI/ValuableDiscoverPatches.cs @@ -0,0 +1,31 @@ +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 (_state == ValuableDiscoverGraphic.State.Reminder) + component.ReminderSetup(); + + if (_state == ValuableDiscoverGraphic.State.Bad) + component.BadSetup(); + + return false; + } +} \ No newline at end of file diff --git a/Source/UI/ValuableDiscoverGraphic.cs b/Source/UI/ValuableDiscoverGraphic.cs new file mode 100644 index 0000000..b4bf44c --- /dev/null +++ b/Source/UI/ValuableDiscoverGraphic.cs @@ -0,0 +1,121 @@ +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"); + + [SerializeField] private Renderer renderer; + + private global::ValuableDiscoverGraphic baseGraphic; + + internal PhysGrabObject target; + + private global::ValuableDiscoverGraphic.State state; + private bool discovered = true; + 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 = false; + } + } + 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); + } +} \ No newline at end of file From c2a9fc0c51e6dc671abe5e4c29cc52b8cdfae75d Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 27 Oct 2025 12:33:34 +0100 Subject: [PATCH 13/20] Add/change a bunch more things - Added compatibility with CustomDiscoverStateLib - Fix some error spam with modded keybinds (even though they'll likely wont work in VR anyways) - Small networking optimization - Look at enemy on death - Clean some things (code, comments) --- CHANGELOG.md | 10 ++++- RepoXR.csproj | 1 + Source/Compat.cs | 1 + Source/Networking/NetworkPlayer.cs | 2 +- Source/Networking/NetworkSystem.cs | 4 ++ Source/Patches/InputPatches.cs | 16 +++++-- Source/Patches/Player/PlayerAvatarPatches.cs | 28 +++++++++++-- Source/Patches/UI/ValuableDiscoverPatches.cs | 44 +++++++++++++++++++- Source/Player/VRPlayer.cs | 1 - Source/Plugin.cs | 4 -- Source/Rendering/CustomPostProcessing.cs | 2 +- Source/UI/ValuableDiscoverGraphic.cs | 16 +++++-- 12 files changed, 109 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 309e164..2be55a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 1.1.0 +-- TODOS -- + +- Death head thingy +- Climbing +- *Maybe* Change camera hierarchy setup to fix bug and make better force looking logic + ## 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. @@ -29,13 +35,15 @@ You can now swap between VR mode and flatscreen mode by pressing the F8 button o **Additions**: - Added eye tracking support - Added an option to detach arms from body -- Added an overhauled spectating system - 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) **Changed**: - 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) **Removals**: - Removed support for REPO v0.2.x diff --git a/RepoXR.csproj b/RepoXR.csproj index f9398e5..83a6d5e 100644 --- a/RepoXR.csproj +++ b/RepoXR.csproj @@ -30,6 +30,7 @@ + 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/Networking/NetworkPlayer.cs b/Source/Networking/NetworkPlayer.cs index c8cc83f..7e3331c 100644 --- a/Source/Networking/NetworkPlayer.cs +++ b/Source/Networking/NetworkPlayer.cs @@ -213,7 +213,7 @@ public void UpdateDominantHand(bool leftHanded) public void UpdateEyeTracking(Vector3 gazePoint) { - // (0, -1000, 0) is sent whenever eye tracking is disabled during a session + // (0, -1000, 0) is sent whenever eye tracking is disabled (or stopped working) during a session if (gazePoint == Vector3.down * 1000) { EyeTracking = false; diff --git a/Source/Networking/NetworkSystem.cs b/Source/Networking/NetworkSystem.cs index e0bc484..6d8ebab 100644 --- a/Source/Networking/NetworkSystem.cs +++ b/Source/Networking/NetworkSystem.cs @@ -222,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); diff --git a/Source/Patches/InputPatches.cs b/Source/Patches/InputPatches.cs index 373de4a..33b26c7 100644 --- a/Source/Patches/InputPatches.cs +++ b/Source/Patches/InputPatches.cs @@ -56,11 +56,19 @@ 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()]; + try + { + __result = (int)key >= bindings + ? AssetCollection.RemappableControls.additionalBindings[(int)key - bindings] + : Actions.Instance[key.ToString()]; - return false; + 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))] 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/UI/ValuableDiscoverPatches.cs b/Source/Patches/UI/ValuableDiscoverPatches.cs index e6ab552..41e3c44 100644 --- a/Source/Patches/UI/ValuableDiscoverPatches.cs +++ b/Source/Patches/UI/ValuableDiscoverPatches.cs @@ -1,4 +1,6 @@ -using HarmonyLib; +using System.Runtime.CompilerServices; +using CustomDiscoverStateLib; +using HarmonyLib; using RepoXR.Assets; using UnityEngine; @@ -20,6 +22,10 @@ private static bool OnValuableDiscovered(ValuableDiscover __instance, PhysGrabOb .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(); @@ -28,4 +34,40 @@ private static bool OnValuableDiscovered(ValuableDiscover __instance, PhysGrabOb 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/VRPlayer.cs b/Source/Player/VRPlayer.cs index 8e6903a..1d67b19 100644 --- a/Source/Player/VRPlayer.cs +++ b/Source/Player/VRPlayer.cs @@ -80,7 +80,6 @@ private void Awake() localRig.leftArmTarget = leftHand; localRig.rightArmTarget = rightHand; - // TODO: Eye tracking eyeTracking = mainCamera.gameObject.AddComponent(); Actions.Instance["ResetHeight"].performed += OnResetHeight; diff --git a/Source/Plugin.cs b/Source/Plugin.cs index 369a1f5..abdae9a 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -194,10 +194,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); diff --git a/Source/Rendering/CustomPostProcessing.cs b/Source/Rendering/CustomPostProcessing.cs index 69acd51..5a8f696 100644 --- a/Source/Rendering/CustomPostProcessing.cs +++ b/Source/Rendering/CustomPostProcessing.cs @@ -20,7 +20,7 @@ private void Start() postProcessing.volume.profile.AddSettings(vignette); - // Disable replaced shaders + // Disable original shaders postProcessing.vignette.enabled.value = false; } diff --git a/Source/UI/ValuableDiscoverGraphic.cs b/Source/UI/ValuableDiscoverGraphic.cs index b4bf44c..21f22ca 100644 --- a/Source/UI/ValuableDiscoverGraphic.cs +++ b/Source/UI/ValuableDiscoverGraphic.cs @@ -10,14 +10,14 @@ public class ValuableDiscoverGraphic : MonoBehaviour private static readonly int BaseColor = Shader.PropertyToID("_BaseColor"); private static readonly int EdgeColor = Shader.PropertyToID("_EdgeColor"); - [SerializeField] private Renderer renderer; + public Renderer renderer; private global::ValuableDiscoverGraphic baseGraphic; internal PhysGrabObject target; private global::ValuableDiscoverGraphic.State state; - private bool discovered = true; + private bool discovered; private float waitTimer; private float animLerp; @@ -55,7 +55,7 @@ private void Update() targetSize = bounds.size; var lookingAt = VREyeTracking.LookingAt(bounds.center, 0.5f, 0.5f); - if (lookingAt && discovered) + if (lookingAt && !discovered) { if (state == global::ValuableDiscoverGraphic.State.Reminder) baseGraphic.sound.Play(target.centerPoint, 0.3f); @@ -63,7 +63,7 @@ private void Update() baseGraphic.sound.Play(target.centerPoint); renderer.enabled = true; - discovered = false; + discovered = true; } } else @@ -118,4 +118,12 @@ public void BadSetup() 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 From 68c9f9c24f001f906735141dc53af594ab8f322d Mon Sep 17 00:00:00 2001 From: DaXcess Date: Thu, 30 Oct 2025 09:16:12 +0100 Subject: [PATCH 14/20] Focus sphere and custom cam optimizations --- CHANGELOG.md | 1 + Source/Assets/AssetCollection.cs | 2 + Source/Config.cs | 7 ++ Source/Experiments.cs | 6 +- Source/Managers/VRSession.cs | 7 +- Source/Patches/CameraPatches.cs | 4 ++ .../Patches/Enemy/EnemyCeilingEyePatches.cs | 24 ++++++- Source/Player/Camera/VRCustomCamera.cs | 72 ++++++++++++++++--- Source/UI/FocusSphere.cs | 65 +++++++++++++++++ 9 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 Source/UI/FocusSphere.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be55a4..69a4636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ You can now swap between VR mode and flatscreen mode by pressing the F8 button o - 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) +- Optimized the custom camera by adding a frame rate limiter **Removals**: - Removed support for REPO v0.2.x diff --git a/Source/Assets/AssetCollection.cs b/Source/Assets/AssetCollection.cs index 9dbf390..c423dd4 100644 --- a/Source/Assets/AssetCollection.cs +++ b/Source/Assets/AssetCollection.cs @@ -24,6 +24,7 @@ internal static class AssetCollection public static GameObject Keyboard; public static GameObject ExpressionWheel; public static GameObject ValuableDiscover; + public static GameObject FocusSphere; public static GameObject MenuSettings; public static GameObject MenuSettingsCategory; @@ -77,6 +78,7 @@ public static bool LoadAssets() 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"); diff --git a/Source/Config.cs b/Source/Config.cs index 5c05c31..991fc2e 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -110,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), + 60f, + 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, 120))); + [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.", diff --git a/Source/Experiments.cs b/Source/Experiments.cs index 36e1618..ec0ab43 100644 --- a/Source/Experiments.cs +++ b/Source/Experiments.cs @@ -40,9 +40,9 @@ private static void FuckLolEnemy(EnemyDirector __instance) __instance.enemiesDifficulty2.Clear(); __instance.enemiesDifficulty3.Clear(); - __instance.enemiesDifficulty1.Add(robe); - __instance.enemiesDifficulty2.Add(robe); - __instance.enemiesDifficulty3.Add(robe); + __instance.enemiesDifficulty1.Add(ceileingEye); + __instance.enemiesDifficulty2.Add(ceileingEye); + __instance.enemiesDifficulty3.Add(ceileingEye); } [HarmonyPatch(typeof(PlayerController), nameof(PlayerController.FixedUpdate))] diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs index ee0f7ae..e048821 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -1,4 +1,5 @@ -using RepoXR.Input; +using RepoXR.Assets; +using RepoXR.Input; using RepoXR.Player; using RepoXR.UI; using UnityEngine; @@ -25,6 +26,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() { @@ -69,5 +71,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/Patches/CameraPatches.cs b/Source/Patches/CameraPatches.cs index 53ede4f..0ae849b 100644 --- a/Source/Patches/CameraPatches.cs +++ b/Source/Patches/CameraPatches.cs @@ -13,6 +13,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; } diff --git a/Source/Patches/Enemy/EnemyCeilingEyePatches.cs b/Source/Patches/Enemy/EnemyCeilingEyePatches.cs index ada6d28..6b7be62 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,6 +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))) + // 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))), + + // 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 +89,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/Player/Camera/VRCustomCamera.cs b/Source/Player/Camera/VRCustomCamera.cs index 21d2d33..9bb903e 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,49 @@ 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; - + Plugin.Config.CustomCameraFOV.SettingChanged += OnFOVChanged; + + UpdateRenderTexture(); } private void OnDestroy() { instance = null!; - + Plugin.Config.CustomCameraFOV.SettingChanged -= OnFOVChanged; } - + private void OnFOVChanged(object sender, EventArgs e) { var fov = Plugin.Config.CustomCameraFOV.Value; @@ -69,15 +79,59 @@ private void Update() // Some weird fog thing, I don't know why but this is needed RenderSettings.fogDensity = SemiFunc.MenuLevel() || SemiFunc.RunIsShop() || SemiFunc.RunIsLobby() ? 0.015f : 0.15f; + + 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 LateUpdate() { // 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; } + + 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/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 From 799b0ada1fa074f9a982ed7cce14c476f10dce31 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Thu, 30 Oct 2025 11:51:57 +0100 Subject: [PATCH 15/20] Small hotswap UI fix --- Source/Managers/HotswapManager.cs | 5 ++++- Source/Player/VREyeTracking.cs | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Source/Managers/HotswapManager.cs b/Source/Managers/HotswapManager.cs index 89e9b5b..282b0d8 100644 --- a/Source/Managers/HotswapManager.cs +++ b/Source/Managers/HotswapManager.cs @@ -46,7 +46,10 @@ private static void HotswapEnableVR() RestartScene(); else { - MenuManager.instance.PageCloseAll(); + // 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", diff --git a/Source/Player/VREyeTracking.cs b/Source/Player/VREyeTracking.cs index 244839b..9c6a695 100644 --- a/Source/Player/VREyeTracking.cs +++ b/Source/Player/VREyeTracking.cs @@ -28,6 +28,7 @@ private void Awake() Plugin.Config.EnableEyeTracking.SettingChanged += OnEyeTrackingSettingChanged; + // TODO: Remove once tested with real hardware debugCube = Instantiate(AssetCollection.Cube).transform; debugCube.GetComponent().material.color = Color.blue; debugCube.GetComponent().enabled = false; @@ -46,18 +47,28 @@ private void OnDestroy() 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; - - gazePosition = ctx.ReadValue(); } 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; - - gazeRotation = ctx.ReadValue(); } private static void OnEyeTrackingSettingChanged(object sender, EventArgs e) From 505cf250d297e2b8012f684d93cdfc11f4db79e2 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 3 Nov 2025 22:47:29 +0100 Subject: [PATCH 16/20] Initial v0.3.0 commit --- CHANGELOG.md | 11 +- RepoXR.csproj | 2 +- Source/Config.cs | 10 +- Source/Entrypoint.cs | 11 +- Source/Experiments.cs | 93 +++++++++++- Source/Input/RebindManager.cs | 3 +- Source/Managers/VRSession.cs | 9 -- Source/Networking/NetworkSystem.cs | 14 +- Source/Patches/CameraPatches.cs | 18 +++ .../Patches/Enemy/EnemyCeilingEyePatches.cs | 23 --- .../Patches/Enemy/EnemyHeartHuggerPatches.cs | 35 +++++ Source/Patches/Enemy/EnemyOoglyPatches.cs | 31 ++++ Source/Patches/Enemy/EnemySpinnyPatches.cs | 21 +++ Source/Patches/Item/ItemBoomboxPatches.cs | 4 +- Source/Patches/PhysGrabObjectPatches.cs | 38 +++-- Source/Patches/PhysGrabberPatches.cs | 109 +++++++------- Source/Patches/Player/InventoryPatches.cs | 39 ++++- Source/Patches/Player/MapToolPatches.cs | 4 +- Source/Patches/Player/PlayerEyesPatches.cs | 2 +- Source/Patches/SpectatePatches.cs | 63 +++++++- Source/Patches/UI/UIPatches.cs | 8 +- Source/Player/Camera/VRCameraAim.cs | 138 ++++++++++-------- Source/Player/Camera/VRCameraTracker.cs | 28 ++++ Source/Player/Camera/VRCustomCamera.cs | 25 +++- Source/Player/VRInventory.cs | 7 +- Source/Player/VRPlayer.cs | 16 +- Source/Player/VRRig.cs | 28 +++- Source/Plugin.cs | 2 +- Source/UI/ChatUI.cs | 2 +- Source/UI/GameHud.cs | 3 +- Source/UI/MainMenu.cs | 2 +- Source/UI/PauseUI.cs | 12 +- 32 files changed, 578 insertions(+), 233 deletions(-) create mode 100644 Source/Patches/Enemy/EnemyHeartHuggerPatches.cs create mode 100644 Source/Patches/Enemy/EnemyOoglyPatches.cs create mode 100644 Source/Patches/Enemy/EnemySpinnyPatches.cs create mode 100644 Source/Player/Camera/VRCameraTracker.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 69a4636..ac47fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,8 @@ # 1.1.0 --- TODOS -- +-- BUGS -- -- Death head thingy -- Climbing -- *Maybe* Change camera hierarchy setup to fix bug and make better force looking logic +- Flashlight not aligned (needs more testing) ## Detached Arms @@ -40,11 +38,14 @@ You can now swap between VR mode and flatscreen mode by pressing the F8 button o - Added support for the monster update - Added hotswapping in the main menu (F8) -**Changed**: +**Changes**: - 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) - Optimized the custom camera by adding a frame rate limiter +- 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) **Removals**: - Removed support for REPO v0.2.x diff --git a/RepoXR.csproj b/RepoXR.csproj index 83a6d5e..bc29226 100644 --- a/RepoXR.csproj +++ b/RepoXR.csproj @@ -34,7 +34,7 @@ - + diff --git a/Source/Config.cs b/Source/Config.cs index 991fc2e..054735a 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -55,14 +55,14 @@ public class Config(string assemblyPath, ConfigFile file) // UI configuration - [ConfigDescriptor(pointerSize: 0.01f, stepSize: 0.05f)] + [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(-0.6f, 0.5f))); + new ConfigDescription("The default height offset for the HUD", new AcceptableValueRange(-1f, 0.5f))); - [ConfigDescriptor(pointerSize: 0.01f, stepSize: 0.05f)] + [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(-0.6f, 0.5f))); + new AcceptableValueRange(-1f, 0.5f))); [ConfigDescriptor(pointerSize: 0.05f, stepSize: 0.25f)] public ConfigEntry SmoothCanvasDistance { get; } = file.Bind("UI", nameof(SmoothCanvasDistance), 1.5f, @@ -84,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: "°")] diff --git a/Source/Entrypoint.cs b/Source/Entrypoint.cs index 1a75156..b770703 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; @@ -39,10 +38,10 @@ internal 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); + var poseDriver = mainCamera.gameObject.AddComponent(); + // poseDriver.positionAction = Actions.Instance.HeadPosition; + // poseDriver.rotationAction = Actions.Instance.HeadRotation; + // poseDriver.trackingStateInput = new InputActionProperty(Actions.Instance.HeadTrackingState); // Parent overlay to main camera overlayCamera.transform.SetParent(mainCamera.transform, false); diff --git a/Source/Experiments.cs b/Source/Experiments.cs index ec0ab43..d0ab358 100644 --- a/Source/Experiments.cs +++ b/Source/Experiments.cs @@ -1,5 +1,7 @@ // ReSharper disable UnusedVariable +using System.Collections.Generic; +using System.Reflection.Emit; using HarmonyLib; namespace RepoXR; @@ -40,9 +42,67 @@ private static void FuckLolEnemy(EnemyDirector __instance) __instance.enemiesDifficulty2.Clear(); __instance.enemiesDifficulty3.Clear(); - __instance.enemiesDifficulty1.Add(ceileingEye); - __instance.enemiesDifficulty2.Add(ceileingEye); - __instance.enemiesDifficulty3.Add(ceileingEye); + __instance.enemiesDifficulty1.Add(slowMouth); + __instance.enemiesDifficulty2.Add(slowMouth); + __instance.enemiesDifficulty3.Add(slowMouth); + } + + private static bool done; + + [HarmonyPatch(typeof(MenuButton), nameof(MenuButton.OnHovering))] + [HarmonyPrefix] + private static void ForceMap() + { + if (done) + return; + + done = true; + + var mgr = RunManager.instance; + + var station = mgr.levels[0]; + var manor = mgr.levels[1]; + var museum = mgr.levels[2]; + var hogwarts = mgr.levels[3]; + + mgr.levels.Clear(); + mgr.levels.Add(museum); + + var boombox = museum.ValuablePresets[0].big[2]; + + for (var i = 0; i < museum.ValuablePresets.Count; i++) + { + for (var j = 0; j < museum.ValuablePresets[i].big.Count; j++) + Logger.LogDebug($"[{museum.ValuablePresets[i]}] Big: {museum.ValuablePresets[i].big[j].prefabName}"); + for (var j = 0; j < museum.ValuablePresets[i].medium.Count; j++) + Logger.LogDebug($"[{museum.ValuablePresets[i]}] Medium: {museum.ValuablePresets[i].medium[j].prefabName}"); + for (var j = 0; j < museum.ValuablePresets[i].small.Count; j++) + Logger.LogDebug($"[{museum.ValuablePresets[i]}] Small: {museum.ValuablePresets[i].small[j].prefabName}"); + for (var j = 0; j < museum.ValuablePresets[i].tall.Count; j++) + Logger.LogDebug($"[{museum.ValuablePresets[i]}] Tall: {museum.ValuablePresets[i].tall[j].prefabName}"); + for (var j = 0; j < museum.ValuablePresets[i].tiny.Count; j++) + Logger.LogDebug($"[{museum.ValuablePresets[i]}] Tiny: {museum.ValuablePresets[i].tiny[j].prefabName}"); + for (var j = 0; j < museum.ValuablePresets[i].veryTall.Count; j++) + Logger.LogDebug($"[{museum.ValuablePresets[i]}] Very Tall: {museum.ValuablePresets[i].veryTall[j].prefabName}"); + for (var j = 0; j < museum.ValuablePresets[i].wide.Count; j++) + Logger.LogDebug($"[{museum.ValuablePresets[i]}] Wide: {museum.ValuablePresets[i].wide[j].prefabName}"); + } + + museum.ValuablePresets[0].big.Clear(); + museum.ValuablePresets[0].medium.Clear(); + museum.ValuablePresets[0].small.Clear(); + museum.ValuablePresets[0].tall.Clear(); + museum.ValuablePresets[0].tiny.Clear(); + museum.ValuablePresets[0].veryTall.Clear(); + museum.ValuablePresets[0].wide.Clear(); + + museum.ValuablePresets[0].big.Add(boombox); + museum.ValuablePresets[0].medium.Add(boombox); + museum.ValuablePresets[0].small.Add(boombox); + museum.ValuablePresets[0].tall.Add(boombox); + museum.ValuablePresets[0].tiny.Add(boombox); + museum.ValuablePresets[0].veryTall.Add(boombox); + museum.ValuablePresets[0].wide.Add(boombox); } [HarmonyPatch(typeof(PlayerController), nameof(PlayerController.FixedUpdate))] @@ -50,6 +110,9 @@ private static void FuckLolEnemy(EnemyDirector __instance) private static void InfiniteSprintPatch(PlayerController __instance) { __instance.EnergyCurrent = __instance.EnergyStart; + + var script = PlayerController.instance?.playerAvatarScript; + if (script != null) script.upgradeTumbleClimb = 100; } [HarmonyPatch(typeof(PlayerHealth), nameof(PlayerHealth.Hurt))] @@ -66,5 +129,29 @@ private static void NoHurtMe(EnemyThinMan __instance) if (__instance.tentacleLerp >= 1f) __instance.tentacleLerp = 0.99f; } + + [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(); + } + + [HarmonyPatch(typeof(SemiFunc), nameof(SemiFunc.DebugTester))] + [HarmonyPostfix] + private static void IAmASurgeonIMeanTester(ref bool __result) + { + __result = true; + } + + [HarmonyPatch(typeof(SemiFunc), nameof(SemiFunc.DebugDev))] + [HarmonyPostfix] + private static void IAmASurgeonIMeanDeveloper(ref bool __result) + { + __result = true; + } } #endif \ No newline at end of file diff --git a/Source/Input/RebindManager.cs b/Source/Input/RebindManager.cs index 6795746..7cb3110 100644 --- a/Source/Input/RebindManager.cs +++ b/Source/Input/RebindManager.cs @@ -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/Managers/VRSession.cs b/Source/Managers/VRSession.cs index e048821..da16a35 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -1,11 +1,8 @@ using RepoXR.Assets; -using RepoXR.Input; using RepoXR.Player; using RepoXR.UI; using UnityEngine; -using UnityEngine.InputSystem; using UnityEngine.InputSystem.UI; -using UnityEngine.InputSystem.XR; namespace RepoXR.Managers; @@ -51,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(); diff --git a/Source/Networking/NetworkSystem.cs b/Source/Networking/NetworkSystem.cs index 6d8ebab..7056f68 100644 --- a/Source/Networking/NetworkSystem.cs +++ b/Source/Networking/NetworkSystem.cs @@ -272,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/Patches/CameraPatches.cs b/Source/Patches/CameraPatches.cs index 0ae849b..4f0c8c5 100644 --- a/Source/Patches/CameraPatches.cs +++ b/Source/Patches/CameraPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using RepoXR.Managers; using UnityEngine; namespace RepoXR.Patches; @@ -80,4 +81,21 @@ 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; + } } \ No newline at end of file diff --git a/Source/Patches/Enemy/EnemyCeilingEyePatches.cs b/Source/Patches/Enemy/EnemyCeilingEyePatches.cs index 6b7be62..c3e118c 100644 --- a/Source/Patches/Enemy/EnemyCeilingEyePatches.cs +++ b/Source/Patches/Enemy/EnemyCeilingEyePatches.cs @@ -57,29 +57,6 @@ private static IEnumerable SetCameraSoftRotationPatch(IEnumerab .InstructionEnumeration(); } - /// - /// 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))) - .InstructionEnumeration(); - } - /// /// Provide haptic feedback while attached to the ceiling eye /// 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/EnemyOoglyPatches.cs b/Source/Patches/Enemy/EnemyOoglyPatches.cs new file mode 100644 index 0000000..8de1b14 --- /dev/null +++ b/Source/Patches/Enemy/EnemyOoglyPatches.cs @@ -0,0 +1,31 @@ +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; + +internal static class EnemyOoglyPatches +{ + [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..18b99e9 --- /dev/null +++ b/Source/Patches/Enemy/EnemySpinnyPatches.cs @@ -0,0 +1,21 @@ +using HarmonyLib; +using RepoXR.Player.Camera; + +namespace RepoXR.Patches.Enemy; + +[RepoXRPatch] +internal static class EnemySpinnyPatches +{ + /// + /// TODO: I don't yet know what this enemy does or looks like + /// + [HarmonyPatch(typeof(EnemySpinny), nameof(EnemySpinny.OverrideTargetPlayerCameraAim))] + [HarmonyPrefix] + private static bool OverrideVRCameraAim(EnemySpinny __instance, float _strenght, float _strenghtNoAim) + { + VRCameraAim.instance.SetAimTargetSoft(__instance.spinnyWheel.position, 0.1f, _strenght, _strenghtNoAim, + __instance.gameObject, 100, Plugin.Config.ReducedAimImpact.Value); + + return false; + } +} \ No newline at end of file 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/PlayerEyesPatches.cs b/Source/Patches/Player/PlayerEyesPatches.cs index e1bfd74..5990cb7 100644 --- a/Source/Patches/Player/PlayerEyesPatches.cs +++ b/Source/Patches/Player/PlayerEyesPatches.cs @@ -16,7 +16,7 @@ internal static class PlayerEyesPatches [HarmonyPostfix] private static void LookAtTransformEyeTracking(PlayerEyes __instance) { - if (__instance.playerAvatar.isLocal) + if (!__instance.playerAvatar || __instance.playerAvatar.isLocal) return; // TODO: Determine if certain occurrences in the game should override eye gaze 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/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/Player/Camera/VRCameraAim.cs b/Source/Player/Camera/VRCameraAim.cs index 4cbdf28..71b6845 100644 --- a/Source/Player/Camera/VRCameraAim.cs +++ b/Source/Player/Camera/VRCameraAim.cs @@ -11,12 +11,13 @@ namespace RepoXR.Player.Camera; 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 +28,7 @@ public class VRCameraAim : MonoBehaviour private bool aimTargetLowImpact; private float aimTargetLerp; - + // Soft aim fields private GameObject? aimTargetSoftObject; private Vector3 aimTargetSoftPosition; @@ -36,18 +37,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 +56,7 @@ private void Awake() private void Update() { // Detect head movement - + if (lastCameraRotation == Quaternion.identity) lastCameraRotation = mainCamera.localRotation; @@ -66,7 +67,7 @@ private void Update() lastCameraRotation = mainCamera.localRotation; // Perform forced rotations - + if (playerAimingTimer > 0) playerAimingTimer -= Time.deltaTime; @@ -75,33 +76,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 +118,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; - return camDelta * transform.rotation; + var qY = Quaternion.LookRotation(localYawFwd, Vector3.up); + var qXZ = Quaternion.Inverse(qY) * localRot; + + return (qY, qXZ); } /// @@ -143,37 +148,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 +206,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 +237,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 +254,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 9bb903e..752534a 100644 --- a/Source/Player/Camera/VRCustomCamera.cs +++ b/Source/Player/Camera/VRCustomCamera.cs @@ -46,6 +46,8 @@ private void Awake() Plugin.Config.CustomCameraFOV.SettingChanged += OnFOVChanged; + Application.onBeforeRender += OnBeforeRender; + UpdateRenderTexture(); } @@ -54,6 +56,13 @@ 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) @@ -79,6 +88,15 @@ private void Update() // Some weird fog thing, I don't know why but this is needed RenderSettings.fogDensity = 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(); @@ -96,13 +114,6 @@ private void Update() uiCamera.Render(); } - private void LateUpdate() - { - // 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; - } - private void UpdateRenderTexture() { lastWidth = Screen.width; 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 1d67b19..65b2ea4 100644 --- a/Source/Player/VRPlayer.cs +++ b/Source/Player/VRPlayer.cs @@ -19,6 +19,7 @@ public class VRPlayer : MonoBehaviour // Tracking stuff private Transform mainCamera; + private Transform handContainer; private Transform leftHand; private Transform rightHand; @@ -59,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(); @@ -156,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; @@ -239,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 dfa1ca5..9208c7a 100644 --- a/Source/Player/VRRig.cs +++ b/Source/Player/VRRig.cs @@ -48,6 +48,7 @@ public class VRRig : MonoBehaviour public Collider rightHandCollider; public Collider mapPickupCollider; public Collider lampTriggerCollider; + public Collider shoulderMapPickupCollider; public VRInventory inventoryController; @@ -56,6 +57,9 @@ public class VRRig : MonoBehaviour public Vector3 mapRightPosition; public Vector3 mapLeftPosition; + public Vector3 shoulderMapRightPosition; + public Vector3 shoulderMapLeftPosition; + private bool armsDetached; private Transform leftArmMesh; @@ -240,7 +244,12 @@ private void UpdateClaw() private Vector3 MapPrimaryPosition => VRSession.IsLeftHanded ? mapLeftPosition : mapRightPosition; private Vector3 MapSecondaryPosition => VRSession.IsLeftHanded ? mapRightPosition : mapLeftPosition; - + + private Vector3 ShoulderMapPrimaryPosition => + VRSession.IsLeftHanded ? shoulderMapLeftPosition : shoulderMapRightPosition; + private Vector3 ShoulderMapSecondaryPosition => + VRSession.IsLeftHanded ? shoulderMapRightPosition : shoulderMapLeftPosition; + private bool mapHovered; private void MapToolLogic() @@ -251,6 +260,9 @@ private void MapToolLogic() // Move map tool anchor to the left if we're holding an item map.transform.localPosition = Vector3.Lerp(map.transform.localPosition, PhysGrabber.instance.grabbed ? MapSecondaryPosition : MapPrimaryPosition, 8 * Time.deltaTime); + shoulderMapPickupCollider.transform.localPosition = PhysGrabber.instance.grabbed + ? ShoulderMapSecondaryPosition + : ShoulderMapPrimaryPosition; mapTool.transform.parent.localPosition = Vector3.Lerp(mapTool.transform.parent.localPosition, Vector3.zero, 5 * Time.deltaTime); @@ -293,7 +305,7 @@ 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, @@ -304,7 +316,8 @@ private void MapToolLogic() // Right hand pickup logic if (!mapTool.Active && Actions.Instance["MapGrabRight"].WasPressedThisFrame() && - Utils.Collide(rightHandCollider, mapPickupCollider) && !PlayerController.instance.sprinting) + (Utils.Collide(rightHandCollider, mapPickupCollider) || + Utils.Collide(rightHandCollider, shoulderMapPickupCollider)) && !PlayerController.instance.sprinting) if (mapTool.HideLerp >= 1) { mapTool.transform.parent.parent = rightHandTip; @@ -315,14 +328,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) || + Utils.Collide(leftHandCollider, shoulderMapPickupCollider)) && !PlayerController.instance.sprinting) if (mapTool.HideLerp >= 1) { mapTool.transform.parent.parent = leftHandTip; @@ -330,11 +344,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; } } diff --git a/Source/Plugin.cs b/Source/Plugin.cs index abdae9a..5511780 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -32,7 +32,7 @@ public class Plugin : BaseUnityPlugin private readonly string[] GAME_ASSEMBLY_HASHES = [ - "E95BEFC4BD5206D9455BAA68C51A950ABAF0A99001012928B3E553D8D0E5CDB3" // v0.2.2 + "137D6E8475DEA976831CC95D7F56F4B7DA311E52A57B4C420591A5122F25589F" // v0.3.0 ]; public new static Config Config { get; private set; } = null!; 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/GameHud.cs b/Source/UI/GameHud.cs index 19cd746..979d24c 100644 --- a/Source/UI/GameHud.cs +++ b/Source/UI/GameHud.cs @@ -161,7 +161,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/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 From b0ea66f0c2e7eff90bd76a782eec475b1ab680b5 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Fri, 7 Nov 2025 10:12:11 +0100 Subject: [PATCH 17/20] Few more 1.1.0 changes - Disabled AO (major performance boost) - Fix one enemy patch not being applied - Bump BepInEx requirement - Add eye tracking debug flag --- CHANGELOG.md | 11 +++-- Source/Experiments.cs | 53 +++++++++++++++------- Source/Patches/Enemy/EnemyOoglyPatches.cs | 4 ++ Source/Patches/Enemy/EnemySpinnyPatches.cs | 5 +- Source/Patches/HarmonyPatcher.cs | 1 + Source/Patches/InputPatches.cs | 3 -- Source/Patches/Player/PlayerEyesPatches.cs | 2 - Source/Player/VREyeTracking.cs | 7 ++- Source/Plugin.cs | 50 +++++++++++--------- Source/Rendering/CustomPostProcessing.cs | 4 ++ 10 files changed, 91 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac47fe5..0edfaec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ # 1.1.0 --- BUGS -- +-- TODOs -- -- Flashlight not aligned (needs more testing) +- LocalPlayerCamera maybe not being synced properly +- Dead head spectate tip might have wrong controls +- Compact canvas UI a little bit (on X axis) so it's easier to see +- Grab map shoulder is only on one side (should be both), also no haptic feedback ## Detached Arms @@ -39,10 +42,12 @@ You can now swap between VR mode and flatscreen mode by pressing the F8 button o - 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) -- Optimized the custom camera by adding a frame rate limiter +- Slightly optimized the custom camera by adding a frame rate limiter +- 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) diff --git a/Source/Experiments.cs b/Source/Experiments.cs index d0ab358..865f6c6 100644 --- a/Source/Experiments.cs +++ b/Source/Experiments.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection.Emit; using HarmonyLib; +using UnityEngine; namespace RepoXR; @@ -10,7 +11,7 @@ namespace RepoXR; internal static class Experiments { [HarmonyPatch(typeof(EnemyDirector), nameof(EnemyDirector.Awake))] - [HarmonyPostfix] + // [HarmonyPostfix] private static void FuckLolEnemy(EnemyDirector __instance) { // Difficulty 1 enemies @@ -50,7 +51,7 @@ private static void FuckLolEnemy(EnemyDirector __instance) private static bool done; [HarmonyPatch(typeof(MenuButton), nameof(MenuButton.OnHovering))] - [HarmonyPrefix] + // [HarmonyPrefix] private static void ForceMap() { if (done) @@ -70,22 +71,28 @@ private static void ForceMap() var boombox = museum.ValuablePresets[0].big[2]; - for (var i = 0; i < museum.ValuablePresets.Count; i++) + foreach (var preset in museum.ValuablePresets) { - for (var j = 0; j < museum.ValuablePresets[i].big.Count; j++) - Logger.LogDebug($"[{museum.ValuablePresets[i]}] Big: {museum.ValuablePresets[i].big[j].prefabName}"); - for (var j = 0; j < museum.ValuablePresets[i].medium.Count; j++) - Logger.LogDebug($"[{museum.ValuablePresets[i]}] Medium: {museum.ValuablePresets[i].medium[j].prefabName}"); - for (var j = 0; j < museum.ValuablePresets[i].small.Count; j++) - Logger.LogDebug($"[{museum.ValuablePresets[i]}] Small: {museum.ValuablePresets[i].small[j].prefabName}"); - for (var j = 0; j < museum.ValuablePresets[i].tall.Count; j++) - Logger.LogDebug($"[{museum.ValuablePresets[i]}] Tall: {museum.ValuablePresets[i].tall[j].prefabName}"); - for (var j = 0; j < museum.ValuablePresets[i].tiny.Count; j++) - Logger.LogDebug($"[{museum.ValuablePresets[i]}] Tiny: {museum.ValuablePresets[i].tiny[j].prefabName}"); - for (var j = 0; j < museum.ValuablePresets[i].veryTall.Count; j++) - Logger.LogDebug($"[{museum.ValuablePresets[i]}] Very Tall: {museum.ValuablePresets[i].veryTall[j].prefabName}"); - for (var j = 0; j < museum.ValuablePresets[i].wide.Count; j++) - Logger.LogDebug($"[{museum.ValuablePresets[i]}] Wide: {museum.ValuablePresets[i].wide[j].prefabName}"); + foreach (var val in preset.big) + Logger.LogDebug($"[{preset}] Big: {val.prefabName}"); + + foreach (var val in preset.medium) + Logger.LogDebug($"[{preset}] Medium: {val.prefabName}"); + + foreach (var val in preset.small) + Logger.LogDebug($"[{preset}] Small: {val.prefabName}"); + + foreach (var val in preset.tall) + Logger.LogDebug($"[{preset}] Tall: {val.prefabName}"); + + foreach (var val in preset.tiny) + Logger.LogDebug($"[{preset}] Tiny: {val.prefabName}"); + + foreach (var val in preset.veryTall) + Logger.LogDebug($"[{preset}] Very Tall: {val.prefabName}"); + + foreach (var val in preset.wide) + Logger.LogDebug($"[{preset}] Wide: {val.prefabName}"); } museum.ValuablePresets[0].big.Clear(); @@ -153,5 +160,17 @@ private static void IAmASurgeonIMeanDeveloper(ref bool __result) { __result = true; } + + [HarmonyPatch(typeof(DebugConsoleUI), nameof(DebugConsoleUI.Update))] + [HarmonyTranspiler] + [HarmonyDebug] + private static IEnumerable KeepEnterKeyThing(IEnumerable instructions) + { + 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)])) + .InstructionEnumeration(); + } } #endif \ No newline at end of file diff --git a/Source/Patches/Enemy/EnemyOoglyPatches.cs b/Source/Patches/Enemy/EnemyOoglyPatches.cs index 8de1b14..e1758b9 100644 --- a/Source/Patches/Enemy/EnemyOoglyPatches.cs +++ b/Source/Patches/Enemy/EnemyOoglyPatches.cs @@ -8,8 +8,12 @@ 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) diff --git a/Source/Patches/Enemy/EnemySpinnyPatches.cs b/Source/Patches/Enemy/EnemySpinnyPatches.cs index 18b99e9..0b7dc03 100644 --- a/Source/Patches/Enemy/EnemySpinnyPatches.cs +++ b/Source/Patches/Enemy/EnemySpinnyPatches.cs @@ -7,14 +7,15 @@ namespace RepoXR.Patches.Enemy; internal static class EnemySpinnyPatches { /// - /// TODO: I don't yet know what this enemy does or looks like + /// 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, Plugin.Config.ReducedAimImpact.Value); + __instance.gameObject, 100, true); return false; } diff --git a/Source/Patches/HarmonyPatcher.cs b/Source/Patches/HarmonyPatcher.cs index 9d8798d..26de57a 100644 --- a/Source/Patches/HarmonyPatcher.cs +++ b/Source/Patches/HarmonyPatcher.cs @@ -87,6 +87,7 @@ internal enum RepoXRPatchTarget /// https://github.com/BepInEx/HarmonyX/blob/master/Harmony/Internal/Patching/ILManipulator.cs#L322 /// Licensed under MIT: https://github.com/BepInEx/HarmonyX/blob/master/LICENSE /// +// TODO: It is unclear how BepInEx currently chooses Harmony versions, so we keep this patch in for now [RepoXRPatch(RepoXRPatchTarget.Universal)] [HarmonyPriority(Priority.First)] internal static class LeaveMyLeaveAlonePatch diff --git a/Source/Patches/InputPatches.cs b/Source/Patches/InputPatches.cs index 33b26c7..64f0e01 100644 --- a/Source/Patches/InputPatches.cs +++ b/Source/Patches/InputPatches.cs @@ -190,10 +190,7 @@ private static bool KeyPullAndPush(ref float __result) var pull = Actions.Instance["Pull"].ReadValue(); if (pull > 0) - { __result = -pull; - return false; - } return false; } diff --git a/Source/Patches/Player/PlayerEyesPatches.cs b/Source/Patches/Player/PlayerEyesPatches.cs index 5990cb7..1b6d439 100644 --- a/Source/Patches/Player/PlayerEyesPatches.cs +++ b/Source/Patches/Player/PlayerEyesPatches.cs @@ -19,8 +19,6 @@ private static void LookAtTransformEyeTracking(PlayerEyes __instance) if (!__instance.playerAvatar || __instance.playerAvatar.isLocal) return; - // TODO: Determine if certain occurrences in the game should override eye gaze - if (!NetworkSystem.instance.GetNetworkPlayer(__instance.playerAvatar, out var player) || !player.EyeTracking) return; diff --git a/Source/Player/VREyeTracking.cs b/Source/Player/VREyeTracking.cs index 9c6a695..8ad506a 100644 --- a/Source/Player/VREyeTracking.cs +++ b/Source/Player/VREyeTracking.cs @@ -28,7 +28,9 @@ private void Awake() Plugin.Config.EnableEyeTracking.SettingChanged += OnEyeTrackingSettingChanged; - // TODO: Remove once tested with real hardware + if (!Plugin.Flags.HasFlag(Flags.EyeTrackingDebug)) + return; + debugCube = Instantiate(AssetCollection.Cube).transform; debugCube.GetComponent().material.color = Color.blue; debugCube.GetComponent().enabled = false; @@ -101,6 +103,9 @@ private void Update() NetworkSystem.instance.UpdateEyeTracking(position); + if (!debugCube) + return; + debugCube.position = position; debugCube.rotation = Quaternion.LookRotation(ray.direction); } diff --git a/Source/Plugin.cs b/Source/Plugin.cs index 5511780..ac87d93 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -21,20 +21,21 @@ public class Plugin : BaseUnityPlugin public const string PLUGIN_GUID = "io.daxcess.repoxr"; public const string PLUGIN_NAME = "RepoXR"; public const string PLUGIN_VERSION = "1.1.0"; - - #if DEBUG + +#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 = [ "137D6E8475DEA976831CC95D7F56F4B7DA311E52A57B4C420591A5122F25589F" // v0.3.0 ]; - + public new static Config Config { get; private set; } = null!; public static Flags Flags { get; private set; } = 0; @@ -105,6 +106,9 @@ private void Awake() } #endif + if (Environment.GetCommandLineArgs().Contains("--repoxr-debug-eyetracking", StringComparer.OrdinalIgnoreCase)) + Flags |= Flags.EyeTrackingDebug; + Native.BringGameWindowToFront(); Config.SetupGlobalCallbacks(); @@ -112,7 +116,7 @@ private void Awake() } 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 @@ -206,8 +210,9 @@ 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; } @@ -231,7 +236,7 @@ public static void ToggleVR() Flags |= Flags.VR; } } - + private static bool InitializeVR() { RepoXR.Logger.LogInfo("Loading VR..."); @@ -245,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; } @@ -276,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 5a8f696..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; @@ -22,6 +23,9 @@ private void Start() // Disable original shaders postProcessing.vignette.enabled.value = false; + + // Disable ambient occlusion (big performance boost) + postProcessing.GetComponent().profile.GetSetting().enabled.value = false; } private void Update() From d9f84600c56f30cb6086f9103c623d7a5edf7301 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Sat, 8 Nov 2025 13:36:05 +0100 Subject: [PATCH 18/20] Fix remaining known bugs --- CHANGELOG.md | 7 -- RepoXR.csproj | 2 +- Source/Entrypoint.cs | 5 +- Source/Experiments.cs | 125 ++------------------------- Source/Patches/CameraPatches.cs | 22 +++++ Source/Patches/HarmonyPatcher.cs | 5 +- Source/Patches/UI/TutorialPatches.cs | 10 +++ Source/Player/VRRig.cs | 27 ++---- Source/Plugin.cs | 2 +- Source/UI/GameHud.cs | 6 +- Source/Utils.cs | 7 +- 11 files changed, 60 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0edfaec..917070d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,5 @@ # 1.1.0 --- TODOs -- - -- LocalPlayerCamera maybe not being synced properly -- Dead head spectate tip might have wrong controls -- Compact canvas UI a little bit (on X axis) so it's easier to see -- Grab map shoulder is only on one side (should be both), also no haptic feedback - ## 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. diff --git a/RepoXR.csproj b/RepoXR.csproj index bc29226..f6013f4 100644 --- a/RepoXR.csproj +++ b/RepoXR.csproj @@ -35,7 +35,7 @@ - + diff --git a/Source/Entrypoint.cs b/Source/Entrypoint.cs index b770703..cd99b22 100644 --- a/Source/Entrypoint.cs +++ b/Source/Entrypoint.cs @@ -38,10 +38,7 @@ internal 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); diff --git a/Source/Experiments.cs b/Source/Experiments.cs index 865f6c6..1018abb 100644 --- a/Source/Experiments.cs +++ b/Source/Experiments.cs @@ -10,108 +10,6 @@ namespace RepoXR; #if DEBUG internal static class Experiments { - [HarmonyPatch(typeof(EnemyDirector), nameof(EnemyDirector.Awake))] - // [HarmonyPostfix] - private static void FuckLolEnemy(EnemyDirector __instance) - { - // Difficulty 1 enemies - var ceileingEye = __instance.enemiesDifficulty1[0]; - var thinMan = __instance.enemiesDifficulty1[1]; - var gnome = __instance.enemiesDifficulty1[2]; - var duck = __instance.enemiesDifficulty1[3]; - var slowMouth = __instance.enemiesDifficulty1[4]; - - // Difficulty 2 enemies - var valuableThrower = __instance.enemiesDifficulty2[0]; - var animal = __instance.enemiesDifficulty2[1]; - var upscream = __instance.enemiesDifficulty2[2]; - var hidden = __instance.enemiesDifficulty2[3]; - var tumbler = __instance.enemiesDifficulty2[4]; - var bowtie = __instance.enemiesDifficulty2[5]; - var floater = __instance.enemiesDifficulty2[6]; - var bang = __instance.enemiesDifficulty2[7]; - - // Difficulty 3 enemies - var head = __instance.enemiesDifficulty3[0]; - var robe = __instance.enemiesDifficulty3[1]; - var hunter = __instance.enemiesDifficulty3[2]; - var runner = __instance.enemiesDifficulty3[3]; - var beamer = __instance.enemiesDifficulty3[4]; - var slowWalker = __instance.enemiesDifficulty3[5]; - - __instance.enemiesDifficulty1.Clear(); - __instance.enemiesDifficulty2.Clear(); - __instance.enemiesDifficulty3.Clear(); - - __instance.enemiesDifficulty1.Add(slowMouth); - __instance.enemiesDifficulty2.Add(slowMouth); - __instance.enemiesDifficulty3.Add(slowMouth); - } - - private static bool done; - - [HarmonyPatch(typeof(MenuButton), nameof(MenuButton.OnHovering))] - // [HarmonyPrefix] - private static void ForceMap() - { - if (done) - return; - - done = true; - - var mgr = RunManager.instance; - - var station = mgr.levels[0]; - var manor = mgr.levels[1]; - var museum = mgr.levels[2]; - var hogwarts = mgr.levels[3]; - - mgr.levels.Clear(); - mgr.levels.Add(museum); - - var boombox = museum.ValuablePresets[0].big[2]; - - foreach (var preset in museum.ValuablePresets) - { - foreach (var val in preset.big) - Logger.LogDebug($"[{preset}] Big: {val.prefabName}"); - - foreach (var val in preset.medium) - Logger.LogDebug($"[{preset}] Medium: {val.prefabName}"); - - foreach (var val in preset.small) - Logger.LogDebug($"[{preset}] Small: {val.prefabName}"); - - foreach (var val in preset.tall) - Logger.LogDebug($"[{preset}] Tall: {val.prefabName}"); - - foreach (var val in preset.tiny) - Logger.LogDebug($"[{preset}] Tiny: {val.prefabName}"); - - foreach (var val in preset.veryTall) - Logger.LogDebug($"[{preset}] Very Tall: {val.prefabName}"); - - foreach (var val in preset.wide) - Logger.LogDebug($"[{preset}] Wide: {val.prefabName}"); - } - - museum.ValuablePresets[0].big.Clear(); - museum.ValuablePresets[0].medium.Clear(); - museum.ValuablePresets[0].small.Clear(); - museum.ValuablePresets[0].tall.Clear(); - museum.ValuablePresets[0].tiny.Clear(); - museum.ValuablePresets[0].veryTall.Clear(); - museum.ValuablePresets[0].wide.Clear(); - - museum.ValuablePresets[0].big.Add(boombox); - museum.ValuablePresets[0].medium.Add(boombox); - museum.ValuablePresets[0].small.Add(boombox); - museum.ValuablePresets[0].tall.Add(boombox); - museum.ValuablePresets[0].tiny.Add(boombox); - museum.ValuablePresets[0].veryTall.Add(boombox); - museum.ValuablePresets[0].wide.Add(boombox); - } - [HarmonyPatch(typeof(PlayerController), nameof(PlayerController.FixedUpdate))] [HarmonyPostfix] private static void InfiniteSprintPatch(PlayerController __instance) @@ -122,21 +20,6 @@ private static void InfiniteSprintPatch(PlayerController __instance) if (script != null) script.upgradeTumbleClimb = 100; } - [HarmonyPatch(typeof(PlayerHealth), nameof(PlayerHealth.Hurt))] - [HarmonyPrefix] - private static bool NoDamage() - { - return false; - } - - [HarmonyPatch(typeof(EnemyThinMan), nameof(EnemyThinMan.TentacleLogic))] - [HarmonyPostfix] - private static void NoHurtMe(EnemyThinMan __instance) - { - if (__instance.tentacleLerp >= 1f) - __instance.tentacleLerp = 0.99f; - } - [HarmonyPatch(typeof(SpectateCamera), nameof(SpectateCamera.HeadEnergyLogic))] [HarmonyTranspiler] private static IEnumerable FastRechargeHead(IEnumerable instructions) @@ -163,13 +46,17 @@ private static void IAmASurgeonIMeanDeveloper(ref bool __result) [HarmonyPatch(typeof(DebugConsoleUI), nameof(DebugConsoleUI.Update))] [HarmonyTranspiler] - [HarmonyDebug] private static IEnumerable KeepEnterKeyThing(IEnumerable instructions) { 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)])) + .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(); } } diff --git a/Source/Patches/CameraPatches.cs b/Source/Patches/CameraPatches.cs index 4f0c8c5..53af970 100644 --- a/Source/Patches/CameraPatches.cs +++ b/Source/Patches/CameraPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using Photon.Pun; using RepoXR.Managers; using UnityEngine; @@ -98,4 +99,25 @@ private static void AlignWithVRCameraPatch(PlayerLocalCamera __instance) __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/HarmonyPatcher.cs b/Source/Patches/HarmonyPatcher.cs index 26de57a..51005c0 100644 --- a/Source/Patches/HarmonyPatcher.cs +++ b/Source/Patches/HarmonyPatcher.cs @@ -82,12 +82,13 @@ 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 /// -// TODO: It is unclear how BepInEx currently chooses Harmony versions, so we keep this patch in for now [RepoXRPatch(RepoXRPatchTarget.Universal)] [HarmonyPriority(Priority.First)] internal static class LeaveMyLeaveAlonePatch 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/Player/VRRig.cs b/Source/Player/VRRig.cs index 9208c7a..c7ce38f 100644 --- a/Source/Player/VRRig.cs +++ b/Source/Player/VRRig.cs @@ -48,7 +48,7 @@ public class VRRig : MonoBehaviour public Collider rightHandCollider; public Collider mapPickupCollider; public Collider lampTriggerCollider; - public Collider shoulderMapPickupCollider; + public Collider[] shoulderMapPickupColliders; public VRInventory inventoryController; @@ -57,9 +57,6 @@ public class VRRig : MonoBehaviour public Vector3 mapRightPosition; public Vector3 mapLeftPosition; - public Vector3 shoulderMapRightPosition; - public Vector3 shoulderMapLeftPosition; - private bool armsDetached; private Transform leftArmMesh; @@ -245,11 +242,6 @@ private void UpdateClaw() private Vector3 MapPrimaryPosition => VRSession.IsLeftHanded ? mapLeftPosition : mapRightPosition; private Vector3 MapSecondaryPosition => VRSession.IsLeftHanded ? mapRightPosition : mapLeftPosition; - private Vector3 ShoulderMapPrimaryPosition => - VRSession.IsLeftHanded ? shoulderMapLeftPosition : shoulderMapRightPosition; - private Vector3 ShoulderMapSecondaryPosition => - VRSession.IsLeftHanded ? shoulderMapRightPosition : shoulderMapLeftPosition; - private bool mapHovered; private void MapToolLogic() @@ -260,9 +252,6 @@ private void MapToolLogic() // Move map tool anchor to the left if we're holding an item map.transform.localPosition = Vector3.Lerp(map.transform.localPosition, PhysGrabber.instance.grabbed ? MapSecondaryPosition : MapPrimaryPosition, 8 * Time.deltaTime); - shoulderMapPickupCollider.transform.localPosition = PhysGrabber.instance.grabbed - ? ShoulderMapSecondaryPosition - : ShoulderMapPrimaryPosition; mapTool.transform.parent.localPosition = Vector3.Lerp(mapTool.transform.parent.localPosition, Vector3.zero, 5 * Time.deltaTime); @@ -287,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) @@ -309,15 +298,15 @@ private void MapToolLogic() // 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) || - Utils.Collide(rightHandCollider, shoulderMapPickupCollider)) && !PlayerController.instance.sprinting) + Utils.Collide(rightHandCollider, [mapPickupCollider, ..shoulderMapPickupColliders]) && + !PlayerController.instance.sprinting) if (mapTool.HideLerp >= 1) { mapTool.transform.parent.parent = rightHandTip; @@ -335,8 +324,8 @@ private void MapToolLogic() // Left hand pickup logic if (!mapTool.Active && Actions.Instance["MapGrabLeft"].WasPressedThisFrame() && - (Utils.Collide(leftHandCollider, mapPickupCollider) || - Utils.Collide(leftHandCollider, shoulderMapPickupCollider)) && !PlayerController.instance.sprinting) + Utils.Collide(leftHandCollider, [mapPickupCollider, ..shoulderMapPickupColliders]) && + !PlayerController.instance.sprinting) if (mapTool.HideLerp >= 1) { mapTool.transform.parent.parent = leftHandTip; diff --git a/Source/Plugin.cs b/Source/Plugin.cs index ac87d93..9f425ad 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -72,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}"); diff --git a/Source/UI/GameHud.cs b/Source/UI/GameHud.cs index 979d24c..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); } /// 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) From 9bc8934b572aa74c848b561488f09fec3b3f5135 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Wed, 12 Nov 2025 21:27:16 +0100 Subject: [PATCH 19/20] (final commit?) Button to remove bindings --- CHANGELOG.md | 3 ++- Source/Config.cs | 2 +- Source/Player/Camera/VRCameraAim.cs | 3 --- Source/UI/Controls/ControlOption.cs | 12 ++++++++++++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 917070d..5228398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,11 +39,12 @@ You can now swap between VR mode and flatscreen mode by pressing the F8 button o - 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 +- 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 diff --git a/Source/Config.cs b/Source/Config.cs index 054735a..5d6eb05 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -112,7 +112,7 @@ public class Config(string assemblyPath, ConfigFile file) [ConfigDescriptor(stepSize: 15, pointerSize: 5)] public ConfigEntry CustomCameraFramerate { get; } = file.Bind("Rendering", nameof(CustomCameraFramerate), - 60f, + 120f, 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, 120))); diff --git a/Source/Player/Camera/VRCameraAim.cs b/Source/Player/Camera/VRCameraAim.cs index 71b6845..fb560dc 100644 --- a/Source/Player/Camera/VRCameraAim.cs +++ b/Source/Player/Camera/VRCameraAim.cs @@ -5,9 +5,6 @@ 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; diff --git a/Source/UI/Controls/ControlOption.cs b/Source/UI/Controls/ControlOption.cs index 1bd749b..0a93874 100644 --- a/Source/UI/Controls/ControlOption.cs +++ b/Source/UI/Controls/ControlOption.cs @@ -30,6 +30,18 @@ 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); From ccdc7af736d23d573704e844bd164eb6b535a74e Mon Sep 17 00:00:00 2001 From: DaXcess Date: Thu, 13 Nov 2025 11:50:37 +0100 Subject: [PATCH 20/20] Increase max custom cam framerate for Steam Frame --- Source/Config.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Config.cs b/Source/Config.cs index 5d6eb05..330a252 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -112,10 +112,10 @@ public class Config(string assemblyPath, ConfigFile file) [ConfigDescriptor(stepSize: 15, pointerSize: 5)] public ConfigEntry CustomCameraFramerate { get; } = file.Bind("Rendering", nameof(CustomCameraFramerate), - 120f, + 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, 120))); + new AcceptableValueRange(15, 144))); [ConfigDescriptor(stepSize: 5)] public ConfigEntry CustomCameraFOV { get; } = file.Bind("Rendering", nameof(CustomCameraFOV), 75f,