diff --git a/CHANGELOG.md b/CHANGELOG.md
index 647df8c..5228398 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,54 @@
+# 1.1.0
+
+## Detached Arms
+
+You can now detach your arms from your body, giving you more control over your aim, not being constrained by the maximum length of the Semibot's arms anymore. You can change this setting at any time, no restarts or rejoins needed.
+
+## Eye Tracking
+
+RepoXR v1.1.0 adds support for eye tracking (for the three people that have it).
+
+#### Singleplayer
+
+Players that use eye tracking will have enhanced immersion when it comes to "looking at" things. A few of the enemies in R.E.P.O. behave differently depending on if you're looking at them or not. This detection now factors in where you are looking with your eyes!
+
+Discovering valuables, or your lost friends' heads will also make use of eye tracking. Just look at the items and the game will discover them without you having to move your head.
+
+#### Multiplayer
+
+When playing with other people, other people will see your pupils move based on your real eye movement. Looking down? People will see you look down. Looking straight through your friend's soul because they broke something? Yup, they'll see that!
+
+#### Configuration
+
+Eye tracking can be enabled and disabled mid-game, there's no need to restart. If your headset does not support eye tracking, it will be considered disabled (even when eye tracking is enabled in the config), so no need to change the settings if you don't have eye tracking.
+
+## Hotswap
+
+You can now swap between VR mode and flatscreen mode by pressing the F8 button on your keyboard while you are in the main menu or in the lobby menu. This works even when you are the host (though the lobby will reload for everybody).
+
+**Additions**:
+- Added eye tracking support
+- Added an option to detach arms from body
+- Added support for the new climbing mechanic
+- Added support for spectating your death head
+- Added support for the monster update
+- Added hotswapping in the main menu (F8)
+
+**Changes**:
+- The ceiling eye now darkens the world except for where the eye is (no more cheating hihi)
+- Removed the performance tab and replaced it with UI in the settings
+- Replaced the valuable discover overlay with a new 3D graphic (supports custom colors)
+- You now look at the enemy/object that killed you while the death animation plays (if possible)
+- Slightly optimized the custom camera by adding a frame rate limiter (disabled by default)
+- Optimized framerate by forcibly disabling ambient occlusion (20%-40% less render time)
+- Renamed "Dynamic Smooth Speed" to "Analog Smooth Turn"
+- Changed the minimum possible HUD height value to account for detached hands
+- The map tool can now also be grabbed from behind your head (near the shoulders)
+- You can now unbind controls at your leisure
+
+**Removals**:
+- Removed support for REPO v0.2.x
+
# 1.0.3
**Additions**:
diff --git a/Preload/RepoXR.Preload.csproj b/Preload/RepoXR.Preload.csproj
index fccda4f..3f3e826 100644
--- a/Preload/RepoXR.Preload.csproj
+++ b/Preload/RepoXR.Preload.csproj
@@ -4,7 +4,7 @@
netstandard2.1
RepoXR Preloader
DaXcess
- 1.0.3
+ 1.1.0
true
latest
RepoXR.Preload
diff --git a/README.md b/README.md
index 85be51a..7093afa 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ Here is a list of RepoXR versions and which version(s) of R.E.P.O. it supports
| RepoXR | R.E.P.O. Version |
|--------|------------------|
+| v1.1.0 | v0.3.0 |
| v1.0.3 | v0.2.1 |
| v1.0.2 | v0.2.1 |
| v1.0.1 | v0.2.1 |
diff --git a/RepoXR.csproj b/RepoXR.csproj
index 37d45e9..f6013f4 100644
--- a/RepoXR.csproj
+++ b/RepoXR.csproj
@@ -2,7 +2,7 @@
Collecting Valuables in VR
- 1.0.3
+ 1.1.0
DaXcess
true
latest
@@ -28,13 +28,14 @@
-
+
+
-
-
+
+
diff --git a/Source/Assets/AssetCollection.cs b/Source/Assets/AssetCollection.cs
index 45d300c..c423dd4 100644
--- a/Source/Assets/AssetCollection.cs
+++ b/Source/Assets/AssetCollection.cs
@@ -11,6 +11,8 @@ internal static class AssetCollection
{
private static AssetBundle assetBundle;
+ public static OpenXRFeaturePack OpenXRFeatures;
+
public static RemappableControls RemappableControls;
public static GameObject RebindHeader;
@@ -21,6 +23,8 @@ internal static class AssetCollection
public static GameObject VRTumble;
public static GameObject Keyboard;
public static GameObject ExpressionWheel;
+ public static GameObject ValuableDiscover;
+ public static GameObject FocusSphere;
public static GameObject MenuSettings;
public static GameObject MenuSettingsCategory;
@@ -47,6 +51,8 @@ internal static class AssetCollection
public static AnimationCurveData HurtHapticCurve;
public static AnimationCurveData EyeAttachHapticCurve;
public static AnimationCurveData KeyboardAnimation;
+
+ public static GameObject Cube;
public static bool LoadAssets()
{
@@ -59,6 +65,8 @@ public static bool LoadAssets()
return false;
}
+ OpenXRFeatures = assetBundle.LoadAsset("OpenXRFeatures");
+
RemappableControls = assetBundle.LoadAsset("RemappableControls").GetComponent();
RebindHeader = assetBundle.LoadAsset("Rebind Header");
@@ -69,6 +77,8 @@ public static bool LoadAssets()
VRTumble = assetBundle.LoadAsset("VRTumble");
Keyboard = assetBundle.LoadAsset("NonNativeKeyboard");
ExpressionWheel = assetBundle.LoadAsset("Expression Radial");
+ ValuableDiscover = assetBundle.LoadAsset("Valuable Discover");
+ FocusSphere = assetBundle.LoadAsset("Focus Sphere");
MenuSettings = assetBundle.LoadAsset("VR Settings Page");
MenuSettingsCategory = assetBundle.LoadAsset("VR Settings Page - Category");
@@ -96,6 +106,8 @@ public static bool LoadAssets()
EyeAttachHapticCurve = assetBundle.LoadAsset("EyeAttachHapticCurve");
KeyboardAnimation = assetBundle.LoadAsset("KeyboardAnimation");
+ Cube = assetBundle.LoadAsset("JustACube");
+
if (RemappableControls?.controls == null)
{
Logger.LogError(
diff --git a/Source/Compat.cs b/Source/Compat.cs
index 199acdf..feb9f0c 100644
--- a/Source/Compat.cs
+++ b/Source/Compat.cs
@@ -5,6 +5,7 @@ namespace RepoXR;
public static class Compat
{
public const string UnityExplorer = "com.sinai.unityexplorer";
+ public const string CustomDiscoverStateLib = "Kistras-CustomDiscoverStateLib";
public static bool IsLoaded(string modId)
{
diff --git a/Source/Config.cs b/Source/Config.cs
index 39ef093..330a252 100644
--- a/Source/Config.cs
+++ b/Source/Config.cs
@@ -38,6 +38,10 @@ public class Config(string assemblyPath, ConfigFile file)
public ConfigEntry LeftHandDominant { get; } = file.Bind("Gameplay", nameof(LeftHandDominant), false,
"Whether to use the left or right hand as dominant hand (the hand used to pick up items)");
+ [ConfigDescriptor(customName: "Arms", falseText: "Attached", trueText: "Detached")]
+ public ConfigEntry DetachedArms { get; } = file.Bind("Gameplay", nameof(DetachedArms), false,
+ "Whether your arms are attached to your body, or if they are separate");
+
[ConfigDescriptor]
public ConfigEntry HapticFeedback { get; } =
file.Bind("Gameplay", nameof(HapticFeedback), HapticFeedbackOption.All,
@@ -45,29 +49,28 @@ public class Config(string assemblyPath, ConfigFile file)
"Controls how much haptic feedback you will experience while playing with the VR mod.",
new AcceptableValueEnum()));
- [ConfigDescriptor(pointerSize: 0.01f, stepSize: 0.05f)]
- public ConfigEntry HUDPlaneOffset { get; } = file.Bind("Gameplay", nameof(HUDPlaneOffset), -0.45f,
- new ConfigDescription("The default height offset for the HUD", new AcceptableValueRange(-0.6f, 0.5f)));
+ [ConfigDescriptor(customName: "Eye Tracking", trueText: "Enabled", falseText: "Disabled")]
+ public ConfigEntry EnableEyeTracking { get; } = file.Bind("Gameplay", nameof(EnableEyeTracking), true,
+ "If supported by the headset, use eye tracking to move your characters pupils for other players and for checking line of sight with enemies.");
+
+ // UI configuration
+
+ [ConfigDescriptor(customName: "HUD Height", pointerSize: 0.01f, stepSize: 0.05f)]
+ public ConfigEntry HUDPlaneOffset { get; } = file.Bind("UI", nameof(HUDPlaneOffset), -0.45f,
+ new ConfigDescription("The default height offset for the HUD", new AcceptableValueRange(-1f, 0.5f)));
- [ConfigDescriptor(pointerSize: 0.01f, stepSize: 0.05f)]
- public ConfigEntry HUDGazePlaneOffset { get; } = file.Bind("Gameplay", nameof(HUDGazePlaneOffset), -0.25f,
- new ConfigDescription("The height offset for the HUD when looking at it", new AcceptableValueRange(-0.6f, 0.5f)));
+ [ConfigDescriptor(customName: "HUD Secondary Height", pointerSize: 0.01f, stepSize: 0.05f)]
+ public ConfigEntry HUDGazePlaneOffset { get; } = file.Bind("UI", nameof(HUDGazePlaneOffset), -0.25f,
+ new ConfigDescription("The height offset for the HUD when looking at it",
+ new AcceptableValueRange(-1f, 0.5f)));
[ConfigDescriptor(pointerSize: 0.05f, stepSize: 0.25f)]
- public ConfigEntry SmoothCanvasDistance { get; } = file.Bind("Gameplay", nameof(SmoothCanvasDistance), 1.5f,
+ public ConfigEntry SmoothCanvasDistance { get; } = file.Bind("UI", nameof(SmoothCanvasDistance), 1.5f,
new ConfigDescription("The distance that the smooth canvas should be away from the main camera",
new AcceptableValueRange(1.25f, 3)));
- // Performance configuration
-
- [ConfigDescriptor(stepSize: 5f, suffix: "%")]
- public ConfigEntry CameraResolution { get; } = file.Bind("Performance", nameof(CameraResolution), 100,
- new ConfigDescription(
- "This setting configures the resolution scale of the game, lower values are more performant, but will make the game look worse.",
- new AcceptableValueRange(5, 200)));
-
// Input configuration
-
+
[ConfigDescriptor(enumDisableBar: true)]
public ConfigEntry TurnProvider { get; } = file.Bind("Input", nameof(TurnProvider),
TurnProviderOption.Smooth,
@@ -81,7 +84,7 @@ public class Config(string assemblyPath, ConfigFile file)
new AcceptableValueRange(0.25f, 5)));
[ConfigDescriptor]
- public ConfigEntry DynamicSmoothSpeed { get; } = file.Bind("Input", nameof(DynamicSmoothSpeed), true,
+ public ConfigEntry AnalogSmoothTurn { get; } = file.Bind("Input", nameof(AnalogSmoothTurn), true,
"When enabled, makes the speed of the smooth turning dependent on how far the analog stick is pushed.");
[ConfigDescriptor(stepSize: 5, suffix: "°")]
@@ -92,6 +95,12 @@ public class Config(string assemblyPath, ConfigFile file)
// Rendering configuration
+ [ConfigDescriptor(stepSize: 5f, suffix: "%")]
+ public ConfigEntry CameraResolution { get; } = file.Bind("Rendering", nameof(CameraResolution), 100,
+ new ConfigDescription(
+ "This setting configures the resolution scale of the game, lower values are more performant, but will make the game look worse.",
+ new AcceptableValueRange(5, 200)));
+
[ConfigDescriptor]
public ConfigEntry Vignette { get; } = file.Bind("Rendering", nameof(Vignette), true,
"Enables the vignette shader used in certain scenarios and levels in the game.");
@@ -101,6 +110,13 @@ public class Config(string assemblyPath, ConfigFile file)
file.Bind("Rendering", nameof(CustomCamera), false,
"Adds a second camera mounted on top of the VR camera that will render separately from the VR camera to the display. This requires extra GPU power!");
+ [ConfigDescriptor(stepSize: 15, pointerSize: 5)]
+ public ConfigEntry CustomCameraFramerate { get; } = file.Bind("Rendering", nameof(CustomCameraFramerate),
+ 144f,
+ new ConfigDescription(
+ "The maximum frequency that the custom camera can render at. The custom camera framerate is limited to the VR headset's refresh rate, so setting this higher won't have any effect.",
+ new AcceptableValueRange(15, 144)));
+
[ConfigDescriptor(stepSize: 5)]
public ConfigEntry CustomCameraFOV { get; } = file.Bind("Rendering", nameof(CustomCameraFOV), 75f,
new ConfigDescription("The field of view that the custom camera should have.",
@@ -124,15 +140,12 @@ public class Config(string assemblyPath, ConfigFile file)
"FOR INTERNAL USE ONLY, DO NOT EDIT");
private static bool leftHandedWarningShown;
-
+
///
/// Create persistent callbacks that persist for the entire duration of the application
///
public void SetupGlobalCallbacks()
{
- if (!VRSession.InVR)
- return;
-
CameraResolution.SettingChanged += (_, _) =>
{
XRSettings.eyeTextureResolutionScale = CameraResolution.Value / 100f;
@@ -140,6 +153,9 @@ public void SetupGlobalCallbacks()
CustomCamera.SettingChanged += (_, _) =>
{
+ if (!VRSession.InVR)
+ return;
+
if (CustomCamera.Value)
Object.Instantiate(AssetCollection.CustomCamera, Camera.main!.transform.parent);
else
diff --git a/Source/Data/OpenXRFeaturePack.cs b/Source/Data/OpenXRFeaturePack.cs
new file mode 100644
index 0000000..ff5009f
--- /dev/null
+++ b/Source/Data/OpenXRFeaturePack.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.XR.OpenXR.Features;
+
+namespace RepoXR.Data;
+
+public class OpenXRFeaturePack : ScriptableObject
+{
+ [SerializeReference] private List features = [];
+
+ public IReadOnlyList Features => features;
+}
\ No newline at end of file
diff --git a/Source/Entrypoint.cs b/Source/Entrypoint.cs
index 6ac78ef..cd99b22 100644
--- a/Source/Entrypoint.cs
+++ b/Source/Entrypoint.cs
@@ -4,10 +4,9 @@
using RepoXR.Managers;
using RepoXR.Networking;
using RepoXR.Patches;
+using RepoXR.Player.Camera;
using RepoXR.UI;
using UnityEngine;
-using UnityEngine.InputSystem;
-using UnityEngine.InputSystem.XR;
using UnityEngine.Rendering.PostProcessing;
using UnityEngine.UI;
@@ -16,18 +15,10 @@ namespace RepoXR;
[RepoXRPatch]
internal static class Entrypoint
{
- public static void OnSceneLoad(string _)
- {
- if (Plugin.Flags.HasFlag(Flags.VR))
- SetupDefaultSceneVR();
-
- SetupDefaultSceneUniversal();
- }
-
///
/// The default setup for VR for every scene
///
- private static void SetupDefaultSceneVR()
+ internal static void SetupDefaultSceneVR()
{
// We grab all these references manually as most of the instances aren't set yet
// Since most of them run in the "Start" lifetime function
@@ -47,10 +38,7 @@ private static void SetupDefaultSceneVR()
var mainCamera = Camera.main!;
// Add tracking to camera
- var poseDriver = mainCamera.gameObject.AddComponent();
- poseDriver.positionAction = Actions.Instance.HeadPosition;
- poseDriver.rotationAction = Actions.Instance.HeadRotation;
- poseDriver.trackingStateInput = new InputActionProperty(Actions.Instance.HeadTrackingState);
+ mainCamera.gameObject.AddComponent();
// Parent overlay to main camera
overlayCamera.transform.SetParent(mainCamera.transform, false);
@@ -131,22 +119,6 @@ private static void SetupDefaultSceneVR()
new GameObject("Data Manager").AddComponent();
}
- private static bool hasShownErrorMessage;
-
- ///
- /// The default setup for every scene (including for non-vr players)
- ///
- private static void SetupDefaultSceneUniversal()
- {
- new GameObject("RepoXR Network System").AddComponent();
-
- ShowVRFailedWarning();
-
-#if DEBUG
- ShowEarlyAccessWarning();
-#endif
- }
-
///
/// is always present in the `Main` scene, so we use it as a base entrypoint
///
@@ -154,7 +126,7 @@ private static void SetupDefaultSceneUniversal()
[HarmonyPostfix]
private static void OnStartup(GameDirector __instance)
{
- VRInputSystem.instance.ActivateInput();
+ VRInputSystem.Instance.ActivateInput();
if (RunManager.instance.levelCurrent == RunManager.instance.levelMainMenu ||
RunManager.instance.levelCurrent == RunManager.instance.levelSplashScreen)
@@ -205,7 +177,49 @@ private static void OnStartupInGame()
{
GameDirector.instance.gameObject.AddComponent();
}
+}
+[RepoXRPatch(RepoXRPatchTarget.Universal)]
+internal static class UniversalEntrypoint
+{
+ private static bool hasShownErrorMessage;
+
+ public static void OnSceneLoad(string _)
+ {
+ if (Plugin.Flags.HasFlag(Flags.VR))
+ Entrypoint.SetupDefaultSceneVR();
+
+ SetupDefaultSceneUniversal();
+ }
+
+ ///
+ /// Enable hotswapping while in the main menu
+ ///
+ [HarmonyPatch(typeof(GameDirector), nameof(GameDirector.Start))]
+ [HarmonyPostfix]
+ private static void OnStartup(GameDirector __instance)
+ {
+ if (RunManager.instance.levelCurrent != RunManager.instance.levelMainMenu &&
+ RunManager.instance.levelCurrent != RunManager.instance.levelLobbyMenu)
+ return;
+
+ new GameObject("VR Hotswapper").AddComponent();
+ }
+
+ ///
+ /// The default setup for every scene (including for non-vr players)
+ ///
+ private static void SetupDefaultSceneUniversal()
+ {
+ new GameObject("RepoXR Network System").AddComponent();
+
+ ShowVRFailedWarning();
+
+#if DEBUG
+ ShowEarlyAccessWarning();
+#endif
+ }
+
private static void ShowVRFailedWarning()
{
if (!Plugin.Flags.HasFlag(Flags.StartupFailed) ||
diff --git a/Source/Experiments.cs b/Source/Experiments.cs
index c230555..1018abb 100644
--- a/Source/Experiments.cs
+++ b/Source/Experiments.cs
@@ -1,50 +1,63 @@
-using HarmonyLib;
+// ReSharper disable UnusedVariable
+
+using System.Collections.Generic;
+using System.Reflection.Emit;
+using HarmonyLib;
+using UnityEngine;
namespace RepoXR;
#if DEBUG
internal static class Experiments
{
- [HarmonyPatch(typeof(EnemyDirector), nameof(EnemyDirector.Awake))]
+ [HarmonyPatch(typeof(PlayerController), nameof(PlayerController.FixedUpdate))]
[HarmonyPostfix]
- private static void FuckLolEnemy(EnemyDirector __instance)
+ private static void InfiniteSprintPatch(PlayerController __instance)
{
- // Only allow eyeyeyeyeyeye spawning
- var enemy = __instance.enemiesDifficulty1[0];
-
- // Only allow mouth spawning
- // var enemy = __instance.enemiesDifficulty1[4];
-
- // Only allow thin-man spawning
- // var enemy = __instance.enemiesDifficulty1[1];
-
- // Only allow upSCREAM! spawning
- // var enemy = __instance.enemiesDifficulty2[2];
+ __instance.EnergyCurrent = __instance.EnergyStart;
- // Only allow beamer spawning
- // var enemy = __instance.enemiesDifficulty3[4];
+ var script = PlayerController.instance?.playerAvatarScript;
+ if (script != null) script.upgradeTumbleClimb = 100;
+ }
- __instance.enemiesDifficulty1.Clear();
- __instance.enemiesDifficulty2.Clear();
- __instance.enemiesDifficulty3.Clear();
+ [HarmonyPatch(typeof(SpectateCamera), nameof(SpectateCamera.HeadEnergyLogic))]
+ [HarmonyTranspiler]
+ private static IEnumerable FastRechargeHead(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false, new CodeMatch(OpCodes.Ldc_R4, 100f))
+ .SetOperandAndAdvance(0.5f)
+ .InstructionEnumeration();
+ }
- __instance.enemiesDifficulty1.Add(enemy);
- __instance.enemiesDifficulty2.Add(enemy);
- __instance.enemiesDifficulty3.Add(enemy);
+ [HarmonyPatch(typeof(SemiFunc), nameof(SemiFunc.DebugTester))]
+ [HarmonyPostfix]
+ private static void IAmASurgeonIMeanTester(ref bool __result)
+ {
+ __result = true;
}
- [HarmonyPatch(typeof(PlayerController), nameof(PlayerController.FixedUpdate))]
+ [HarmonyPatch(typeof(SemiFunc), nameof(SemiFunc.DebugDev))]
[HarmonyPostfix]
- private static void InfiniteSprintPatch(PlayerController __instance)
+ private static void IAmASurgeonIMeanDeveloper(ref bool __result)
{
- __instance.EnergyCurrent = __instance.EnergyStart;
+ __result = true;
}
- [HarmonyPatch(typeof(PlayerHealth), nameof(PlayerHealth.Hurt))]
- [HarmonyPrefix]
- private static bool NoDamage()
+ [HarmonyPatch(typeof(DebugConsoleUI), nameof(DebugConsoleUI.Update))]
+ [HarmonyTranspiler]
+ private static IEnumerable KeepEnterKeyThing(IEnumerable instructions)
{
- return false;
+ return new CodeMatcher(instructions)
+ .MatchForward(false, new CodeMatch(OpCodes.Ldc_I4_S, (sbyte)9))
+ .SetOperandAndAdvance((sbyte)KeyCode.Return)
+ .SetOperandAndAdvance(AccessTools.Method(typeof(UnityEngine.Input), nameof(UnityEngine.Input.GetKeyDown),
+ [typeof(KeyCode)]))
+ .MatchForward(false, new CodeMatch(OpCodes.Ldc_I4_S, (sbyte)13))
+ .SetOperandAndAdvance((sbyte)KeyCode.Backspace)
+ .SetOperandAndAdvance(AccessTools.Method(typeof(UnityEngine.Input), nameof(UnityEngine.Input.GetKeyDown),
+ [typeof(KeyCode)]))
+ .InstructionEnumeration();
}
}
#endif
\ No newline at end of file
diff --git a/Source/Input/Actions.cs b/Source/Input/Actions.cs
index e7fe355..b8089c7 100644
--- a/Source/Input/Actions.cs
+++ b/Source/Input/Actions.cs
@@ -18,6 +18,9 @@ public class Actions
public InputAction RightHandPosition { get; private set; }
public InputAction RightHandRotation { get; private set; }
public InputAction RightHandTrackingState { get; private set; }
+
+ public InputAction EyeGazePosition { get; private set; }
+ public InputAction EyeGazeRotation { get; private set; }
private Actions()
{
@@ -32,9 +35,12 @@ private Actions()
RightHandPosition = AssetCollection.DefaultXRActions.FindAction("Right/Position");
RightHandRotation = AssetCollection.DefaultXRActions.FindAction("Right/Rotation");
RightHandTrackingState = AssetCollection.DefaultXRActions.FindAction("Right/Tracking State");
-
+
+ EyeGazePosition = AssetCollection.DefaultXRActions.FindAction("Eye Gaze/Position");
+ EyeGazeRotation = AssetCollection.DefaultXRActions.FindAction("Eye Gaze/Rotation");
+
AssetCollection.DefaultXRActions.Enable();
}
- public InputAction this[string name] => VRInputSystem.instance.Actions[name];
+ public InputAction this[string name] => VRInputSystem.Instance.Actions[name];
}
\ No newline at end of file
diff --git a/Source/Input/RebindManager.cs b/Source/Input/RebindManager.cs
index 41df47c..7cb3110 100644
--- a/Source/Input/RebindManager.cs
+++ b/Source/Input/RebindManager.cs
@@ -52,7 +52,7 @@ public class RebindManager : MonoBehaviour
private void Awake()
{
Instance = this;
- playerInput = VRInputSystem.instance.GetPlayerInput();
+ playerInput = VRInputSystem.Instance.GetPlayerInput();
DestroyOldUI();
CreateUI();
@@ -73,8 +73,7 @@ private void OnDestroy()
private void OnControlsChanged(PlayerInput input)
{
Logger.LogDebug($"New control scheme: {input.currentControlScheme}");
- // TODO: Change some text somewhere
-
+
ReloadBindings();
}
diff --git a/Source/Input/TrackingInput.cs b/Source/Input/TrackingInput.cs
index ad03013..61d8c39 100644
--- a/Source/Input/TrackingInput.cs
+++ b/Source/Input/TrackingInput.cs
@@ -9,7 +9,8 @@ namespace RepoXR.Input;
///
public class TrackingInput : MonoBehaviour
{
- public static TrackingInput instance;
+ public static TrackingInput Instance => _instance ?? InputManager.instance.gameObject.AddComponent();
+ private static TrackingInput? _instance;
public Transform HeadTransform { get; private set; }
public Transform LeftHandTransform { get; private set; }
@@ -17,13 +18,13 @@ public class TrackingInput : MonoBehaviour
private void Awake()
{
- if (instance != null)
+ if (_instance != null)
{
Destroy(gameObject);
return;
}
- instance = this;
+ _instance = this;
DontDestroyOnLoad(gameObject);
CreateTrackingOrigins();
diff --git a/Source/Input/VRInputSystem.cs b/Source/Input/VRInputSystem.cs
index 8d723bf..bbd12a6 100644
--- a/Source/Input/VRInputSystem.cs
+++ b/Source/Input/VRInputSystem.cs
@@ -10,7 +10,8 @@ namespace RepoXR.Input;
public class VRInputSystem : MonoBehaviour
{
- public static VRInputSystem instance;
+ public static VRInputSystem Instance => _instance ?? InputManager.instance.gameObject.AddComponent();
+ private static VRInputSystem? _instance;
private PlayerInput playerInput;
@@ -21,7 +22,7 @@ public class VRInputSystem : MonoBehaviour
private void Awake()
{
- instance = this;
+ _instance = this;
playerInput = gameObject.AddComponent();
playerInput.actions = AssetCollection.VRInputs;
diff --git a/Source/Managers/HapticManager.cs b/Source/Managers/HapticManager.cs
index 05e628a..ba1187a 100644
--- a/Source/Managers/HapticManager.cs
+++ b/Source/Managers/HapticManager.cs
@@ -33,6 +33,13 @@ private void Awake()
private void Update()
{
+ // Destroy object if we toggled out of VR
+ if (SemiFunc.FPSImpulse1() && !VRSession.InVR)
+ {
+ Destroy(gameObject);
+ return;
+ }
+
if (priorityTimer > 0)
{
priorityTimer -= Time.deltaTime;
diff --git a/Source/Managers/HotswapManager.cs b/Source/Managers/HotswapManager.cs
new file mode 100644
index 0000000..282b0d8
--- /dev/null
+++ b/Source/Managers/HotswapManager.cs
@@ -0,0 +1,68 @@
+using Photon.Pun;
+using UnityEngine;
+using UnityEngine.InputSystem;
+using UnityEngine.SceneManagement;
+
+namespace RepoXR.Managers;
+
+public class HotswapManager : MonoBehaviour
+{
+ private readonly InputAction swapAction = new(binding: "/F8");
+
+ private void Awake()
+ {
+ swapAction.performed += SwapActionPerformed;
+ swapAction.Enable();
+ }
+
+ private void OnDestroy()
+ {
+ swapAction.performed -= SwapActionPerformed;
+ }
+
+ private static void SwapActionPerformed(InputAction.CallbackContext context)
+ {
+ if (!context.performed)
+ return;
+
+ if (VRSession.InVR)
+ HotswapDisableVR();
+ else
+ HotswapEnableVR();
+ }
+
+ private static void HotswapDisableVR()
+ {
+ Plugin.ToggleVR();
+
+ RestartScene();
+ }
+
+ private static void HotswapEnableVR()
+ {
+ Plugin.ToggleVR();
+
+ if (VRSession.InVR)
+ RestartScene();
+ else
+ {
+ // Close existing popup if one is open
+ if (MenuPagePopUp.instance != null)
+ MenuPagePopUp.instance.ButtonEvent();
+
+ MenuManager.instance.PagePopUp("VR Startup Failed", Color.red,
+ "RepoXR tried to swap the game to VR, however an error occured during initialization.\n\nYou can update your settings and press F8 to try again.",
+ "Darn it",
+ true);
+ }
+ }
+
+ private static void RestartScene()
+ {
+ if (SemiFunc.IsMultiplayer() && !PhotonNetwork.IsMasterClient)
+ // RestartScene is not allowed when not the host, so we just re-join the lobby
+ SceneManager.LoadSceneAsync("LobbyJoin");
+ else
+ RunManager.instance.RestartScene();
+ }
+}
\ No newline at end of file
diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs
index ee0f7ae..da16a35 100644
--- a/Source/Managers/VRSession.cs
+++ b/Source/Managers/VRSession.cs
@@ -1,10 +1,8 @@
-using RepoXR.Input;
+using RepoXR.Assets;
using RepoXR.Player;
using RepoXR.UI;
using UnityEngine;
-using UnityEngine.InputSystem;
using UnityEngine.InputSystem.UI;
-using UnityEngine.InputSystem.XR;
namespace RepoXR.Managers;
@@ -25,6 +23,7 @@ public class VRSession : MonoBehaviour
public Camera MainCamera { get; private set; }
public VRPlayer Player { get; private set; }
public GameHud HUD { get; private set; }
+ public FocusSphere FocusSphere { get; private set; }
private void Awake()
{
@@ -49,12 +48,6 @@ private void InitializeVRSession()
MainCamera = CameraUtils.Instance.MainCamera;
MainCamera.targetTexture = null;
MainCamera.depth = 0;
-
- // Setup camera tracking
- var cameraPoseDriver = MainCamera.gameObject.AddComponent();
- cameraPoseDriver.positionAction = Actions.Instance.HeadPosition;
- cameraPoseDriver.rotationAction = Actions.Instance.HeadRotation;
- cameraPoseDriver.trackingStateInput = new InputActionProperty(Actions.Instance.HeadTrackingState);
// Setup "on top" camera
var topCamera = MainCamera.transform.Find("Camera Top").GetComponent();
@@ -69,5 +62,8 @@ private void InitializeVRSession()
// Initialize Handheld Map (if it wasn't created yet)
VRMapTool.Create();
+
+ // Initialize Focus Sphere
+ FocusSphere = Instantiate(AssetCollection.FocusSphere, MainCamera.transform.parent).GetComponent();
}
}
\ No newline at end of file
diff --git a/Source/Networking/Frames/EyeGaze.cs b/Source/Networking/Frames/EyeGaze.cs
new file mode 100644
index 0000000..cfa02da
--- /dev/null
+++ b/Source/Networking/Frames/EyeGaze.cs
@@ -0,0 +1,20 @@
+using Photon.Pun;
+using UnityEngine;
+
+namespace RepoXR.Networking.Frames;
+
+[Frame(FrameHelper.FrameEyeGaze)]
+public class EyeGaze : IFrame
+{
+ public Vector3 GazePoint;
+
+ public void Serialize(PhotonStream stream)
+ {
+ stream.SendNext(GazePoint);
+ }
+
+ public void Deserialize(PhotonStream stream)
+ {
+ GazePoint = (Vector3)stream.ReceiveNext();
+ }
+}
\ No newline at end of file
diff --git a/Source/Networking/Frames/Frame.cs b/Source/Networking/Frames/Frame.cs
index 3db28f0..f1f56b1 100644
--- a/Source/Networking/Frames/Frame.cs
+++ b/Source/Networking/Frames/Frame.cs
@@ -14,6 +14,7 @@ public static class FrameHelper
public const int FrameMaptool = 3;
public const int FrameHeadlamp = 4;
public const int FrameDominantHand = 5;
+ public const int FrameEyeGaze = 6;
private static Dictionary cachedTypes = [];
diff --git a/Source/Networking/NetworkPlayer.cs b/Source/Networking/NetworkPlayer.cs
index eb9d8ea..7e3331c 100644
--- a/Source/Networking/NetworkPlayer.cs
+++ b/Source/Networking/NetworkPlayer.cs
@@ -37,6 +37,10 @@ public class NetworkPlayer : MonoBehaviour
private bool isLeftHanded;
private bool isMapLeftHanded;
private bool isHeadlampEnabled;
+
+ // Eye tracking
+ public bool EyeTracking { get; private set; }
+ public Vector3 EyeGazePoint { get; private set; }
private void Start()
{
@@ -206,4 +210,17 @@ public void UpdateDominantHand(bool leftHanded)
playerRightArm.physGrabBeam.PhysGrabPointOrigin.SetParent(isLeftHanded ? leftHandAnchor : rightHandAnchor);
playerRightArm.physGrabBeam.PhysGrabPointOrigin.localPosition = Vector3.zero;
}
+
+ public void UpdateEyeTracking(Vector3 gazePoint)
+ {
+ // (0, -1000, 0) is sent whenever eye tracking is disabled (or stopped working) during a session
+ if (gazePoint == Vector3.down * 1000)
+ {
+ EyeTracking = false;
+ return;
+ }
+
+ EyeTracking = true;
+ EyeGazePoint = gazePoint;
+ }
}
\ No newline at end of file
diff --git a/Source/Networking/NetworkSystem.cs b/Source/Networking/NetworkSystem.cs
index 096e1c2..7056f68 100644
--- a/Source/Networking/NetworkSystem.cs
+++ b/Source/Networking/NetworkSystem.cs
@@ -92,6 +92,19 @@ public void UpdateDominantHand(bool leftHanded)
});
}
+ public void UpdateEyeTracking(Vector3 gazePoint)
+ {
+ EnqueueFrame(new EyeGaze
+ {
+ GazePoint = gazePoint
+ });
+ }
+
+ public void DisableEyeTracking()
+ {
+ UpdateEyeTracking(Vector3.down * 1000);
+ }
+
///
/// Enqueues a frame to be sent next serialization sequence. This function contains an optimization that removes
/// duplicate frames to reduce network usage, which reduces server costs.
@@ -170,6 +183,17 @@ private void HandleFrame(PlayerAvatar player, IFrame frame)
break;
}
+
+ case FrameHelper.FrameEyeGaze:
+ {
+ var eyeGazeFrame = (EyeGaze)frame;
+ if (!networkPlayers.TryGetValue(player.photonView.controllerActorNr, out var networkPlayer))
+ return;
+
+ networkPlayer.UpdateEyeTracking(eyeGazeFrame.GazePoint);
+
+ break;
+ }
}
}
catch (Exception ex)
@@ -198,6 +222,10 @@ internal void OnPlayerLeave(int actorNumber)
internal void WriteAdditionalData(PhotonStream stream)
{
+ // Just don't send anything if we have nothing to say
+ if (scheduledFrames.Count == 0)
+ return;
+
stream.SendNext(REPOXR_MAGIC);
stream.SendNext(PROTOCOL_VERSION);
@@ -244,23 +272,23 @@ internal void ReadAdditionalData(PlayerAvatar playerAvatar, PhotonStream stream)
[RepoXRPatch(RepoXRPatchTarget.Universal)]
internal static class NetworkingPatches
{
- // The reason that this code is injected on PhysGrabber is that it's the last observed component on the
- // player avatar controller's PhotonView, meaning no additional data is available on the PhotonStream.
- // If we started injecting data too early, it would cause vanilla clients to no longer be able
- // to understand our photon data, and that breaks multiplayer.
+ // The reason that this code is injected on PlayerLocalCamera is that it's still enabled
+ // even after the player dies. Previous versions of this code would make the NetworkSystem
+ // stop functioning after the player died, which was fine before, but now the game
+ // has features that VR needs some special interactions with even after death
///
/// Inject additional code when serializing/deserializing a network component
///
- [HarmonyPatch(typeof(PhysGrabber), nameof(PhysGrabber.OnPhotonSerializeView))]
+ [HarmonyPatch(typeof(PlayerLocalCamera), nameof(PlayerLocalCamera.OnPhotonSerializeView))]
[HarmonyPostfix]
- private static void OnAfterSerializeView(PhysGrabber __instance, PhotonStream stream)
+ private static void OnAfterSerializeView(PlayerLocalCamera __instance, PhotonStream stream)
{
if (stream.IsWriting)
NetworkSystem.instance.WriteAdditionalData(stream);
else
NetworkSystem.instance.ReadAdditionalData(
- __instance.playerAvatar ?? __instance.GetComponent(), stream);
+ __instance.transform.parent.GetComponentInChildren(true), stream);
}
[HarmonyPatch(typeof(NetworkManager), nameof(NetworkManager.OnPlayerLeftRoom))]
diff --git a/Source/OpenXR.cs b/Source/OpenXR.cs
index 9acc39d..5c909a4 100644
--- a/Source/OpenXR.cs
+++ b/Source/OpenXR.cs
@@ -8,12 +8,12 @@
using BepInEx.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using RepoXR.Assets;
using Steamworks;
using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.Management;
using UnityEngine.XR.OpenXR;
-using UnityEngine.XR.OpenXR.Features.Interactions;
namespace RepoXR;
@@ -371,6 +371,12 @@ public static bool InitializeXR()
return false;
}
+ public static void DeinitializeXR()
+ {
+ xrManagerSettings?.DeinitializeLoader();
+ xrGeneralSettings?.StopXRSDK();
+ }
+
private static bool InitializeXR(Runtime? runtime)
{
if (xrManagerSettings == null || xrGeneralSettings == null || xrLoader == null)
@@ -421,36 +427,7 @@ private static void InitializeScripts()
OpenXRSettings.Instance.renderMode = OpenXRSettings.RenderMode.MultiPass;
OpenXRSettings.Instance.depthSubmissionMode = OpenXRSettings.DepthSubmissionMode.None;
-
- if (OpenXRSettings.Instance.features.Length != 0)
- return;
-
- var valveIndex = ScriptableObject.CreateInstance();
- var hpReverb = ScriptableObject.CreateInstance();
- var htcVive = ScriptableObject.CreateInstance();
- var mmController = ScriptableObject.CreateInstance();
- var khrSimple = ScriptableObject.CreateInstance();
- var metaQuestTouch = ScriptableObject.CreateInstance();
- var oculusTouch = ScriptableObject.CreateInstance();
-
- valveIndex.enabled = true;
- hpReverb.enabled = true;
- htcVive.enabled = true;
- mmController.enabled = true;
- khrSimple.enabled = true;
- metaQuestTouch.enabled = true;
- oculusTouch.enabled = true;
-
- OpenXRSettings.Instance.features =
- [
- valveIndex,
- hpReverb,
- htcVive,
- mmController,
- khrSimple,
- metaQuestTouch,
- oculusTouch
- ];
+ OpenXRSettings.Instance.features = AssetCollection.OpenXRFeatures.Features.ToArray();
}
}
}
\ No newline at end of file
diff --git a/Source/Patches/CameraPatches.cs b/Source/Patches/CameraPatches.cs
index 53ede4f..53af970 100644
--- a/Source/Patches/CameraPatches.cs
+++ b/Source/Patches/CameraPatches.cs
@@ -1,4 +1,6 @@
using HarmonyLib;
+using Photon.Pun;
+using RepoXR.Managers;
using UnityEngine;
namespace RepoXR.Patches;
@@ -13,6 +15,10 @@ internal static class CameraPatches
[HarmonyPrefix]
private static void DisableTargetTextureOverride(Camera __instance, ref RenderTexture? value)
{
+ // We make an exception for our manually rendered custom camera
+ if (__instance.name.StartsWith("Custom Camera"))
+ return;
+
value = null;
}
@@ -76,4 +82,42 @@ private static bool OnScreenVR(Vector3 position, float padWidth, float padHeight
return screenPoint.x > -padWidth && screenPoint.x < 1 + padWidth &&
screenPoint.y > -padHeight && screenPoint.y < 1 + padHeight;
}
+
+ ///
+ /// Assign the transform the same values as our VR camera
+ ///
+ [HarmonyPatch(typeof(PlayerLocalCamera), nameof(PlayerLocalCamera.Update))]
+ [HarmonyPostfix]
+ private static void AlignWithVRCameraPatch(PlayerLocalCamera __instance)
+ {
+ if (SemiFunc.IsMultiplayer() && !__instance.photonView.IsMine)
+ return;
+
+ if (VRSession.Instance is not { } session)
+ return;
+
+ __instance.transform.position = session.MainCamera.transform.position;
+ __instance.transform.rotation = session.MainCamera.transform.rotation;
+ }
+
+ ///
+ /// Make sure to synchronize the VR camera transforms instead of only the aim transforms
+ ///
+ [HarmonyPatch(typeof(PlayerLocalCamera), nameof(PlayerLocalCamera.OnPhotonSerializeView))]
+ [HarmonyPrefix]
+ private static bool CameraSerializeVRParams(PlayerLocalCamera __instance, PhotonStream stream,
+ PhotonMessageInfo info)
+ {
+ if (!SemiFunc.MasterAndOwnerOnlyRPC(info, __instance.photonView))
+ return false;
+
+ if (!stream.IsWriting)
+ return true;
+
+ stream.SendNext(__instance.transform.position);
+ stream.SendNext(__instance.transform.rotation);
+ stream.SendNext(__instance.teleported);
+
+ return false;
+ }
}
\ No newline at end of file
diff --git a/Source/Patches/Enemy/EnemyCeilingEyePatches.cs b/Source/Patches/Enemy/EnemyCeilingEyePatches.cs
index ada6d28..c3e118c 100644
--- a/Source/Patches/Enemy/EnemyCeilingEyePatches.cs
+++ b/Source/Patches/Enemy/EnemyCeilingEyePatches.cs
@@ -5,6 +5,7 @@
using RepoXR.Assets;
using RepoXR.Managers;
using RepoXR.Player.Camera;
+using RepoXR.UI;
using static HarmonyLib.AccessTools;
namespace RepoXR.Patches.Enemy;
@@ -32,29 +33,27 @@ private static IEnumerable SetCameraSoftRotationPatch(IEnumerab
.InsertAndAdvance(new CodeInstruction(OpCodes.Callvirt,
PropertyGetter(typeof(ConfigEntry), nameof(ConfigEntry.Value))))
.SetOperandAndAdvance(Method(typeof(VRCameraAim), nameof(VRCameraAim.SetAimTargetSoft)))
- .InstructionEnumeration();
- }
+ // Set focus sphere target to look at the ceiling eye
+ .Insert(
+ // VRSession.Instance.FocusSphere
+ new CodeInstruction(OpCodes.Call, PropertyGetter(typeof(VRSession), nameof(VRSession.Instance))),
+ new CodeInstruction(OpCodes.Callvirt, PropertyGetter(typeof(VRSession), nameof(VRSession.FocusSphere))),
- ///
- /// Replace with
- ///
- [HarmonyPatch(typeof(EnemyCeilingEye), nameof(EnemyCeilingEye.UpdateStateRPC))]
- [HarmonyTranspiler]
- private static IEnumerable SetCameraRotationPatch(IEnumerable instructions)
- {
- return new CodeMatcher(instructions)
- .MatchForward(false,
- new CodeMatch(OpCodes.Callvirt, Method(typeof(CameraAim), nameof(CameraAim.AimTargetSet))))
- .Advance(-10)
- .SetOperandAndAdvance(Field(typeof(VRCameraAim), nameof(VRCameraAim.instance)))
- .Advance(9)
- // Make the rotation less severe if reduced aim impact is enabled
- .InsertAndAdvance(new CodeInstruction(OpCodes.Call, Plugin.GetConfigGetter()))
- .InsertAndAdvance(new CodeInstruction(OpCodes.Callvirt,
- PropertyGetter(typeof(Config), nameof(Config.ReducedAimImpact))))
- .InsertAndAdvance(new CodeInstruction(OpCodes.Callvirt,
- PropertyGetter(typeof(ConfigEntry), nameof(ConfigEntry.Value))))
- .SetOperandAndAdvance(Method(typeof(VRCameraAim), nameof(VRCameraAim.SetAimTarget)))
+ // target: this.enemy.CenterTransform
+ new CodeInstruction(OpCodes.Ldarg_0),
+ new CodeInstruction(OpCodes.Ldfld, Field(typeof(EnemyCeilingEye), nameof(EnemyCeilingEye.enemy))),
+ new CodeInstruction(OpCodes.Ldfld, Field(typeof(global::Enemy), nameof(global::Enemy.CenterTransform))),
+
+ // time: 1f
+ new CodeInstruction(OpCodes.Ldc_R4, 0.5f),
+
+ // speed: 2f
+ new CodeInstruction(OpCodes.Ldc_R4, 2f),
+
+ // strength: 1f (100%)
+ new CodeInstruction(OpCodes.Ldc_R4, 1f),
+ new CodeInstruction(OpCodes.Callvirt, Method(typeof(FocusSphere), nameof(FocusSphere.SetLookAtTarget)))
+ )
.InstructionEnumeration();
}
@@ -67,7 +66,7 @@ private static void EyeAttachHapticFeedback(EnemyCeilingEye __instance)
{
if (__instance.currentState != EnemyCeilingEye.State.HasTarget)
return;
-
+
if (!__instance.targetPlayer || !__instance.targetPlayer.isLocal)
return;
diff --git a/Source/Patches/Enemy/EnemyHeartHuggerPatches.cs b/Source/Patches/Enemy/EnemyHeartHuggerPatches.cs
new file mode 100644
index 0000000..44d391b
--- /dev/null
+++ b/Source/Patches/Enemy/EnemyHeartHuggerPatches.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Reflection.Emit;
+using BepInEx.Configuration;
+using HarmonyLib;
+using RepoXR.Player.Camera;
+
+using static HarmonyLib.AccessTools;
+
+namespace RepoXR.Patches.Enemy;
+
+[RepoXRPatch]
+internal static class EnemyHeartHuggerPatches
+{
+ ///
+ /// Force the VR camera to look at the heart hugger
+ ///
+ [HarmonyPatch(typeof(EnemyHeartHugger), nameof(EnemyHeartHugger.JumpScareAtChompStartForceLookAtHead))]
+ [HarmonyTranspiler]
+ private static IEnumerable LookAtPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false, new CodeMatch(OpCodes.Ldsfld, Field(typeof(CameraAim), nameof(CameraAim.Instance))))
+ .SetOperandAndAdvance(Field(typeof(VRCameraAim), nameof(VRCameraAim.instance)))
+ .MatchForward(false,
+ new CodeMatch(OpCodes.Callvirt, Method(typeof(CameraAim), nameof(CameraAim.AimTargetSoftSet))))
+ .SetAndAdvance(OpCodes.Call, Plugin.GetConfigGetter())
+ .InsertAndAdvance(
+ new CodeInstruction(OpCodes.Callvirt, PropertyGetter(typeof(Config), nameof(Config.ReducedAimImpact))),
+ new CodeInstruction(OpCodes.Callvirt,
+ PropertyGetter(typeof(ConfigEntry), nameof(ConfigEntry.Value))),
+ new CodeInstruction(OpCodes.Callvirt, Method(typeof(VRCameraAim), nameof(VRCameraAim.SetAimTargetSoft)))
+ )
+ .InstructionEnumeration();
+ }
+}
\ No newline at end of file
diff --git a/Source/Patches/Enemy/EnemyOnScreenPatches.cs b/Source/Patches/Enemy/EnemyOnScreenPatches.cs
new file mode 100644
index 0000000..7d7bbf5
--- /dev/null
+++ b/Source/Patches/Enemy/EnemyOnScreenPatches.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Reflection.Emit;
+using HarmonyLib;
+using RepoXR.Player;
+
+using static HarmonyLib.AccessTools;
+
+namespace RepoXR.Patches.Enemy;
+
+[RepoXRPatch]
+internal static class EnemyOnScreenPatches
+{
+ ///
+ /// Replace the "on screen" detection in enemies with custom detection that is better suited for VR and supports
+ /// eye tracking
+ ///
+ [HarmonyPatch(typeof(EnemyOnScreen), nameof(EnemyOnScreen.Logic), MethodType.Enumerator)]
+ [HarmonyTranspiler]
+ private static IEnumerable LogicPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false, new CodeMatch(OpCodes.Call, Method(typeof(SemiFunc), nameof(SemiFunc.OnScreen))))
+ .SetOperandAndAdvance(Method(typeof(VREyeTracking), nameof(VREyeTracking.LookingAt)))
+ .InstructionEnumeration();
+ }
+}
\ No newline at end of file
diff --git a/Source/Patches/Enemy/EnemyOoglyPatches.cs b/Source/Patches/Enemy/EnemyOoglyPatches.cs
new file mode 100644
index 0000000..e1758b9
--- /dev/null
+++ b/Source/Patches/Enemy/EnemyOoglyPatches.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Reflection.Emit;
+using BepInEx.Configuration;
+using HarmonyLib;
+using RepoXR.Player.Camera;
+
+using static HarmonyLib.AccessTools;
+
+namespace RepoXR.Patches.Enemy;
+
+[RepoXRPatch]
+internal static class EnemyOoglyPatches
+{
+ ///
+ /// Oh you don't wanna know... oh the horror (look at oogly while being attached)
+ ///
+ [HarmonyPatch(typeof(EnemyOogly), nameof(EnemyOogly.UpdateEvilEyesTimer))]
+ [HarmonyTranspiler]
+ private static IEnumerable LookAtPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false, new CodeMatch(OpCodes.Ldsfld, Field(typeof(CameraAim), nameof(CameraAim.Instance))))
+ .SetOperandAndAdvance(Field(typeof(VRCameraAim), nameof(VRCameraAim.instance)))
+ .MatchForward(false,
+ new CodeMatch(OpCodes.Callvirt, Method(typeof(CameraAim), nameof(CameraAim.AimTargetSet))))
+ .SetAndAdvance(OpCodes.Call, Plugin.GetConfigGetter())
+ .InsertAndAdvance(
+ new CodeInstruction(OpCodes.Callvirt, PropertyGetter(typeof(Config), nameof(Config.ReducedAimImpact))),
+ new CodeInstruction(OpCodes.Callvirt,
+ PropertyGetter(typeof(ConfigEntry), nameof(ConfigEntry.Value))),
+ new CodeInstruction(OpCodes.Callvirt, Method(typeof(VRCameraAim), nameof(VRCameraAim.SetAimTarget)))
+ )
+ .InstructionEnumeration();
+ }
+}
\ No newline at end of file
diff --git a/Source/Patches/Enemy/EnemySpinnyPatches.cs b/Source/Patches/Enemy/EnemySpinnyPatches.cs
new file mode 100644
index 0000000..0b7dc03
--- /dev/null
+++ b/Source/Patches/Enemy/EnemySpinnyPatches.cs
@@ -0,0 +1,22 @@
+using HarmonyLib;
+using RepoXR.Player.Camera;
+
+namespace RepoXR.Patches.Enemy;
+
+[RepoXRPatch]
+internal static class EnemySpinnyPatches
+{
+ ///
+ /// Make sure to always look at the little gambling machine
+ ///
+ [HarmonyPatch(typeof(EnemySpinny), nameof(EnemySpinny.OverrideTargetPlayerCameraAim))]
+ [HarmonyPrefix]
+ private static bool OverrideVRCameraAim(EnemySpinny __instance, float _strenght, float _strenghtNoAim)
+ {
+ // Always low impact, there's not really a need to force up-down look here
+ VRCameraAim.instance.SetAimTargetSoft(__instance.spinnyWheel.position, 0.1f, _strenght, _strenghtNoAim,
+ __instance.gameObject, 100, true);
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Source/Patches/HarmonyPatcher.cs b/Source/Patches/HarmonyPatcher.cs
index 8b4d948..51005c0 100644
--- a/Source/Patches/HarmonyPatcher.cs
+++ b/Source/Patches/HarmonyPatcher.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Reflection;
using HarmonyLib;
using JetBrains.Annotations;
@@ -27,6 +28,11 @@ public static void PatchVR()
Patch(VRPatcher, RepoXRPatchTarget.VROnly);
}
+ public static void UnpatchVR()
+ {
+ VRPatcher.UnpatchSelf();
+ }
+
public static void PatchClass(Type type)
{
UniversalPatcher.CreateClassProcessor(type, true).Patch();
@@ -76,7 +82,9 @@ internal enum RepoXRPatchTarget
}
///
-/// Fixes a bug in older BepInEx versions (shame on you TS for using a 2-year-old BepInEx)
+/// To keep RepoXR compatible with older BepInEx versions, this patch will not be removed
+///
+/// Fixes a bug in older BepInEx versions
///
/// https://github.com/BepInEx/HarmonyX/blob/master/Harmony/Internal/Patching/ILManipulator.cs#L322
/// Licensed under MIT: https://github.com/BepInEx/HarmonyX/blob/master/LICENSE
@@ -198,4 +206,19 @@ private static void Emit(this CecilILGenerator il, SRE.OpCode opcode, object ope
method.Invoke(null, [il, opcode, operand]);
}
+}
+
+[RepoXRPatch(RepoXRPatchTarget.Universal)]
+internal static class HarmonyLibPatches
+{
+ ///
+ /// Ironically, patching Harmony like this fixes some issues with *un*patching
+ ///
+ [HarmonyPatch(typeof(MethodBaseExtensions), nameof(MethodBaseExtensions.HasMethodBody))]
+ [HarmonyPostfix]
+ private static void OnUnpatch(MethodBase member, ref bool __result)
+ {
+ if (new StackTrace().GetFrame(2)?.GetMethod().Name == "UnpatchConditional")
+ __result = true;
+ }
}
\ No newline at end of file
diff --git a/Source/Patches/InputPatches.cs b/Source/Patches/InputPatches.cs
index 811bf26..64f0e01 100644
--- a/Source/Patches/InputPatches.cs
+++ b/Source/Patches/InputPatches.cs
@@ -23,20 +23,23 @@ private static void AllowBackgroundTracking(ref InputSettings.BackgroundBehavior
value = InputSettings.BackgroundBehavior.IgnoreFocus;
}
+ ///
+ /// Add additional mapping tags during startup
+ ///
[HarmonyPatch(typeof(InputManager), nameof(InputManager.Start))]
[HarmonyPostfix]
private static void OnInputManagerStart(InputManager __instance)
{
var offset = Enum.GetNames(typeof(InputKey)).Length;
-
+
for (var i = 0; i < AssetCollection.RemappableControls.additionalBindings.Length; i++)
{
var binding = AssetCollection.RemappableControls.additionalBindings[i];
-
+
__instance.tagDictionary.Add($"[{binding.action.name}]", (InputKey)(i + offset));
}
}
-
+
///
/// Create a custom component on the , allowing the use of s
///
@@ -44,8 +47,6 @@ private static void OnInputManagerStart(InputManager __instance)
[HarmonyPostfix]
private static void OnInitializeInputManager(InputManager __instance)
{
- __instance.gameObject.AddComponent();
-
new GameObject("VR Tracking Input").AddComponent();
}
@@ -54,12 +55,20 @@ private static void OnInitializeInputManager(InputManager __instance)
private static bool GetAction(ref InputKey key, ref InputAction __result)
{
var bindings = Enum.GetNames(typeof(InputKey)).Length;
-
- __result = (int)key >= bindings
- ? AssetCollection.RemappableControls.additionalBindings[(int)key - bindings]
- : Actions.Instance[key.ToString()];
- return false;
+ try
+ {
+ __result = (int)key >= bindings
+ ? AssetCollection.RemappableControls.additionalBindings[(int)key - bindings]
+ : Actions.Instance[key.ToString()];
+
+ return false;
+ }
+ catch
+ {
+ // If no key was found, fall back to vanilla keybind (likely won't work with VR controllers though)
+ return true;
+ }
}
[HarmonyPatch(typeof(InputManager), nameof(InputManager.GetMovement))]
@@ -70,7 +79,7 @@ private static bool GetMovement(InputManager __instance, ref Vector2 __result)
return true;
__result = Actions.Instance["Movement"].ReadValue();
-
+
return false;
}
@@ -152,7 +161,7 @@ private static bool KeyUp(InputManager __instance, ref InputKey key, ref bool __
return true;
__result = __instance.GetAction(key).WasReleasedThisFrame();
-
+
return false;
}
@@ -181,11 +190,8 @@ private static bool KeyPullAndPush(ref float __result)
var pull = Actions.Instance["Pull"].ReadValue();
if (pull > 0)
- {
__result = -pull;
- return false;
- }
-
+
return false;
}
@@ -200,14 +206,14 @@ private static bool InputDisplayGet(InputManager __instance, InputKey _inputKey,
if (action == null)
{
__result = "Unassigned";
-
+
return false;
}
- var index = action.GetBindingIndex(VRInputSystem.instance.CurrentControlScheme);
+ var index = action.GetBindingIndex(VRInputSystem.Instance.CurrentControlScheme);
__result = __instance.InputDisplayGetString(action, index);
-
+
return false;
}
@@ -220,7 +226,7 @@ private static bool InputDisplayGetString(InputAction action, int bindingIndex,
{
var binding = action.bindings[bindingIndex].effectivePath;
__result = Utils.GetControlSpriteString(binding);
-
+
return false;
}
@@ -231,7 +237,7 @@ private static bool InputDisplayGetString(InputAction action, int bindingIndex,
[HarmonyPrefix]
private static bool InputToggleGet(ref InputKey key, ref bool __result)
{
- __result = VRInputSystem.instance.InputToggleGet(key.ToString());
+ __result = VRInputSystem.Instance.InputToggleGet(key.ToString());
return false;
}
@@ -260,7 +266,7 @@ private static bool NoUnderlinePatch(InputManager __instance, ref string __resul
private static bool ResetVRControls()
{
RebindManager.Instance.ResetControls();
-
+
return false;
}
diff --git a/Source/Patches/Item/ItemBoomboxPatches.cs b/Source/Patches/Item/ItemBoomboxPatches.cs
index 2d5dab3..4c7da8b 100644
--- a/Source/Patches/Item/ItemBoomboxPatches.cs
+++ b/Source/Patches/Item/ItemBoomboxPatches.cs
@@ -21,8 +21,8 @@ private static void BoomboxAimPatch(ValuableBoombox __instance)
var bopSpeed = Plugin.Config.ReducedAimImpact.Value ? 5 : 15;
var bopMultiplier = Plugin.Config.ReducedAimImpact.Value ? 0.15f : 0.5f;
- var cameraPosition = PhysGrabber.instance.playerAvatar.localCameraPosition;
- var cameraForward = PhysGrabber.instance.playerAvatar.localCameraTransform.forward * 2;
+ var cameraPosition = PhysGrabber.instance.playerAvatar.localCamera.transform.position;
+ var cameraForward = PhysGrabber.instance.playerAvatar.localCamera.transform.forward * 2;
var upOffset = Vector3.up * Mathf.Sin(Time.time * bopSpeed) * bopMultiplier;
var lookAtPosition = cameraPosition + cameraForward + upOffset;
diff --git a/Source/Patches/PhysGrabObjectPatches.cs b/Source/Patches/PhysGrabObjectPatches.cs
index c21d6e5..ec11dee 100644
--- a/Source/Patches/PhysGrabObjectPatches.cs
+++ b/Source/Patches/PhysGrabObjectPatches.cs
@@ -16,31 +16,35 @@ internal static class PhysGrabObjectPatches
private static Transform GetTargetTransform(PlayerAvatar player)
{
if (player.isLocal)
- return VRSession.Instance is { } session ? session.Player.MainHand : player.localCameraTransform;
+ return VRSession.Instance is { } session ? session.Player.MainHand : player.localCamera.transform;
return NetworkSystem.instance.GetNetworkPlayer(player, out var networkPlayer)
? networkPlayer.PrimaryHand
- : player.localCameraTransform;
+ : player.localCamera.transform;
}
private static Quaternion GetTargetRotation(PlayerAvatar player)
{
if (player.isLocal)
- return VRSession.Instance is { } session ? session.Player.MainHand.rotation : player.localCameraRotation;
+ return VRSession.Instance is { } session
+ ? session.Player.MainHand.rotation
+ : player.localCamera.transform.rotation;
return NetworkSystem.instance.GetNetworkPlayer(player, out var networkPlayer)
? networkPlayer.PrimaryHand.rotation
- : player.localCameraRotation;
+ : player.localCamera.transform.rotation;
}
private static Vector3 GetTargetPosition(PlayerAvatar player)
{
if (player.isLocal)
- return VRSession.Instance is { } session ? session.Player.MainHand.position : player.localCameraPosition;
+ return VRSession.Instance is { } session
+ ? session.Player.MainHand.position
+ : player.localCamera.transform.position;
return NetworkSystem.instance.GetNetworkPlayer(player, out var networkPlayer)
? networkPlayer.PrimaryHand.position
- : player.localCameraPosition;
+ : player.localCamera.transform.position;
}
private static Transform GetCartSteerTransform(PhysGrabber grabber)
@@ -62,7 +66,7 @@ private static IEnumerable HandRelativeMovementPatch(IEnumerabl
{
return new CodeMatcher(instructions)
.MatchForward(false,
- new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraTransform))))
+ new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera))))
.Repeat(matcher =>
matcher.SetInstruction(new CodeInstruction(OpCodes.Call,
((Func)GetTargetTransform).Method)))
@@ -106,14 +110,17 @@ private static IEnumerable HandRelativeCartCannonPatch(IEnumera
{
return new CodeMatcher(instructions)
.MatchForward(false,
- new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraRotation))))
- .Set(OpCodes.Call, ((Func)GetTargetRotation).Method)
+ new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera))))
+ .SetAndAdvance(OpCodes.Call, ((Func)GetTargetRotation).Method)
+ .RemoveInstructions(2)
.MatchForward(false,
- new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraRotation))))
- .Set(OpCodes.Call, ((Func)GetTargetRotation).Method)
+ new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera))))
+ .SetAndAdvance(OpCodes.Call, ((Func)GetTargetRotation).Method)
+ .RemoveInstructions(2)
.MatchForward(false,
- new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraPosition))))
- .Set(OpCodes.Call, ((Func)GetTargetPosition).Method)
+ new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera))))
+ .SetAndAdvance(OpCodes.Call, ((Func)GetTargetPosition).Method)
+ .RemoveInstructions(2)
.InstructionEnumeration();
}
@@ -126,8 +133,9 @@ private static IEnumerable RotationTargetHandRelative(IEnumerab
{
return new CodeMatcher(instructions)
.MatchForward(false,
- new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraRotation))))
- .Set(OpCodes.Call, ((Func)GetTargetRotation).Method)
+ new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera))))
+ .SetAndAdvance(OpCodes.Call, ((Func)GetTargetRotation).Method)
+ .RemoveInstructions(2)
.InstructionEnumeration();
}
}
\ No newline at end of file
diff --git a/Source/Patches/PhysGrabberPatches.cs b/Source/Patches/PhysGrabberPatches.cs
index 12a19ba..a1372b9 100644
--- a/Source/Patches/PhysGrabberPatches.cs
+++ b/Source/Patches/PhysGrabberPatches.cs
@@ -149,7 +149,7 @@ private static IEnumerable RayCheckPatches(IEnumerable)CalculateNewForward).Method),
new CodeInstruction(OpCodes.Stloc_1),
@@ -158,14 +158,11 @@ private static IEnumerable RayCheckPatches(IEnumerable matcher.Advance(-1).ReplaceCameraWithHand())
- .Start()
- .MatchForward(false, new CodeMatch(OpCodes.Call, PropertyGetter(typeof(Camera), nameof(Camera.main))))
- .Repeat(matcher => matcher.ReplaceCameraWithHand())
.InstructionEnumeration();
static Vector3 CalculateNewForward(PhysGrabber grabber)
{
- if (grabber.overrideGrab && grabber.overrideGrabTarget)
+ if (grabber.overrideGrabTarget)
return (grabber.overrideGrabTarget.transform.position - VRSession.Instance.Player.MainHand.position)
.normalized;
@@ -272,38 +269,27 @@ private static void OnReleaseObject(PhysGrabber __instance)
session.Player.Rig.inventoryController.TryEquipItem(item);
}
- private static float forceGrabTimer;
-
///
- /// Every time a grab override is triggered, reset the timer
- ///
- [HarmonyPatch(typeof(PhysGrabber), nameof(PhysGrabber.OverrideGrab))]
- [HarmonyPostfix]
- private static void OnOverrideGrab(PhysGrabber __instance)
- {
- forceGrabTimer = 0.1f;
- }
-
- ///
- /// If the is above zero, do not allow the grabber to let go
+ /// Make sure the force grab timer also works in VR, where the grab key is the same as the "take from inventory" key
///
[HarmonyPatch(typeof(PhysGrabber), nameof(PhysGrabber.Update))]
[HarmonyTranspiler]
- private static IEnumerable ForceOverrideGrabPatch(IEnumerable instructions)
+ private static IEnumerable ForceHoldPatch(IEnumerable instructions)
{
return new CodeMatcher(instructions)
.MatchForward(false,
- new CodeMatch(OpCodes.Stfld, Field(typeof(PhysGrabber), nameof(PhysGrabber.overrideGrabTarget))))
- .Advance(-13)
- .SetInstruction(new CodeInstruction(OpCodes.Call, ((Func)CheckAndUpdate).Method))
+ new CodeMatch(OpCodes.Ldfld,
+ Field(typeof(GameplayManager), nameof(GameplayManager.itemUnequipAutoHold))))
+ .Advance(-2)
+ .InsertAndAdvance(
+ new CodeInstruction(OpCodes.Ldarg_0),
+ new CodeInstruction(OpCodes.Ldfld, Field(typeof(PhysGrabber), nameof(PhysGrabber.overrideGrabTimer))),
+ new CodeInstruction(OpCodes.Call, ((Func)ShouldToggleGrabOff).Method)
+ )
.InstructionEnumeration();
- static bool CheckAndUpdate(PhysGrabber grabber)
- {
- forceGrabTimer -= Time.deltaTime;
-
- return grabber.overrideGrab && forceGrabTimer <= 0;
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static bool ShouldToggleGrabOff(bool grabHeld, float grabTimer) => grabHeld && grabTimer <= 0;
}
}
@@ -313,32 +299,14 @@ internal static class PhysGrabberUniversalPatches
private static Transform GetHandTransform(PhysGrabber grabber)
{
if (grabber.playerAvatar.isLocal)
- return VRSession.InVR ? VRSession.Instance.Player.MainHand : grabber.playerAvatar.localCameraTransform;
+ return VRSession.InVR ? VRSession.Instance.Player.MainHand : grabber.playerAvatar.localCamera.transform;
if (!NetworkSystem.instance)
- {
- Logger.LogError("NetworkSystem is null?");
- return grabber.playerAvatar.localCameraTransform;
- }
-
- if (NetworkSystem.instance.GetNetworkPlayer(grabber.playerAvatar, out var networkPlayer))
- {
- if (!networkPlayer)
- {
- Logger.LogError("NetworkPlayer is null?");
- return grabber.playerAvatar.localCameraTransform;
- }
-
- if (!networkPlayer.PrimaryHand)
- {
- Logger.LogError("GrabberHand is null?");
- return grabber.playerAvatar.localCameraTransform;
- }
-
- return networkPlayer.PrimaryHand;
- }
+ return grabber.playerAvatar.localCamera.transform;
- return grabber.playerAvatar.localCameraTransform;
+ return NetworkSystem.instance.GetNetworkPlayer(grabber.playerAvatar, out var networkPlayer)
+ ? networkPlayer.PrimaryHand
+ : grabber.playerAvatar.localCamera.transform;
}
///
@@ -363,17 +331,48 @@ private static IEnumerable ObjectTurningPatches(IEnumerable)GetHandTransform).Method))
// Replace camera transform with hand transform (remote player)
.MatchForward(false,
- new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCameraTransform))))
+ new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerAvatar), nameof(PlayerAvatar.localCamera))))
.Advance(-1)
- .RemoveInstructions(2)
+ .RemoveInstructions(3)
.Insert(new CodeInstruction(OpCodes.Call,
((Func)GetHandTransform).Method))
.InstructionEnumeration();
}
+
+ ///
+ /// Make our tumble climb follow our hand rotation instead of the camera rotation
+ ///
+ [HarmonyPatch(typeof(PhysGrabber), nameof(PhysGrabber.GrabStateClimb))]
+ [HarmonyTranspiler]
+ private static IEnumerable HandBasedTumbleClimbPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false,
+ new CodeMatch(OpCodes.Stloc_2))
+ .Advance(-2)
+ .SetAndAdvance(OpCodes.Call, ((Func)GetRotation).Method)
+ .RemoveInstruction()
+ .InstructionEnumeration();
+
+ static Quaternion GetRotation(PhysGrabber grabber)
+ {
+ if (grabber.playerAvatar.isLocal)
+ return VRSession.InVR
+ ? VRSession.Instance.Player.MainHand.rotation * Quaternion.Euler(0, 180, 0)
+ : grabber.climbStickTransform.rotation;
+
+ if (!NetworkSystem.instance)
+ return grabber.climbStickTransform.rotation;
+
+ return NetworkSystem.instance.GetNetworkPlayer(grabber.playerAvatar, out var networkPlayer)
+ ? networkPlayer.PrimaryHand.rotation * Quaternion.Euler(0, 180, 0)
+ : grabber.climbStickTransform.rotation;
+ }
+ }
}
\ No newline at end of file
diff --git a/Source/Patches/Player/InventoryPatches.cs b/Source/Patches/Player/InventoryPatches.cs
index 8f1071a..e7e9aef 100644
--- a/Source/Patches/Player/InventoryPatches.cs
+++ b/Source/Patches/Player/InventoryPatches.cs
@@ -140,6 +140,31 @@ static bool ShouldTeleport(PhysGrabObject @object)
}
}
+ ///
+ /// Prevent items from being "hidden" when equipped in an inventory
+ ///
+ [HarmonyPatch(typeof(PhysGrabObject), nameof(PhysGrabObject.OverrideDeactivate))]
+ [HarmonyTranspiler]
+ private static IEnumerable DisableItemOverrideHiding(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false,
+ new CodeMatch(OpCodes.Call, Method(typeof(SemiFunc), nameof(SemiFunc.IsMasterClientOrSingleplayer))))
+ .Set(OpCodes.Call, ((Func)ShouldMoveItem).Method)
+ .Insert(new CodeInstruction(OpCodes.Ldarg_0))
+ .InstructionEnumeration();
+
+ static bool ShouldMoveItem(PhysGrabObject @object)
+ {
+ var result = SemiFunc.IsMasterClientOrSingleplayer();
+
+ if (!@object.TryGetComponent(out var item))
+ return result;
+
+ return result && !ItemIsMine(item);
+ }
+ }
+
[HarmonyPatch(typeof(InventorySpot), nameof(InventorySpot.EquipItem))]
[HarmonyPrefix]
private static void OnItemEquip(InventorySpot __instance, ItemEquippable item)
@@ -187,8 +212,16 @@ internal static class UniversalInventoryPatches
[HarmonyPrefix]
private static bool DontMeleeWhenEquipped(ItemMelee __instance)
{
- return !(__instance.itemEquippable.currentState == ItemEquippable.ItemState.Equipped &&
- (!SemiFunc.IsMultiplayer() || PhotonView.Find(__instance.itemEquippable.ownerPlayerId)
- .GetComponent().IsVRPlayer()));
+ var isEquipped =
+ __instance.itemEquippable.currentState is ItemEquippable.ItemState.Equipped
+ or ItemEquippable.ItemState.Equipping;
+
+ if (!isEquipped) // Return early, as `ownerPlayerId` isn't set here yet
+ return true;
+
+ var isVRPlayer = (SemiFunc.IsMultiplayer() && PhotonView.Find(__instance.itemEquippable.ownerPlayerId)
+ .GetComponent().IsVRPlayer()) || (!SemiFunc.IsMultiplayer() && VRSession.InVR);
+
+ return !isVRPlayer;
}
}
\ No newline at end of file
diff --git a/Source/Patches/Player/MapToolPatches.cs b/Source/Patches/Player/MapToolPatches.cs
index fc9f39a..e3d1b66 100644
--- a/Source/Patches/Player/MapToolPatches.cs
+++ b/Source/Patches/Player/MapToolPatches.cs
@@ -32,8 +32,8 @@ private static void OnMapToolCreated(MapToolController __instance)
private static IEnumerable MapToolDisableInput(IEnumerable instructions)
{
return new CodeMatcher(instructions)
- .Advance(1)
- .RemoveInstructions(87)
+ .Start()
+ .RemoveInstructions(89)
.InstructionEnumeration();
}
diff --git a/Source/Patches/Player/PlayerAvatarPatches.cs b/Source/Patches/Player/PlayerAvatarPatches.cs
index 957e973..bf037d3 100644
--- a/Source/Patches/Player/PlayerAvatarPatches.cs
+++ b/Source/Patches/Player/PlayerAvatarPatches.cs
@@ -1,7 +1,12 @@
-using HarmonyLib;
+using System.Collections.Generic;
+using System.Reflection.Emit;
+using HarmonyLib;
using RepoXR.Managers;
+using RepoXR.Player.Camera;
using UnityEngine;
+using static HarmonyLib.AccessTools;
+
namespace RepoXR.Patches.Player;
[RepoXRPatch]
@@ -14,7 +19,7 @@ internal static class PlayerAvatarPatches
[HarmonyPostfix]
private static void OnPlayerDeath(PlayerAvatar __instance)
{
- if (!__instance.isLocal || VRSession.Instance is not {} session)
+ if (!__instance.isLocal || VRSession.Instance is not { } session)
return;
session.Player.Rig.SetVisible(false);
@@ -29,7 +34,7 @@ private static void OnPlayerRevive(PlayerAvatar __instance)
{
if (!__instance.isLocal || VRSession.Instance is not { } session)
return;
-
+
session.Player.Rig.SetVisible(true);
// Reset CameraAimOffset (for when revived during the top-down death sequence)
@@ -37,4 +42,21 @@ private static void OnPlayerRevive(PlayerAvatar __instance)
offsetTransform.localRotation = Quaternion.identity;
offsetTransform.localPosition = Vector3.zero;
}
+
+ ///
+ /// Look at the enemy that killed you (if possible)
+ ///
+ [HarmonyPatch(typeof(PlayerAvatar), nameof(PlayerAvatar.Update))]
+ [HarmonyTranspiler]
+ private static IEnumerable PlayerDeathLookAtEnemyPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false,
+ new CodeMatch(OpCodes.Ldsfld, Field(typeof(CameraAim), nameof(CameraAim.Instance))))
+ .SetOperandAndAdvance(Field(typeof(VRCameraAim), nameof(VRCameraAim.instance)))
+ .Advance(9)
+ .InsertAndAdvance(new CodeInstruction(OpCodes.Ldc_I4_1))
+ .SetOperandAndAdvance(Method(typeof(VRCameraAim), nameof(VRCameraAim.SetAimTarget)))
+ .InstructionEnumeration();
+ }
}
\ No newline at end of file
diff --git a/Source/Patches/Player/PlayerDeathHeadPatches.cs b/Source/Patches/Player/PlayerDeathHeadPatches.cs
new file mode 100644
index 0000000..a07c3f3
--- /dev/null
+++ b/Source/Patches/Player/PlayerDeathHeadPatches.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Reflection.Emit;
+using HarmonyLib;
+using RepoXR.Player;
+
+using static HarmonyLib.AccessTools;
+
+namespace RepoXR.Patches.Player;
+
+[RepoXRPatch]
+internal static class PlayerDeathHeadPatches
+{
+ ///
+ /// Replace the "on screen" detection with custom detection that is better suited for VR and supports
+ /// eye tracking
+ ///
+ [HarmonyPatch(typeof(PlayerDeathHead), nameof(PlayerDeathHead.Update))]
+ [HarmonyTranspiler]
+ private static IEnumerable LookAtHeadPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false, new CodeMatch(OpCodes.Call, Method(typeof(SemiFunc), nameof(SemiFunc.OnScreen))))
+ .SetOperandAndAdvance(Method(typeof(VREyeTracking), nameof(VREyeTracking.LookingAt)))
+ .InstructionEnumeration();
+ }
+}
\ No newline at end of file
diff --git a/Source/Patches/Player/PlayerEyesPatches.cs b/Source/Patches/Player/PlayerEyesPatches.cs
new file mode 100644
index 0000000..1b6d439
--- /dev/null
+++ b/Source/Patches/Player/PlayerEyesPatches.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection.Emit;
+using HarmonyLib;
+using RepoXR.Networking;
+
+namespace RepoXR.Patches.Player;
+
+[RepoXRPatch(RepoXRPatchTarget.Universal)]
+internal static class PlayerEyesPatches
+{
+ ///
+ /// Make players that use eye tracking have their eyes controller by their *real* eyes
+ ///
+ [HarmonyPatch(typeof(PlayerEyes), nameof(PlayerEyes.LookAtTransform))]
+ [HarmonyPostfix]
+ private static void LookAtTransformEyeTracking(PlayerEyes __instance)
+ {
+ if (!__instance.playerAvatar || __instance.playerAvatar.isLocal)
+ return;
+
+ if (!NetworkSystem.instance.GetNetworkPlayer(__instance.playerAvatar, out var player) || !player.EyeTracking)
+ return;
+
+ __instance.lookAtActive = true;
+ __instance.lookAt.transform.position = player.EyeGazePoint;
+ }
+
+ ///
+ /// Make sure eye tracked players don't move their heads by merely looking around
+ ///
+ [HarmonyPatch(typeof(PlayerAvatarVisuals), nameof(PlayerAvatarVisuals.Update))]
+ [HarmonyTranspiler]
+ private static IEnumerable KeepHeadRotationPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false, new CodeMatch(OpCodes.Ldc_R4, 40f))
+ .SetAndAdvance(OpCodes.Ldarg_0, null)
+ .Insert(new CodeInstruction(OpCodes.Call, ((Func)GetMaxAngle).Method))
+ .InstructionEnumeration();
+
+ static float GetMaxAngle(PlayerAvatarVisuals visuals)
+ {
+ if (visuals.isMenuAvatar)
+ return 40;
+
+ if (!NetworkSystem.instance.GetNetworkPlayer(visuals.playerAvatar, out var player) || !player.EyeTracking)
+ return 40;
+
+ // Do not angle the head with the eyes if eye tracking is enabled
+ return 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/Patches/SpectatePatches.cs b/Source/Patches/SpectatePatches.cs
index e988fdf..66eff18 100644
--- a/Source/Patches/SpectatePatches.cs
+++ b/Source/Patches/SpectatePatches.cs
@@ -3,6 +3,7 @@
using System.Reflection.Emit;
using HarmonyLib;
using RepoXR.Input;
+using RepoXR.Player.Camera;
using UnityEngine;
using static HarmonyLib.AccessTools;
@@ -153,7 +154,7 @@ private static void CameraTurnPatch(SpectateCamera __instance)
break;
case Config.TurnProviderOption.Smooth:
- if (!Plugin.Config.DynamicSmoothSpeed.Value)
+ if (!Plugin.Config.AnalogSmoothTurn.Value)
value = value == 0 ? 0 : Math.Sign(value);
spectateTurnAmount += 180 * Time.deltaTime * Plugin.Config.SmoothTurnSpeedModifier.Value * value;
@@ -163,4 +164,64 @@ private static void CameraTurnPatch(SpectateCamera __instance)
break;
}
}
+
+ ///
+ /// Allow snap/smooth turning in the spectator head camera
+ ///
+ [HarmonyPatch(typeof(SpectateCamera), nameof(SpectateCamera.StateHead))]
+ [HarmonyPostfix]
+ private static void HeadCameraTurnPatch(SpectateCamera __instance)
+ {
+ var value = Actions.Instance["Turn"].ReadValue();
+
+ switch (Plugin.Config.TurnProvider.Value)
+ {
+ case Config.TurnProviderOption.Snap:
+ var should = Mathf.Abs(value) > 0.75f;
+ var snapSize = Plugin.Config.SnapTurnSize.Value;
+
+ if (!turnedLastInput && should)
+ if (value > 0)
+ VRCameraAim.instance.TurnAimNow(snapSize);
+ else
+ VRCameraAim.instance.TurnAimNow(-snapSize);
+
+ turnedLastInput = should;
+
+ break;
+
+ case Config.TurnProviderOption.Smooth:
+ if (!Plugin.Config.AnalogSmoothTurn.Value)
+ value = value == 0 ? 0 : Math.Sign(value);
+
+ VRCameraAim.instance.TurnAimNow(180 * Time.deltaTime * Plugin.Config.SmoothTurnSpeedModifier.Value * value);
+ break;
+
+ case Config.TurnProviderOption.Disabled:
+ break;
+ }
+ }
+
+ ///
+ /// Make sure the VR camera is always centered to the player's death head (3-DoF)
+ ///
+ [HarmonyPatch(typeof(SpectateCamera), nameof(SpectateCamera.StateHead))]
+ [HarmonyTranspiler]
+ private static IEnumerable AlignCameraWithHeadPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false, new CodeMatch(OpCodes.Call, PropertyGetter(typeof(Time), nameof(Time.deltaTime))))
+ .Advance(-3)
+ .InsertAndAdvance(new CodeInstruction(OpCodes.Ldarg_0))
+ .SetAndAdvance(OpCodes.Call, ((Func)GetTargetPoint).Method)
+ .RemoveInstruction()
+ .InstructionEnumeration();
+
+ static Vector3 GetTargetPoint(PlayerDeathHead head, SpectateCamera camera)
+ {
+ var cameraPos = camera.transform.InverseTransformPoint(Camera.main!.transform.position);
+
+ return head.physGrabObject.centerPoint - camera.transform.rotation * cameraPos;
+ }
+ }
}
diff --git a/Source/Patches/UI/ChatPatches.cs b/Source/Patches/UI/ChatPatches.cs
index 2b4e360..5822121 100644
--- a/Source/Patches/UI/ChatPatches.cs
+++ b/Source/Patches/UI/ChatPatches.cs
@@ -29,7 +29,7 @@ private static IEnumerable ChatOpenButtonPatch(IEnumerable ChatCloseButtonPatch(IEnumerable
+ /// Reset position when loading UI is shown
+ ///
+ [HarmonyPatch(typeof(LoadingUI), nameof(LoadingUI.StartLoading))]
+ [HarmonyPostfix]
+ private static void OnStartLoading()
+ {
+ Object.FindObjectOfType()?.ResetPosition();
+ }
+
///
/// Fix the controller binding icon on the stuck text and mask it away when it's not shown
///
diff --git a/Source/Patches/UI/TutorialPatches.cs b/Source/Patches/UI/TutorialPatches.cs
index b76aa62..961d2ef 100644
--- a/Source/Patches/UI/TutorialPatches.cs
+++ b/Source/Patches/UI/TutorialPatches.cs
@@ -115,4 +115,14 @@ private static void TruckForceRotate(TutorialTruckTrigger __instance)
VRCameraAim.instance.SetAimTarget(__instance.lookTarget.position + Vector3.down, 0.1f, 5, __instance.gameObject,
90, true);
}
+
+ ///
+ /// Make the spectate head prompt UI have the correct control sprites
+ ///
+ [HarmonyPatch(typeof(SpectateHeadUI), nameof(SpectateHeadUI.Awake))]
+ [HarmonyPostfix]
+ private static void OnSpectateHeadUICreate(SpectateHeadUI __instance)
+ {
+ __instance.promptText.spriteAsset = AssetCollection.TMPInputsSpriteAsset;
+ }
}
\ No newline at end of file
diff --git a/Source/Patches/UI/UIPatches.cs b/Source/Patches/UI/UIPatches.cs
index 303ba11..e183df8 100644
--- a/Source/Patches/UI/UIPatches.cs
+++ b/Source/Patches/UI/UIPatches.cs
@@ -219,9 +219,10 @@ private static void HandleVRScrollLogic(MenuScrollBox __instance)
__instance.scrollHandleTargetPosition = pos;
}
- if (manager.GetUIScrollY() != 0)
+ if (manager.GetUIScrollY() != 0 && SemiFunc.NoTextInputsActive())
{
__instance.scrollHandleTargetPosition += manager.GetUIScrollY() * 20 / (__instance.scrollHeight * 0.01f);
+
if (__instance.scrollHandleTargetPosition < __instance.scrollHandle.sizeDelta.y / 2f)
__instance.scrollHandleTargetPosition = __instance.scrollHandle.sizeDelta.y / 2f;
if (__instance.scrollHandleTargetPosition >
@@ -276,8 +277,7 @@ private static IEnumerable ScrollDisableInputs(IEnumerable
diff --git a/Source/Patches/UI/ValuableDiscoverGraphicPatches.cs b/Source/Patches/UI/ValuableDiscoverGraphicPatches.cs
deleted file mode 100644
index 5885369..0000000
--- a/Source/Patches/UI/ValuableDiscoverGraphicPatches.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-using HarmonyLib;
-using RepoXR.Managers;
-using UnityEngine;
-
-namespace RepoXR.Patches.UI;
-
-[RepoXRPatch]
-internal static class ValuableDiscoverGraphicPatches
-{
- [HarmonyPatch(typeof(ValuableDiscoverGraphic), nameof(ValuableDiscoverGraphic.Start))]
- [HarmonyPostfix]
- private static void OnValuableDiscovered(ValuableDiscoverGraphic __instance)
- {
- // Create canvas for rendering in world space
- var canvas = new GameObject("World Space Valuable Graphic") { layer = 5 }.AddComponent