diff --git a/.github/workflows/build-debug.yaml b/.github/workflows/build-debug.yaml
index 48bc60f..8ce67f9 100644
--- a/.github/workflows/build-debug.yaml
+++ b/.github/workflows/build-debug.yaml
@@ -56,6 +56,9 @@ jobs:
# Set up template
mkdir package
git --work-tree=./package checkout origin/thunderstore ./
+
+ # Create patchers directory
+ mkdir -p ./package/BepInEx/patchers/LCVR/
# Copy and sign debug binaries
cp bin/Debug/netstandard2.1/LCVR.dll ./package/BepInEx/plugins/LCVR/
diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml
index bb1de7d..9d48532 100644
--- a/.github/workflows/build-release.yaml
+++ b/.github/workflows/build-release.yaml
@@ -55,6 +55,9 @@ jobs:
# Set up template
mkdir package
git --work-tree=./package checkout origin/thunderstore ./
+
+ # Create patchers directory
+ mkdir -p ./package/BepInEx/patchers/LCVR/
# Copy and sign release binaries
cp bin/Release/netstandard2.1/LCVR.dll ./package/BepInEx/plugins/LCVR/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9b815a..05937ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,30 @@
+# 1.5.0
+
+**Additions**:
+- Added support for the new utility slot, which is accessible like a normal inventory slot
+- Added the status text UI (like oxygen level critical) to the VR interface
+- Added a fog quality option
+- Added audible item shaking on certain supported items
+
+**Changes**:
+- Changed rendering to Single Pass Instanced, this will break a lot of mods
+- Entrance teleports now behave the same as vanilla when determining your looking direction
+- Motion blur has been disabled and cannot be enabled while in VR
+- Removed `TypeLoadExceptionFixer` dependency, fixing some issues with UnityExplorer
+- Removed toggle sprint option as this is now a vanilla setting
+- Reverted the vignette shader back to the built-in one, which doesn't look as good but still functions normally for now
+- Spectators can no longer experience the underwater effect
+- Made some visual changes to some assets
+
+**Removals**:
+- Removed dynamic resolution support as it doesn't seem to function properly with SPI rendering
+- Removed the ladder climb interaction for now until a better replacement is ready
+
+**Fixes**:
+- Fixed smooth camera having jarring motion after player teleports
+- Fixed nutcrackers following spectators after kicking them
+- Fixed loading screen partially covering the custom camera
+
# 1.4.6
**Additions**:
diff --git a/Docs/COMPATIBILITY.md b/Docs/COMPATIBILITY.md
new file mode 100644
index 0000000..7e9d351
--- /dev/null
+++ b/Docs/COMPATIBILITY.md
@@ -0,0 +1,34 @@
+# Compatibility with LCVR
+
+> This document is specifically for mods that add anything that can be rendered by the game, it is not about code or hooking into the VR mod.
+
+With the VR mod *finally* hopping over to [Single Pass Instanced rendering](https://docs.unity3d.com/6000.6/Documentation/Manual/SinglePassInstancing.html), a lot of mods will break due to them missing SPI support in their exported shaders.
+
+Single Pass Instanced rendering requires two things from shaders for them to render properly, which are:
+
+- The shaders **must** support GPU instancing
+- The shaders **must** be built with Stereo variants
+
+# Supporting SPI in custom shaders
+
+> Full technical details are explained [over here](https://docs.unity3d.com/6000.6/Documentation/Manual/SinglePassInstancing.html)
+
+If you are shipping custom/hand written shaders in your mods, you must make changes to the shaders for them to work correctly.
+
+These changes differ depending on what kind of shader you are writing (e.g. post processing shaders require additional changes).
+
+If you are **not** using custom shaders, you do not have to modify anything.
+
+# Exporting shaders with Stereo variants
+
+Even if your shaders already support SPI rendering (whether you're only using HDRP built-in shaders, or your custom shaders already support SPI), Unity will still strip this support out of the shaders by default.
+
+To tell Unity to also build in SPI support when exporting your assets, you will have to install the Unity OpenXR plugin.
+
+
+
+> You can also add the package by name: `com.unity.xr.openxr`
+
+You do not have to configure anything after installing this plugin, just re-export your asset bundle and all bundled shaders will now have SPI variants built in.
+
+> In the case play mode starts behaving weirdly, disable VR by going to `Player Settings` -> `XR Plug-in Management` and disabling the `Initialize XR on Startup` option
diff --git a/Docs/Thunderstore/README.md b/Docs/Thunderstore/README.md
index ffa952f..3370c00 100644
--- a/Docs/Thunderstore/README.md
+++ b/Docs/Thunderstore/README.md
@@ -65,7 +65,9 @@ Here is a list of LCVR versions and which version(s) of Lethal Company it suppor
| LCVR | Lethal Company |
|-------------------|-------------------|
-| v1.4.6 *(LATEST)* | V73 |
+| v1.5.0 *(LATEST)* | V81 |
+| v1.4.7 | V73 |
+| v1.4.6 | V73 |
| v1.4.5 | V73 |
| v1.4.4 | V73 |
| v1.4.3 | V73 |
@@ -110,10 +112,6 @@ Most mods should all work fine with LCVR, like interior mods, new moons, most it
In general, most emote mods, mods adding UI elements and mods that require new bindings are not compatible with LCVR by default, and either require configuration changes, or dedicated VR support.
-# Hot switching
-
-You can swap between VR and flatscreen when using LCVR. For more info, check the [Hot Switching Wiki Page](https://thunderstore.io/c/lethal-company/p/DaXcess/LethalCompanyVR/wiki/2888-hot-switching/).
-
# Configuring the mod
You can change the mod configuration from within the game itself. Just launch the game with the VR mod installed, get to the main menu, and press the big VR button on the right side of the screen. This will open a big settings menu where you can configure the VR mod to your liking.
@@ -239,7 +237,7 @@ This mod, in addition to adding VR and motion controls, also adds a few special
Hate having to just watch a flat screen where your fellow employees die to the horrors of the facilities? Well fear no more! With the new Company™ Device© you retain the rights to wander the desolate planets even when your physical body is no longer showing signs compatible with life!
-_Since the company was a big fan of using Linux for the Device©, the colors look more gray when dead since they cheaped out on the HDR support._
+_Since the company wants to cut costs, the colors look more gray when dead. This is for **your** benefit!_
You can teleport to other employees by using the **Interact** _(Default: Right Controller Trigger)_ button. This will cycle through each employee in the lobby that has not yet met their maker. Use this to quickly see how a fellow employee is going about their day, or to get unstuck if you have fallen into a pit.
diff --git a/LCVR.csproj b/LCVR.csproj
index be075fe..bd2c20f 100644
--- a/LCVR.csproj
+++ b/LCVR.csproj
@@ -4,12 +4,12 @@
+ /// Disable motion blur
+ ///
+ [HarmonyPatch(typeof(IngamePlayerSettings), nameof(IngamePlayerSettings.SetMotionBlur))]
+ [HarmonyPostfix]
+ private static void DisableMotionBlur(IngamePlayerSettings __instance)
+ {
+ if (__instance.universalVolume.sharedProfile.TryGet(out var motionBlur))
+ motionBlur.active = false;
+ }
}
\ No newline at end of file
diff --git a/Source/Patches/InputPatches.cs b/Source/Patches/InputPatches.cs
index fed03e3..a3ad3b4 100644
--- a/Source/Patches/InputPatches.cs
+++ b/Source/Patches/InputPatches.cs
@@ -38,6 +38,17 @@ internal static void OnCreateSettings(IngamePlayerSettings __instance)
playerInput.enabled = true;
}
+ ///
+ /// Force lethal to use the PlayerInput component for actions (for automatic controller detection)
+ ///
+ [HarmonyPatch(typeof(InputSystem), nameof(InputSystem.actions), MethodType.Getter)]
+ [HarmonyPrefix]
+ private static bool ReplaceActions(ref InputActionAsset __result)
+ {
+ __result = IngamePlayerSettings.Instance.playerInput.actions;
+ return false;
+ }
+
internal static void RestoreOriginalBindings()
{
var playerInput = IngamePlayerSettings.Instance.playerInput;
@@ -82,7 +93,7 @@ private static IEnumerable DiscardSettingsDontTouchMyOverrides(
Field(typeof(IngamePlayerSettings.Settings), nameof(IngamePlayerSettings.Settings.keyBindings)))
])
.Advance(-2)
- .RemoveInstructions(18)
+ .RemoveInstructions(14)
.InstructionEnumeration();
}
diff --git a/Source/Patches/Items/BeltItemPatches.cs b/Source/Patches/Items/BeltItemPatches.cs
index 0af3a94..e0ae82d 100644
--- a/Source/Patches/Items/BeltItemPatches.cs
+++ b/Source/Patches/Items/BeltItemPatches.cs
@@ -1,4 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection.Emit;
+using GameNetcodeStuff;
using HarmonyLib;
+using LCVR.Managers;
+using UnityEngine;
+
+using static HarmonyLib.AccessTools;
namespace LCVR.Patches.Items;
@@ -42,4 +50,24 @@ private static bool CloseBagPatch(BeltBagItem __instance, bool buttonDown)
StartOfRound.Instance.localPlayerController.SetInSpecialMenu(false);
return false;
}
+
+ ///
+ /// Make the belt check for items from your hand instead of your head
+ ///
+ [HarmonyPatch(typeof(BeltBagItem), nameof(BeltBagItem.ItemInteractLeftRight))]
+ [HarmonyTranspiler]
+ private static IEnumerable RayFromHandPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false,
+ new CodeMatch(OpCodes.Ldfld,
+ Field(typeof(PlayerControllerB), nameof(PlayerControllerB.gameplayCamera))))
+ .Repeat(matcher =>
+ matcher.Set(OpCodes.Call, ((Func)GetPlayerInteractTransform).Method))
+ .InstructionEnumeration();
+
+ static Transform GetPlayerInteractTransform(PlayerControllerB player) => player.IsLocalPlayer()
+ ? VRSession.Instance.LocalPlayer.PrimaryController.InteractOrigin
+ : player.gameplayCamera.transform;
+ }
}
diff --git a/Source/Patches/Items/SprayPaintItemPatches.cs b/Source/Patches/Items/SprayPaintItemPatches.cs
index 0a7c8d9..0c00a31 100644
--- a/Source/Patches/Items/SprayPaintItemPatches.cs
+++ b/Source/Patches/Items/SprayPaintItemPatches.cs
@@ -5,6 +5,7 @@
using LCVR.Managers;
using LCVR.Player;
using UnityEngine;
+
using static HarmonyLib.AccessTools;
namespace LCVR.Patches.Items;
@@ -53,7 +54,7 @@ private static bool SprayPaintFromHand(SprayPaintItem __instance, ref bool __res
private static IEnumerable WeedKillerSprayFromHand(IEnumerable instructions)
{
return new CodeMatcher(instructions)
- .Advance(1)
+ .Advance(3)
.RemoveInstructions(13)
.InsertAndAdvance(
new CodeInstruction(OpCodes.Call, ((Func)GetSprayPosition).Method)
diff --git a/Source/Patches/PlayerControllerPatches.cs b/Source/Patches/PlayerControllerPatches.cs
index 5514d1c..27162d5 100644
--- a/Source/Patches/PlayerControllerPatches.cs
+++ b/Source/Patches/PlayerControllerPatches.cs
@@ -83,10 +83,15 @@ internal static class PlayerControllerPatches
private static IEnumerable SprintPatch(IEnumerable instructions)
{
return new CodeMatcher(instructions)
+ // Insert our sprint value as truth
.MatchForward(false, new CodeMatch(OpCodes.Ldstr, "Sprint"))
- .Advance(-3)
+ .Advance(-1)
.SetAndAdvance(OpCodes.Call, ((Func)GetSprintValue).Method)
- .RemoveInstructions(6)
+ .RemoveInstructions(4)
+ // Remove vanilla toggle sprint
+ .MatchForward(false, new CodeMatch(OpCodes.Call, PropertyGetter(typeof(IngamePlayerSettings), nameof(IngamePlayerSettings.Instance))))
+ .SetOpcodeAndAdvance(OpCodes.Ldc_I4_0)
+ .RemoveInstructions(2)
.InstructionEnumeration();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -424,6 +429,48 @@ static void SetAnglesIfNotOwner(PlayerControllerB player, Vector3 angles)
player.gameplayCamera.transform.localEulerAngles = angles;
}
}
+
+ ///
+ /// Make just scrolling between slots also scroll through the utility slot in VR
+ ///
+ [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.NextItemSlot))]
+ [HarmonyPrefix]
+ private static bool ScrollToUtilitySlot(PlayerControllerB __instance, bool forward, ref int __result)
+ {
+ // Scrolling away is already handled by the game
+ if (__instance.currentItemSlot == 0x32)
+ return true;
+
+ // If we scroll backwards on the first slot, go to utility
+ if (__instance.currentItemSlot == 0 && !forward)
+ {
+ __result = 0x32;
+ return false;
+ }
+
+ // If we scroll forwards on the last slot, go to utility
+ if (__instance.currentItemSlot + 1 == __instance.ItemSlots.Length && forward)
+ {
+ __result = 0x32;
+ return false;
+ }
+
+ // Otherwise fall back to vanilla behavior
+ return true;
+ }
+
+ ///
+ /// Force custom camera to update immediately instead of animating after a teleport
+ ///
+ [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.TeleportPlayer))]
+ [HarmonyPostfix]
+ private static void OnPlayerTeleport(PlayerControllerB __instance)
+ {
+ if (!__instance.IsLocalPlayer())
+ return;
+
+ VRSession.Instance.ForceUpdateCamera();
+ }
}
[LCVRPatch(LCVRPatchTarget.Universal)]
diff --git a/Source/Patches/Rendering/MaterialPatches.cs b/Source/Patches/Rendering/MaterialPatches.cs
new file mode 100644
index 0000000..9338c1d
--- /dev/null
+++ b/Source/Patches/Rendering/MaterialPatches.cs
@@ -0,0 +1,34 @@
+using HarmonyLib;
+using LCVR.Assets;
+using UnityEngine;
+
+namespace LCVR.Patches.Rendering;
+
+[LCVRPatch]
+[HarmonyPatch]
+internal static class MaterialPatches
+{
+ ///
+ /// Replace some ship particles once level loads
+ ///
+ [HarmonyPatch(typeof(Terminal), nameof(Terminal.Awake))]
+ [HarmonyPostfix]
+ private static void OnShipLevelLoad()
+ {
+ var giantMagnet = GameObject.Find("GiantCylinderMagnet").transform;
+
+ var magnetParticle1 = giantMagnet.Find("MagnetParticle").GetComponent();
+ var magnetParticle2 =
+ magnetParticle1.transform.Find("MagnetParticle (1)").GetComponent();
+ var magnetParticle3 =
+ magnetParticle1.transform.Find("MagnetParticle (2)").GetComponent();
+
+ var chargerParticle = GameObject.Find("ShipModels2b").transform.Find("ChargeStation/ZapParticle")
+ .GetComponent();
+
+ magnetParticle1.material = magnetParticle2.material =
+ AssetManager.assetsBundle.LoadAsset("LightningSpriteSheetMaterial3");
+ magnetParticle3.material = AssetManager.assetsBundle.LoadAsset("LightningSpriteSheetMaterial2");
+ chargerParticle.material = AssetManager.assetsBundle.LoadAsset("LightningSpriteSheetMaterial");
+ }
+}
diff --git a/Source/Patches/Rendering/RenderingPatches.cs b/Source/Patches/Rendering/RenderingPatches.cs
new file mode 100644
index 0000000..4a6e5df
--- /dev/null
+++ b/Source/Patches/Rendering/RenderingPatches.cs
@@ -0,0 +1,41 @@
+using HarmonyLib;
+using LCVR.Managers;
+using UnityEngine;
+using UnityEngine.Rendering;
+using UnityEngine.Rendering.HighDefinition;
+
+namespace LCVR.Patches.Rendering;
+
+[LCVRPatch]
+[HarmonyPatch]
+internal static class RenderingPatches
+{
+ ///
+ /// Fixes left-right eye rendering issues when using local volumetric fog
+ ///
+ [HarmonyPatch(typeof(LocalVolumetricFog), nameof(LocalVolumetricFog.OnEnable))]
+ [HarmonyPostfix]
+ private static void FixedBlendMode(LocalVolumetricFog __instance)
+ {
+ __instance.parameters.blendingMode = LocalVolumetricFogBlendingMode.Overwrite;
+ }
+
+ ///
+ /// Also render batched children in the custom camera
+ ///
+ [HarmonyPatch(typeof(BatchAllMeshChildren), nameof(BatchAllMeshChildren.RenderBatches))]
+ [HarmonyPostfix]
+ private static void RenderInCustomCamera(BatchAllMeshChildren __instance, bool renderOnHeadmountedCam)
+ {
+ if (renderOnHeadmountedCam || VRSession.Instance.CustomCamera is not {} camera)
+ return;
+
+ if (!StartOfRound.Instance.activeCamera.enabled)
+ return;
+
+ foreach (var batch in __instance.Batches)
+ for (var i = 0; i < __instance.mesh.subMeshCount; i++)
+ Graphics.DrawMeshInstanced(__instance.mesh, i, __instance.material, batch, null, ShadowCastingMode.On,
+ true, 24, camera);
+ }
+}
diff --git a/Source/Patches/SettingsOptionPatches.cs b/Source/Patches/SettingsOptionPatches.cs
index a14abc4..389ef5f 100644
--- a/Source/Patches/SettingsOptionPatches.cs
+++ b/Source/Patches/SettingsOptionPatches.cs
@@ -1,4 +1,5 @@
using HarmonyLib;
+using TMPro;
namespace LCVR.Patches;
@@ -16,4 +17,20 @@ private static bool IgnoreBindingTextReload()
{
return false;
}
+
+ ///
+ /// Disable the Motion Blur option in the settings menu
+ ///
+ [HarmonyPatch(typeof(SettingsOption), nameof(SettingsOption.OnEnable))]
+ [HarmonyPostfix]
+ private static void DisableMotionBlurOption(SettingsOption __instance)
+ {
+ if (__instance.optionType != SettingsOptionType.MotionBlur)
+ return;
+
+ var dropdown = __instance.GetComponent();
+ dropdown.interactable = false;
+ dropdown.enabled = false;
+ dropdown.captionText.text = "Absolutely not";
+ }
}
diff --git a/Source/Patches/Spectating/AIPatches.cs b/Source/Patches/Spectating/AIPatches.cs
index c151934..5f6e30c 100644
--- a/Source/Patches/Spectating/AIPatches.cs
+++ b/Source/Patches/Spectating/AIPatches.cs
@@ -1,6 +1,8 @@
+using System;
using System.Collections.Generic;
-using System.Reflection;
+using System.Linq;
using System.Reflection.Emit;
+using System.Runtime.CompilerServices;
using GameNetcodeStuff;
using HarmonyLib;
using UnityEngine;
@@ -49,25 +51,14 @@ private static IEnumerable LineOfSightPlayerIgnoreDeadPlayer(
{
return new CodeMatcher(instructions, generator)
.MatchForward(false,
- new CodeMatch(i => i.opcode == OpCodes.Ldfld && (FieldInfo)i.operand ==
- Field(typeof(StartOfRound), nameof(StartOfRound.allPlayerScripts))))
- .Advance(-1)
- .Insert(new CodeInstruction(OpCodes.Call,
- PropertyGetter(typeof(StartOfRound), nameof(StartOfRound.Instance))))
- .CreateLabel(out var label)
- .Advance(1)
- .InsertAndAdvance(new CodeInstruction(OpCodes.Ldfld,
- Field(typeof(StartOfRound), nameof(StartOfRound.allPlayerScripts))))
- .InsertAndAdvance(new CodeInstruction(OpCodes.Ldloc_1))
- .InsertAndAdvance(new CodeInstruction(OpCodes.Ldelem_Ref))
- .InsertAndAdvance(new CodeInstruction(OpCodes.Ldfld,
- Field(typeof(PlayerControllerB), nameof(PlayerControllerB.isPlayerDead))))
- .InsertBranchAndAdvance(OpCodes.Brtrue, 78)
- .MatchForward(false, new CodeMatch(OpCodes.Blt))
- .Advance(1)
- .MatchForward(false, new CodeMatch(OpCodes.Blt))
- .SetOperandAndAdvance(label)
+ new CodeMatch(OpCodes.Ldfld, Field(typeof(StartOfRound), nameof(StartOfRound.allPlayerScripts))))
+ .Repeat(matcher =>
+ matcher.Set(OpCodes.Call, ((Func)GetAlivePlayers).Method))
.InstructionEnumeration();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static PlayerControllerB[] GetAlivePlayers(StartOfRound round) =>
+ round.allPlayerScripts.Where(p => !p.isPlayerDead).ToArray();
}
///
@@ -80,25 +71,14 @@ private static IEnumerable LineOfSightClosestPlayerIgnoreDeadPl
{
return new CodeMatcher(instructions, generator)
.MatchForward(false,
- new CodeMatch(i => i.opcode == OpCodes.Ldfld && (FieldInfo)i.operand ==
- Field(typeof(StartOfRound), nameof(StartOfRound.allPlayerScripts))))
- .Advance(-1)
- .Insert(new CodeInstruction(OpCodes.Call,
- PropertyGetter(typeof(StartOfRound), nameof(StartOfRound.Instance))))
- .CreateLabel(out var label)
- .Advance(1)
- .InsertAndAdvance(new CodeInstruction(OpCodes.Ldfld,
- Field(typeof(StartOfRound), nameof(StartOfRound.allPlayerScripts))))
- .InsertAndAdvance(new CodeInstruction(OpCodes.Ldloc_S, 4))
- .InsertAndAdvance(new CodeInstruction(OpCodes.Ldelem_Ref))
- .InsertAndAdvance(new CodeInstruction(OpCodes.Ldfld,
- Field(typeof(PlayerControllerB), nameof(PlayerControllerB.isPlayerDead))))
- .InsertBranchAndAdvance(OpCodes.Brtrue, 79)
- .MatchForward(false, new CodeMatch(OpCodes.Blt))
- .Advance(1)
- .MatchForward(false, new CodeMatch(OpCodes.Blt))
- .SetOperandAndAdvance(label)
+ new CodeMatch(OpCodes.Ldfld, Field(typeof(StartOfRound), nameof(StartOfRound.allPlayerScripts))))
+ .Repeat(matcher =>
+ matcher.Set(OpCodes.Call, ((Func)GetAlivePlayers).Method))
.InstructionEnumeration();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static PlayerControllerB[] GetAlivePlayers(StartOfRound round) =>
+ round.allPlayerScripts.Where(p => !p.isPlayerDead).ToArray();
}
///
diff --git a/Source/Patches/Spectating/EnemyPatches.cs b/Source/Patches/Spectating/EnemyPatches.cs
index ceac0db..5f19dd4 100644
--- a/Source/Patches/Spectating/EnemyPatches.cs
+++ b/Source/Patches/Spectating/EnemyPatches.cs
@@ -33,6 +33,36 @@ private static bool NutcrackerCheckForLocalPlayer(ref bool __result)
return false;
}
+ ///
+ /// Prevent a nutcracker from keeping aggro on a spectator after killing them
+ ///
+ [HarmonyPatch(typeof(NutcrackerEnemyAI), nameof(NutcrackerEnemyAI.Update))]
+ [HarmonyTranspiler]
+ private static IEnumerable NutcrackerLoseAggroPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false,
+ new CodeMatch(OpCodes.Call, Method(typeof(EnemyAI), nameof(EnemyAI.CheckLineOfSightForPosition))))
+ .Set(OpCodes.Call,
+ ((Func)CheckLineOfSightAlt).Method)
+ .Insert(
+ new CodeInstruction(OpCodes.Ldarg_0),
+ new CodeInstruction(OpCodes.Ldfld,
+ Field(typeof(NutcrackerEnemyAI), nameof(NutcrackerEnemyAI.lastPlayerSeenMoving)))
+ )
+ .InstructionEnumeration();
+
+ static bool CheckLineOfSightAlt(EnemyAI enemy, Vector3 position, float width, int range,
+ float proximityAwareness, Transform overrideEye, int playerId)
+ {
+ if ((uint)playerId == StartOfRound.Instance.localPlayerController.playerClientId &&
+ StartOfRound.Instance.localPlayerController.isPlayerDead)
+ return false;
+
+ return enemy.CheckLineOfSightForPosition(position, width, range, proximityAwareness, overrideEye);
+ }
+ }
+
///
/// Prevent detection by centipedes that are hidden on the ceiling
///
@@ -148,4 +178,34 @@ static bool CheckCollision(Vector3 position, float radius, int layerMask,
return !everyoneDead;
}
}
+
+ ///
+ /// Fix stingrays triggering when dead player walks over them
+ ///
+ [HarmonyPatch(typeof(StingrayAI), nameof(StingrayAI.Update))]
+ [HarmonyTranspiler]
+ private static IEnumerable StingrayIgnoreDeadPlayer(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ // Prevent spectators from being squirted on
+ .MatchForward(false, new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerControllerB), nameof(PlayerControllerB.gameplayCamera))))
+ .MatchBack(false, new CodeMatch(OpCodes.Ldfld, Field(typeof(GameNetworkManager), nameof(GameNetworkManager.localPlayerController))))
+ .Advance(-1)
+ .RemoveInstructions(3)
+ .Insert(
+ new CodeInstruction(OpCodes.Call, ((Func)IsLocalAndAlive).Method)
+ )
+ // Detect local player step on
+ .MatchForward(false, new CodeMatch(OpCodes.Callvirt, PropertyGetter(typeof(CharacterController), nameof(CharacterController.isGrounded))))
+ .Set(OpCodes.Call, ((Func)IsGroundedAndAlive).Method)
+ .Advance(-1)
+ .RemoveInstruction()
+ .InstructionEnumeration();
+
+ static bool IsGroundedAndAlive(PlayerControllerB player) =>
+ player.thisController.isGrounded && !player.isPlayerDead;
+
+ static bool IsLocalAndAlive(PlayerControllerB player) =>
+ player == GameNetworkManager.Instance.localPlayerController && !player.isPlayerDead;
+ }
}
\ No newline at end of file
diff --git a/Source/Patches/Spectating/EnvironmentPatches.cs b/Source/Patches/Spectating/EnvironmentPatches.cs
index e7c8763..6cc3af6 100644
--- a/Source/Patches/Spectating/EnvironmentPatches.cs
+++ b/Source/Patches/Spectating/EnvironmentPatches.cs
@@ -1,10 +1,13 @@
-using System.Collections;
+using System;
+using System.Collections;
using System.Collections.Generic;
using System.Reflection.Emit;
using GameNetcodeStuff;
using HarmonyLib;
using UnityEngine;
+using static HarmonyLib.AccessTools;
+
namespace LCVR.Patches.Spectating;
///
@@ -140,19 +143,6 @@ private static IEnumerable SpectatorCamDontTriggerWater(IEnumer
.InstructionEnumeration();
}
- ///
- /// Allow dead players to still experience the underwater filter
- ///
- [HarmonyPatch(typeof(PlayerControllerB), nameof(PlayerControllerB.SetFaceUnderwaterFilters))]
- [HarmonyTranspiler]
- private static IEnumerable EnableDeadPlayerUnderwater(IEnumerable instructions)
- {
- return new CodeMatcher(instructions)
- .Advance(1)
- .RemoveInstructions(4)
- .InstructionEnumeration();
- }
-
///
/// Prevent dead players from dying again if they are underwater as a spectator
///
@@ -177,4 +167,22 @@ private static bool DontTouchBallPatch(SoccerBallProp __instance, Collider other
other.GetComponent() != StartOfRound.Instance.localPlayerController ||
!StartOfRound.Instance.localPlayerController.isPlayerDead;
}
+
+ ///
+ /// Prevent spectators from creating water splashes
+ ///
+ [HarmonyPatch(typeof(QuicksandTrigger), nameof(QuicksandTrigger.OnTriggerStay))]
+ [HarmonyTranspiler]
+ private static IEnumerable NoSplashPatch(IEnumerable instructions)
+ {
+ return new CodeMatcher(instructions)
+ .MatchForward(false, new CodeMatch(OpCodes.Ldfld, Field(typeof(PlayerControllerB), nameof(PlayerControllerB.isUnderwater))))
+ .Set(OpCodes.Call, ((Func)PlayerCantSplash).Method)
+ .InstructionEnumeration();
+
+ static bool PlayerCantSplash(PlayerControllerB player)
+ {
+ return player.isPlayerDead || player.isUnderwater;
+ }
+ }
}
diff --git a/Source/Patches/XRPatches.cs b/Source/Patches/XRPatches.cs
index 96b7161..cca6361 100644
--- a/Source/Patches/XRPatches.cs
+++ b/Source/Patches/XRPatches.cs
@@ -3,26 +3,15 @@
using HarmonyLib;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
-using UnityEngine.InputSystem.XR;
-using UnityEngine.Rendering.HighDefinition;
+using UnityEngine.XR.Management;
using static HarmonyLib.AccessTools;
namespace LCVR.Patches;
-[LCVRPatch]
+[LCVRPatch(LCVRPatchTarget.Universal)]
[HarmonyPatch]
internal static class XRPatches
{
- ///
- /// Funny Non-NVIDIA BepInEx Entrypoint quick fix
- ///
- [HarmonyPatch(typeof(XRSupport), nameof(XRSupport.Initialize))]
- [HarmonyPrefix]
- private static bool OnBeforeInitialize()
- {
- return false;
- }
-
///
/// Make the occlusion mesh color black
///
@@ -33,6 +22,16 @@ private static void OnXRSystemInitialize()
XRSystem.s_OcclusionMeshMaterial?.SetVector("_ClearColor", Vector4.zero);
}
+ ///
+ /// Make sure the game doesn't override our OpenXR features
+ ///
+ [HarmonyPatch(typeof(XRGeneralSettings), nameof(XRGeneralSettings.AttemptStartXRSDKOnBeforeSplashScreen))]
+ [HarmonyPrefix]
+ private static void OnBeforeOpenXRInitialize()
+ {
+ OpenXR.Loader.UpdateFeatures();
+ }
+
///
/// Injects an additional offset to the mirror view shader
///
@@ -58,18 +57,4 @@ private static void ApplyUVOffset(ref Vector4 srcRect)
srcRect += new Vector4(0, 0, 1, 0) * Plugin.Config.MirrorXOffset.Value +
new Vector4(0, 0, 0, 1) * Plugin.Config.MirrorYOffset.Value;
}
-
- ///
- /// "XR provider doesn't support rendering Terrain with multipass" even though it does
- ///
- [HarmonyPatch(typeof(HDRenderPipeline), nameof(HDRenderPipeline.PrepareAndCullCamera))]
- [HarmonyTranspiler]
- private static IEnumerable UnityStopLyingToMePatch(IEnumerable instructions)
- {
- return new CodeMatcher(instructions)
- .MatchForward(false, new CodeMatch(OpCodes.Call, Method(typeof(Debug), nameof(Debug.LogWarning), [typeof(object)])))
- .Advance(-8)
- .Set(OpCodes.Ldc_I4_0, null) // Always false
- .InstructionEnumeration();
- }
}
diff --git a/Source/Physics/Interactions/Face.cs b/Source/Physics/Interactions/Face.cs
index 9e51638..380bc43 100644
--- a/Source/Physics/Interactions/Face.cs
+++ b/Source/Physics/Interactions/Face.cs
@@ -36,7 +36,7 @@ private void Update()
var item = GetItem();
if (!item || heldItem == item ||
- VRSession.Instance.LocalPlayer.PlayerController.timeSinceSwitchingSlots < 0.075f)
+ VRSession.Instance.LocalPlayer.PlayerController.timeSinceSwitchingSlots < 0.075f || !CanInteract)
return;
heldItem = item;
diff --git a/Source/Physics/Interactions/Ladder.cs b/Source/Physics/Interactions/Ladder.cs
deleted file mode 100644
index 6bcadcf..0000000
--- a/Source/Physics/Interactions/Ladder.cs
+++ /dev/null
@@ -1,315 +0,0 @@
-using System.Collections;
-using HarmonyLib;
-using LCVR.Assets;
-using LCVR.Managers;
-using LCVR.Patches;
-using LCVR.Player;
-using UnityEngine;
-using Object = UnityEngine.Object;
-
-namespace LCVR.Physics.Interactions;
-
-public class VRLadder : MonoBehaviour, VRInteractable
-{
- private const float MAX_CLIMB_SPEED = 3.0f;
- private const float CLIMB_STRENGTH = 1.0f;
-
- private InteractTrigger ladderTrigger;
-
- private Vector3? leftHandGripPoint;
- private Vector3? rightHandGripPoint;
-
- private VRInteractor leftHandInteractor;
- private VRInteractor rightHandInteractor;
-
- private bool isActiveLadder;
- private float climbStartTime;
-
- public InteractableFlags Flags => InteractableFlags.BothHands;
-
- private void Awake()
- {
- ladderTrigger = GetComponentInParent();
- }
-
- private void Update()
- {
- if (VRSession.Instance is not { } instance)
- return;
-
- var player = instance.LocalPlayer.PlayerController;
-
- if (!player.isClimbingLadder || !isActiveLadder)
- return;
-
- var totalMovement = Vector3.zero;
- var grippingHands = 0;
-
- if (leftHandGripPoint.HasValue)
- {
- var leftHand = VRSession.Instance.LocalPlayer.LeftHandVRTarget;
- var worldGripPoint = transform.TransformPoint(leftHandGripPoint.Value);
- var pullVector = worldGripPoint - leftHand.position;
- totalMovement += pullVector;
- grippingHands++;
- }
-
- if (rightHandGripPoint.HasValue)
- {
- var rightHand = VRSession.Instance.LocalPlayer.RightHandVRTarget;
- var worldGripPoint = transform.TransformPoint(rightHandGripPoint.Value);
- var pullVector = worldGripPoint - rightHand.position;
- totalMovement += pullVector;
- grippingHands++;
- }
-
- totalMovement *= CLIMB_STRENGTH / grippingHands;
- totalMovement.x = 0;
- totalMovement.z = 0;
-
- var maxMovementThisFrame = MAX_CLIMB_SPEED * Time.deltaTime;
- if (Mathf.Abs(totalMovement.y) > maxMovementThisFrame)
- totalMovement.y = Mathf.Sign(totalMovement.y) * maxMovementThisFrame;
-
- if (Mathf.Abs(totalMovement.y) > 0.001f)
- player.thisPlayerBody.position += totalMovement;
-
- if (Time.time - climbStartTime < 0.5f || ladderTrigger.topOfLadderPosition == null)
- return;
-
- var topY = ladderTrigger.topOfLadderPosition.position.y;
- var playerHeadY = player.gameplayCamera.transform.position.y;
-
- if (playerHeadY < topY - 0.3f)
- return;
-
- Vector3 exitPosition;
-
- if (ladderTrigger.useRaycastToGetTopPosition)
- {
- var rayStart = player.transform.position + Vector3.up * 0.5f;
- var rayEnd = ladderTrigger.topOfLadderPosition.position + Vector3.up * 0.5f;
-
- exitPosition = UnityEngine.Physics.Linecast(rayStart, rayEnd, out var hit,
- StartOfRound.Instance.collidersAndRoomMaskAndDefault,
- QueryTriggerInteraction.Ignore)
- ? hit.point
- : ladderTrigger.topOfLadderPosition.position;
- }
- else
- {
- exitPosition = ladderTrigger.topOfLadderPosition.position;
- }
-
- StartCoroutine(ExitLadder(player, exitPosition));
- }
-
- private IEnumerator ExitLadder(GameNetcodeStuff.PlayerControllerB player, Vector3 exitPosition)
- {
- leftHandGripPoint = null;
- rightHandGripPoint = null;
-
- if (leftHandInteractor != null)
- {
- leftHandInteractor.FingerCurler.ForceFist(false);
- leftHandInteractor.isHeld = false;
- leftHandInteractor = null;
- }
-
- if (rightHandInteractor != null)
- {
- rightHandInteractor.FingerCurler.ForceFist(false);
- rightHandInteractor.isHeld = false;
- rightHandInteractor = null;
- }
-
- isActiveLadder = false;
-
- player.isClimbingLadder = false;
- player.thisController.enabled = true;
- player.inSpecialInteractAnimation = false;
- player.UpdateSpecialAnimationValue(false);
-
- player.takingFallDamage = false;
- player.fallValue = 0f;
- player.fallValueUncapped = 0f;
-
- var waitTime = ladderTrigger.animationWaitTime * 0.5f;
- var timer = 0f;
- while (timer <= waitTime)
- {
- yield return null;
- timer += Time.deltaTime;
-
- player.thisPlayerBody.position = Vector3.Lerp(player.thisPlayerBody.position, exitPosition,
- Mathf.SmoothStep(0f, 1f, timer / waitTime));
- player.thisPlayerBody.rotation = Quaternion.Lerp(player.thisPlayerBody.rotation,
- ladderTrigger.ladderPlayerPositionNode.rotation, Mathf.SmoothStep(0f, 1f, timer / waitTime));
- }
-
- player.TeleportPlayer(exitPosition);
-
- ladderTrigger.usingLadder = false;
- ladderTrigger.isPlayingSpecialAnimation = false;
- ladderTrigger.lockedPlayer = null;
- }
-
- public bool OnButtonPress(VRInteractor interactor)
- {
- var player = VRSession.Instance.LocalPlayer.PlayerController;
-
- // Store grip point in ladder's local space
- if (interactor.IsRightHand)
- {
- rightHandGripPoint =
- transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.RightHandVRTarget.position);
- rightHandInteractor = interactor;
- }
- else
- {
- leftHandGripPoint =
- transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.LeftHandVRTarget.position);
- leftHandInteractor = interactor;
- }
-
- if (!player.isClimbingLadder)
- {
- if (ladderTrigger != null && ladderTrigger.interactable)
- {
- isActiveLadder = true;
- climbStartTime = Time.time;
- player.isClimbingLadder = true;
- player.thisController.enabled = false;
-
- player.takingFallDamage = false;
- player.fallValue = 0f;
- player.fallValueUncapped = 0f;
- }
- else
- {
- return false;
- }
- }
- else if (player.isClimbingLadder && !isActiveLadder)
- {
- return false;
- }
-
- interactor.FingerCurler.ForceFist(true);
- return true;
- }
-
- public void OnButtonRelease(VRInteractor interactor)
- {
- if (interactor.IsRightHand)
- {
- rightHandGripPoint = null;
- rightHandInteractor = null;
- }
- else
- {
- leftHandGripPoint = null;
- leftHandInteractor = null;
- }
-
- interactor.FingerCurler.ForceFist(false);
-
- if (leftHandGripPoint.HasValue || rightHandGripPoint.HasValue || !isActiveLadder)
- return;
-
- var player = VRSession.Instance.LocalPlayer.PlayerController;
-
- isActiveLadder = false;
- player.isClimbingLadder = false;
- player.thisController.enabled = true;
- player.inSpecialInteractAnimation = false;
- player.UpdateSpecialAnimationValue(false, 0, 0f, false);
-
- player.takingFallDamage = false;
- player.fallValue = 0f;
- player.fallValueUncapped = 0f;
- }
-
- public void OnColliderEnter(VRInteractor interactor) { }
- public void OnColliderExit(VRInteractor interactor) { }
-}
-
-// Lightweight wrapper that forwards to the shared ladder component
-internal class VRLadderInteractable : MonoBehaviour, VRInteractable
-{
- public VRLadder ladder;
-
- public InteractableFlags Flags => InteractableFlags.BothHands;
-
- public bool OnButtonPress(VRInteractor interactor) => ladder.OnButtonPress(interactor);
- public void OnButtonRelease(VRInteractor interactor) => ladder.OnButtonRelease(interactor);
- public void OnColliderEnter(VRInteractor interactor) { }
- public void OnColliderExit(VRInteractor interactor) { }
-}
-
-[LCVRPatch]
-[HarmonyPatch]
-internal static class LadderPatches
-{
- [HarmonyPatch(typeof(InteractTrigger), nameof(InteractTrigger.Start))]
- [HarmonyPostfix]
- private static void OnLadderStart(InteractTrigger __instance)
- {
- if (!__instance.isLadder)
- return;
-
- if (Plugin.Config.DisableLadderClimbingInteraction.Value)
- return;
-
- var ladderComponent = __instance.gameObject.AddComponent();
-
- // Create two separate colliders offset to left and right
- // This allows both hands to interact simultaneously
- var leftHandCollider = Object.Instantiate(AssetManager.Interactable, __instance.transform);
- var rightHandCollider = Object.Instantiate(AssetManager.Interactable, __instance.transform);
-
- if (__instance.topOfLadderPosition != null && __instance.bottomOfLadderPosition != null)
- {
- var topPos = __instance.topOfLadderPosition.localPosition;
- var bottomPos = __instance.bottomOfLadderPosition.localPosition;
- var midPoint = (topPos + bottomPos) / 2f;
- var height = Mathf.Abs(topPos.y - bottomPos.y);
-
- // Offset left collider to the left side
- leftHandCollider.transform.localPosition = midPoint + new Vector3(-0.3f, 0f, 0.3f);
- leftHandCollider.transform.localScale = new Vector3(0.8f, height, 0.8f);
-
- // Offset right collider to the right side
- rightHandCollider.transform.localPosition = midPoint + new Vector3(0.3f, 0f, 0.3f);
- rightHandCollider.transform.localScale = new Vector3(0.8f, height, 0.8f);
- }
- else
- {
- leftHandCollider.transform.localPosition = new Vector3(-0.3f, 0f, 0.3f);
- leftHandCollider.transform.localScale = new Vector3(0.8f, 3f, 0.8f);
-
- rightHandCollider.transform.localPosition = new Vector3(0.3f, 0f, 0.3f);
- rightHandCollider.transform.localScale = new Vector3(0.8f, 3f, 0.8f);
- }
-
- // Both colliders reference the same ladder component
- leftHandCollider.AddComponent().ladder = ladderComponent;
- rightHandCollider.AddComponent().ladder = ladderComponent;
-
- foreach (var collider in __instance.GetComponents())
- collider.enabled = false;
- }
-
- [HarmonyPatch(typeof(InteractTrigger), nameof(InteractTrigger.Interact))]
- [HarmonyPrefix]
- private static bool PreventLadderInteract(InteractTrigger __instance)
- {
- if (!__instance.isLadder)
- return true;
-
- if (Plugin.Config.DisableLadderClimbingInteraction.Value)
- return true;
-
- return false;
- }
-}
\ No newline at end of file
diff --git a/Source/Physics/Interactions/ShipLever.cs b/Source/Physics/Interactions/ShipLever.cs
index 2fd304e..98c11b7 100644
--- a/Source/Physics/Interactions/ShipLever.cs
+++ b/Source/Physics/Interactions/ShipLever.cs
@@ -3,12 +3,13 @@
using LCVR.Player;
using System.Collections;
using System.IO;
+using LCVR.Input;
using LCVR.Managers;
using UnityEngine;
namespace LCVR.Physics.Interactions;
-internal class ShipLeverInteractable : MonoBehaviour, VRInteractable
+public class ShipLeverInteractable : MonoBehaviour, VRInteractable
{
private ShipLever lever;
private VRInteractor currentInteractor;
@@ -33,9 +34,10 @@ public bool OnButtonPress(VRInteractor interactor)
currentInteractor = interactor;
+ interactor.SnapTo(transform, rotationOffset: new Vector3(0, 0, -90));
interactor.FingerCurler.ForceFist(true);
- lever.StartInteracting(interactor.transform, ShipLever.Actor.Self);
+ lever.StartInteracting(interactor.TrackedController, ShipLever.Actor.Self);
return true;
}
@@ -44,17 +46,34 @@ public void OnButtonRelease(VRInteractor interactor)
{
currentInteractor = null;
+ interactor.SnapTo(null);
interactor.FingerCurler.ForceFist(false);
lever.StopInteracting();
}
- public void OnColliderEnter(VRInteractor _) { }
+ public void OnColliderEnter(VRInteractor interactor)
+ {
+ if (lever.InOrbit || !lever.CanInteract)
+ return;
+
+ var velocity = interactor.IsRightHand
+ ? Actions.Instance.RightHandVelocity.ReadValue()
+ : Actions.Instance.LeftHandVelocity.ReadValue();
+
+ if (velocity.sqrMagnitude < 1f)
+ return;
+
+ lever.ShoveLever();
+ }
+
public void OnColliderExit(VRInteractor _) { }
}
public class ShipLever : MonoBehaviour
{
+ private static readonly int shoveLever = Animator.StringToHash("shoveLever");
+
private Animator animator;
private StartMatchLever lever;
private Transform rotateTo;
@@ -62,7 +81,8 @@ public class ShipLever : MonoBehaviour
private Actor currentActor;
private Channel channel;
- public bool CanInteract => lever.triggerScript.interactable && currentActor != Actor.Other;
+ public bool CanInteract => currentActor != Actor.Other && lever.triggerScript.interactable;
+ public bool InOrbit => lever.playersManager.inShipPhase;
private void Awake()
{
@@ -102,6 +122,18 @@ private void Update()
transform.eulerAngles = eulerAngles;
}
+ public void ShoveLever(bool isOwner = true)
+ {
+ // Can only be shoved if nobody (including ourselves) is interacting with the lever
+ if (currentActor != Actor.None)
+ return;
+
+ StartCoroutine(PerformLeverAction(isOwner, true));
+
+ if (isOwner)
+ channel.SendPacket([2]);
+ }
+
public void StartInteracting(Transform target, Actor actor)
{
currentActor = actor;
@@ -109,7 +141,7 @@ public void StartInteracting(Transform target, Actor actor)
rotateTo = target;
if (actor == Actor.Self)
- channel.SendPacket([1]);
+ channel.SendPacket([0]);
}
public void StopInteracting()
@@ -127,7 +159,7 @@ public void StopInteracting()
finally
{
if (currentActor == Actor.Self)
- channel.SendPacket([0]);
+ channel.SendPacket([1]);
// Always reset at the end
rotateTo = null;
@@ -136,31 +168,42 @@ public void StopInteracting()
}
}
- private IEnumerator PerformLeverAction(bool isLocal)
+ private IEnumerator PerformLeverAction(bool isLocal, bool isShove = false)
{
- if (isLocal) lever.LeverAnimation();
+ if (isShove)
+ lever.leverAnimatorObject.SetBool(shoveLever, true);
+
+ if (isLocal)
+ lever.LeverAnimation();
yield return new WaitForSeconds(1.67f);
animator.enabled = true;
- if (isLocal) lever.PullLever();
+
+ if (isLocal)
+ lever.PullLever();
+
+ lever.leverAnimatorObject.SetBool(shoveLever, false);
}
private void OnOtherInteractWithLever(ushort other, BinaryReader reader)
{
- var interacting = reader.ReadBoolean();
+ var interaction = reader.ReadByte();
if (!NetworkSystem.Instance.TryGetPlayer(other, out var player))
return;
- switch (interacting)
+ switch (interaction)
{
- case true when currentActor == Actor.None:
+ case 0 when currentActor == Actor.None:
StartInteracting(player.Bones.RightHand, Actor.Other);
break;
- case false when currentActor == Actor.Other:
+ case 1 when currentActor == Actor.Other:
StopInteracting();
break;
+ case 2:
+ ShoveLever(false);
+ break;
}
}
@@ -168,6 +211,8 @@ public static void Create()
{
var startMatch = FindObjectOfType();
startMatch.leverAnimatorObject.gameObject.AddComponent();
+ startMatch.leverAnimatorObject.runtimeAnimatorController = AssetManager.IntroLeverAnimator;
+ startMatch.leverAnimatorObject.GetComponent().audioClip3 = AssetManager.LeverShove;
if (!VRSession.InVR)
return;
diff --git a/Source/Player/VRController.cs b/Source/Player/VRController.cs
index 19b597f..c52ab65 100644
--- a/Source/Player/VRController.cs
+++ b/Source/Player/VRController.cs
@@ -1,12 +1,14 @@
-using GameNetcodeStuff;
-using System;
+using System;
+using GameNetcodeStuff;
using UnityEngine.InputSystem;
using UnityEngine;
using UnityEngine.XR;
using LCVR.Input;
using LCVR.Assets;
using System.Collections.Generic;
+using System.IO;
using LCVR.Managers;
+using LCVR.Networking;
namespace LCVR.Player;
@@ -26,6 +28,9 @@ public class VRController : MonoBehaviour
private LineRenderer debugLineRenderer;
+ private ShakeDetector jiggleDetector;
+ private Channel jiggleChannel;
+
private static string CursorTip
{
set
@@ -38,6 +43,7 @@ private static string CursorTip
public Transform InteractOrigin { get; private set; }
public bool IsHovering { get; private set; }
+
private void Awake()
{
var interactOriginObject = new GameObject("Raycast Origin");
@@ -66,13 +72,24 @@ private void Awake()
// Re-enable local player controller to make sure our "Interact" runs first
Actions.Instance["Interact"].performed += OnInteractPerformed;
+
+ jiggleDetector = new ShakeDetector(transform, 0.005f, true, 0.25f);
+ jiggleChannel = NetworkSystem.Instance.CreateChannel(ChannelType.ItemJiggle);
+ }
+
+ private void OnEnable()
+ {
+ jiggleDetector.onShake += OnJiggleDetected;
+ jiggleChannel.OnPacketReceived += OnJiggleReceived;
}
private void OnDisable()
{
IsHovering = false;
+ jiggleDetector.onShake -= OnJiggleDetected;
+ jiggleChannel.OnPacketReceived -= OnJiggleReceived;
- if (!PlayerController)
+ if (PlayerController == null)
return;
PlayerController.cursorIcon.enabled = false;
@@ -113,54 +130,47 @@ private void OnInteractPerformed(InputAction.CallbackContext context)
if (ShipBuildModeManager.Instance.InBuildMode)
return;
- try
- {
- // Ignore server player controller
- if (!PlayerController.IsOwner ||
- (PlayerController.IsServer && !PlayerController.isHostPlayerObject)) return;
+ // Ignore server player controller
+ if (!PlayerController.IsOwner ||
+ (PlayerController.IsServer && !PlayerController.isHostPlayerObject)) return;
- if (!context.performed) return;
- if (PlayerController.timeSinceSwitchingSlots < 0.2f) return;
+ if (!context.performed) return;
+ if (PlayerController.timeSinceSwitchingSlots < 0.2f) return;
- ShipBuildModeManager.Instance.CancelBuildMode();
+ ShipBuildModeManager.Instance.CancelBuildMode();
- if (PlayerController.isGrabbingObjectAnimation || PlayerController.isTypingChat ||
- PlayerController.inTerminalMenu || PlayerController.throwingObject || PlayerController.IsInspectingItem)
- return;
+ if (PlayerController.isGrabbingObjectAnimation || PlayerController.isTypingChat ||
+ PlayerController.inTerminalMenu || PlayerController.throwingObject || PlayerController.IsInspectingItem)
+ return;
- if (PlayerController.inAnimationWithEnemy != null)
- return;
+ if (PlayerController.inAnimationWithEnemy != null)
+ return;
- if (PlayerController.jetpackControls || PlayerController.disablingJetpackControls)
- return;
+ if (PlayerController.jetpackControls || PlayerController.disablingJetpackControls)
+ return;
- if (StartOfRound.Instance.suckingPlayersOutOfShip)
- return;
+ if (StartOfRound.Instance.suckingPlayersOutOfShip)
+ return;
- // Handle belt bag item
- if (PlayerController.currentlyHeldObjectServer is BeltBagItem bag && TryBagTool(bag))
- return;
+ if (!PlayerController.activatingItem && !PlayerController.waitingToDropItem && !PlayerController.isPlayerDead)
+ BeginGrabObject();
- // Here we try and pickup the item if it's possible
- if (!PlayerController.activatingItem && !VRSession.Instance.LocalPlayer.PlayerController.isPlayerDead)
- BeginGrabObject();
+ // Ignore hold triggers
+ if (PlayerController.hoveringOverTrigger == null || PlayerController.hoveringOverTrigger.holdInteraction)
+ return;
- // WHAT?
- if (PlayerController.hoveringOverTrigger == null || PlayerController.hoveringOverTrigger.holdInteraction ||
- (PlayerController.isHoldingObject && !PlayerController.hoveringOverTrigger.oneHandedItemAllowed) ||
- (PlayerController.twoHanded && (!PlayerController.hoveringOverTrigger.twoHandedItemAllowed ||
- PlayerController.hoveringOverTrigger.specialCharacterAnimation)))
- return;
+ if (PlayerController.isHoldingObject && !PlayerController.hoveringOverTrigger.oneHandedItemAllowed)
+ return;
- if (!PlayerController.InteractTriggerUseConditionsMet()) return;
+ // Prevent picking up when two-handed
+ if (PlayerController.twoHanded && (!PlayerController.hoveringOverTrigger.twoHandedItemAllowed ||
+ PlayerController.hoveringOverTrigger.specialCharacterAnimation))
+ return;
- PlayerController.hoveringOverTrigger.Interact(PlayerController.thisPlayerBody);
- }
- catch (Exception ex)
- {
- Debug.LogError(ex.Message);
- Debug.LogError(ex.StackTrace);
- }
+ if (!PlayerController.InteractTriggerUseConditionsMet())
+ return;
+
+ PlayerController.hoveringOverTrigger.Interact(PlayerController.thisPlayerBody);
}
private void ClickHoldInteraction()
@@ -211,6 +221,8 @@ private void ClickHoldInteraction()
private void Update()
{
+ jiggleDetector.Update();
+
if (PlayerController.IsOwner && PlayerController.isPlayerControlled)
ClickHoldInteraction();
}
@@ -250,6 +262,7 @@ private void LateUpdate()
if (component != PlayerController.previousHoveringOverTrigger &&
PlayerController.previousHoveringOverTrigger != null)
{
+ PlayerController.previousHoveringOverTrigger.StopInteraction();
PlayerController.previousHoveringOverTrigger.isBeingHeldByPlayer = false;
}
@@ -316,13 +329,28 @@ private void LateUpdate()
shouldReset = false;
IsHovering = true;
- if (PlayerController.FirstEmptyItemSlot() == -1)
+ GrabbableObject component = null;
+ int slot;
+
+ if (PlayerController.ItemOnlySlot == null)
+ {
+ component = hit.collider.GetComponent();
+ slot = PlayerController.FirstEmptyItemSlot(component);
+ }
+ else
+ slot = PlayerController.FirstEmptyItemSlot();
+
+ if (slot == -1)
{
CursorTip = "Inventory full!";
}
else
{
- var component = hit.collider.gameObject.GetComponent();
+ if (!component)
+ component = hit.collider.GetComponent();
+
+ if (component is null)
+ return;
if (!GameNetworkManager.Instance.gameHasStarted &&
!component.itemProperties.canBeGrabbedBeforeGameStart && StartOfRound.Instance.testRoom == null)
@@ -331,14 +359,10 @@ private void LateUpdate()
return;
}
- if (component is null)
- return;
-
- if (PlayerController.currentlyHeldObjectServer is { itemProperties.itemId: 20 } &&
- CanBeInsertedIntoBag(component))
- CursorTip = "Store in bag";
- else if (!string.IsNullOrEmpty(component.customGrabTooltip))
+ if (!string.IsNullOrEmpty(component.customGrabTooltip))
CursorTip = component.customGrabTooltip;
+ else if (slot == 0x32)
+ CursorTip = "Equip to belt";
else
CursorTip = "Grab";
}
@@ -375,21 +399,6 @@ private void BeginGrabObject()
GrabItem(hit.collider.transform.gameObject.GetComponent());
}
-
- private bool TryBagTool(BeltBagItem bag)
- {
- var ray = new Ray(InteractOrigin.position, InteractOrigin.forward);
- if (!ray.Raycast(out var hit, 4, 1073742144))
- return false;
-
- var item = hit.collider.GetComponent();
- if (item == null || item == PlayerController.currentlyHeldObjectServer || !CanBeInsertedIntoBag(item))
- return false;
-
- bag.TryAddObjectToBag(item);
-
- return true;
- }
public void GrabItem(GrabbableObject item)
{
@@ -411,7 +420,8 @@ public void GrabItem(GrabbableObject item)
return;
PlayerController.currentlyGrabbingObject.InteractItem();
- if (PlayerController.currentlyGrabbingObject.grabbable && PlayerController.FirstEmptyItemSlot() != -1)
+ if (PlayerController.currentlyGrabbingObject.grabbable &&
+ PlayerController.FirstEmptyItemSlot(PlayerController.currentlyGrabbingObject) != -1)
{
PlayerController.playerBodyAnimator.SetBool(grabInvalidated, false);
PlayerController.playerBodyAnimator.SetBool(grabValidated, false);
@@ -426,6 +436,7 @@ public void GrabItem(GrabbableObject item)
Mathf.Clamp(
PlayerController.carryWeight +
(PlayerController.currentlyGrabbingObject.itemProperties.weight - 1f), 1f, 10f);
+ StartOfRound.Instance.SendChangedWeightEvent();
PlayerController.grabObjectAnimationTime =
PlayerController.currentlyGrabbingObject.itemProperties.grabAnimationTime > 0f
? PlayerController.currentlyGrabbingObject.itemProperties.grabAnimationTime
@@ -441,9 +452,26 @@ public void GrabItem(GrabbableObject item)
}
}
- private static bool CanBeInsertedIntoBag(GrabbableObject item)
+ private void OnJiggleDetected()
+ {
+ if (PlayerController.currentlyHeldObjectServer is not {} item || item.isPocketed)
+ return;
+
+ var loudness = Actions.Instance.RightHandVelocity.ReadValue().magnitude;
+ item.JiggleItemEffect(loudness);
+
+ jiggleChannel.SendPacket(BitConverter.GetBytes(loudness));
+ }
+
+ private static void OnJiggleReceived(ushort playerId, BinaryReader reader)
{
- return !item.itemProperties.isScrap && !item.isHeld && !item.isHeldByEnemy &&
- item.itemProperties.itemId is not (123984 or 819501);
+ if (!NetworkSystem.Instance.TryGetPlayer(playerId, out var player))
+ return;
+
+ if (player.PlayerController.currentlyHeldObjectServer is not { } item || item.isPocketed)
+ return;
+
+ var loudness = reader.ReadSingle();
+ item.JiggleItemEffect(loudness);
}
}
\ No newline at end of file
diff --git a/Source/Player/VRPlayer.cs b/Source/Player/VRPlayer.cs
index a95e513..882938c 100644
--- a/Source/Player/VRPlayer.cs
+++ b/Source/Player/VRPlayer.cs
@@ -658,7 +658,7 @@ PlayerController.currentTriggerInAnimationWith is not null &&
lastFrameHmdPosition = head.localPosition;
// Set sprint
- if (Plugin.Config.ToggleSprint.Value)
+ if (IngamePlayerSettings.Instance.settings.toggleSprint)
{
if (PlayerController.isExhausted)
isSprinting = false;
diff --git a/Source/Plugin.cs b/Source/Plugin.cs
index 242c9ab..7b2a9a1 100644
--- a/Source/Plugin.cs
+++ b/Source/Plugin.cs
@@ -10,8 +10,6 @@
using System.Reflection;
using UnityEngine;
using UnityEngine.InputSystem;
-using UnityEngine.Rendering;
-using UnityEngine.Rendering.HighDefinition;
using UnityEngine.SceneManagement;
using UnityEngine.XR.Interaction.Toolkit.Inputs.Composites;
using UnityEngine.XR.Interaction.Toolkit.Inputs.Interactions;
@@ -27,7 +25,7 @@ public class Plugin : BaseUnityPlugin
{
public const string PLUGIN_GUID = "io.daxcess.lcvr";
public const string PLUGIN_NAME = "LCVR";
- public const string PLUGIN_VERSION = "1.4.6";
+ public const string PLUGIN_VERSION = "1.5.0";
#if DEBUG
private const string SKIP_CHECKSUM_VAR = $"--lcvr-skip-checksum={PLUGIN_VERSION}-dev";
@@ -39,7 +37,7 @@ public class Plugin : BaseUnityPlugin
private readonly string[] GAME_ASSEMBLY_HASHES =
[
- "8A8B86FF5BB655BB8B81CE05586D24F6D530E6632C272D9FB59D2243F42E088E", // V73
+ "774EFCF54CF475373938643AC32D5529755F12C86F3FA29735E2F7D00C2309F7", // V81
];
public new static Config Config { get; private set; }
@@ -51,10 +49,6 @@ private void Awake()
// Why isn't this the default in LC??
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
- // Reload Unity's Input System plugins since BepInEx in some
- // configurations runs after the Input System has already been initialized
- InputSystem.PerformDefaultPluginInitialization();
-
// Register XR Toolkit (these normally load during RuntimeInitializeOnLoad -> BeforeSceneLoad)
ButtonFallbackComposite.Initialize();
IntegerFallbackComposite.Initialize();
@@ -172,7 +166,7 @@ private void Awake()
Native.BringGameWindowToFront();
}
- private static string GetCommitHash()
+ internal static string GetCommitHash()
{
try
{
@@ -257,10 +251,6 @@ private bool PreloadRuntimeDependencies()
{
var filename = Path.GetFileName(file);
- // Ignore known unmanaged libraries
- if (filename is "UnityOpenXR.dll" or "openxr_loader.dll")
- continue;
-
Logger.LogDebug($"Preloading '{filename}'...");
try
@@ -307,25 +297,8 @@ private static bool InitializeVR()
LCVR.Logger.LogDebug("Inserted VR patches using Harmony");
- // Change HDRP settings
- var asset = QualitySettings.renderPipeline as HDRenderPipelineAsset;
- var settings = asset!.currentPlatformRenderPipelineSettings;
-
- settings.dynamicResolutionSettings.enabled = Config.EnableDynamicResolution.Value;
- settings.dynamicResolutionSettings.dynResType = DynamicResolutionType.Hardware;
- settings.dynamicResolutionSettings.upsampleFilter = Config.DynamicResolutionUpscaleFilter.Value;
- settings.dynamicResolutionSettings.minPercentage = settings.dynamicResolutionSettings.maxPercentage =
- Config.DynamicResolutionPercentage.Value;
- settings.supportMotionVectors = true;
-
- settings.xrSettings.occlusionMesh = Config.EnableOcclusionMesh.Value;
- settings.xrSettings.singlePass = false;
-
- settings.lodBias = new FloatScalableSetting([Config.LODBias.Value, Config.LODBias.Value, Config.LODBias.Value],
- ScalableSettingSchemaId.With3Levels);
-
- asset.currentPlatformRenderPipelineSettings = settings;
-
+ // Apply settings and register custom shaders
+ Config.ApplySettings();
VolumeManager.RegisterCustomPostProcessShaders();
// Input settings
diff --git a/Source/Rendering/VolumeManager.cs b/Source/Rendering/VolumeManager.cs
index 20dd338..d4acc54 100644
--- a/Source/Rendering/VolumeManager.cs
+++ b/Source/Rendering/VolumeManager.cs
@@ -11,7 +11,7 @@ public class VolumeManager : MonoBehaviour
private Coroutine takeDamageCoroutine;
private Volume volume;
- private Vignette vignette;
+ private UnityEngine.Rendering.HighDefinition.Vignette vignette;
private ColorAdjustments colorAdjustments;
private Color vignetteColor = Color.black;
diff --git a/Source/UI/Environment/MainMenuEnvironment.cs b/Source/UI/Environment/MainMenuEnvironment.cs
index 17329d9..70246db 100644
--- a/Source/UI/Environment/MainMenuEnvironment.cs
+++ b/Source/UI/Environment/MainMenuEnvironment.cs
@@ -32,7 +32,7 @@ public class MainMenuEnvironment : BaseMenuEnvironment
versionLabel.text = $"v{Plugin.PLUGIN_VERSION}";
#if DEBUG
- versionLabel.text += " (DEVELOPMENT)";
+ versionLabel.text += $" (DEVELOPMENT - {Plugin.GetCommitHash()})";
#endif
settingsManager = settingsPanel.GetComponent();
diff --git a/Source/UI/Settings/SettingsManager.cs b/Source/UI/Settings/SettingsManager.cs
index 9a94bba..eb9807b 100644
--- a/Source/UI/Settings/SettingsManager.cs
+++ b/Source/UI/Settings/SettingsManager.cs
@@ -5,9 +5,6 @@
using LCVR.Managers;
using TMPro;
using UnityEngine;
-using UnityEngine.InputSystem;
-using UnityEngine.Rendering;
-using UnityEngine.Rendering.HighDefinition;
using UnityEngine.UI;
namespace LCVR.UI.Settings;
@@ -242,37 +239,6 @@ public void ConfirmSettings()
if (!VRSession.InVR)
return;
- #region Reload and apply HDRP pipeline and input settings
-
- var asset = QualitySettings.renderPipeline as HDRenderPipelineAsset;
-
- if (!asset)
- {
- Logger.LogError("Failed to apply render pipeline changes: Render pipeline is null??");
- return;
- }
-
- var settings = asset.currentPlatformRenderPipelineSettings;
-
- settings.dynamicResolutionSettings.enabled = Plugin.Config.EnableDynamicResolution.Value;
- settings.dynamicResolutionSettings.dynResType = DynamicResolutionType.Hardware;
- settings.dynamicResolutionSettings.upsampleFilter = Plugin.Config.DynamicResolutionUpscaleFilter.Value;
- settings.dynamicResolutionSettings.minPercentage = settings.dynamicResolutionSettings.maxPercentage =
- Plugin.Config.DynamicResolutionPercentage.Value;
- settings.supportMotionVectors = true;
-
- settings.xrSettings.occlusionMesh = Plugin.Config.EnableOcclusionMesh.Value;
- settings.xrSettings.singlePass = false;
-
- settings.lodBias =
- new FloatScalableSetting(
- [Plugin.Config.LODBias.Value, Plugin.Config.LODBias.Value, Plugin.Config.LODBias.Value],
- ScalableSettingSchemaId.With3Levels);
-
- asset.currentPlatformRenderPipelineSettings = settings;
-
- InputSystem.settings.defaultButtonPressPoint = Plugin.Config.ButtonPressPoint.Value;
-
- #endregion
+ Config.ApplySettings();
}
}
\ No newline at end of file
diff --git a/Source/UI/Spectating/SpectatingMenu.cs b/Source/UI/Spectating/SpectatingMenu.cs
index b99b3e3..0954039 100644
--- a/Source/UI/Spectating/SpectatingMenu.cs
+++ b/Source/UI/Spectating/SpectatingMenu.cs
@@ -250,6 +250,6 @@ private void UpdateButtons()
lightToggleButtonText.text = spectateManager.LightEnabled ? "Disable Lights" : "Enable Lights";
fogToggleButtonText.text = spectateManager.FogDisabled ? "Enable Fog" : "Disable Fog";
- fogToggleButton.interactable = !Plugin.Config.DisableVolumetrics.Value;
+ fogToggleButton.interactable = Plugin.Config.FogQuality.Value != Config.FogQualityOption.Disabled;
}
}
\ No newline at end of file
diff --git a/Source/UI/VRHUD.cs b/Source/UI/VRHUD.cs
index beee70b..0ef7a96 100644
--- a/Source/UI/VRHUD.cs
+++ b/Source/UI/VRHUD.cs
@@ -28,6 +28,7 @@ public class VRHUD : MonoBehaviour
private GameObject clock;
private GameObject battery;
private GameObject inventory;
+ private GameObject utilitySlot;
///
/// The "Face" canvas is a canvas that simulates a screen-space canvas by always being stuck in front of the camera,
@@ -275,14 +276,22 @@ private void Awake()
batteryMeter.localRotation = Quaternion.identity;
batteryMeter.localScale = Vector3.one;
- // Inventory: Attach to right hand (below knuckles)
+ // Inventory & Utility Slot: Attach to right hand (below knuckles)
inventory = GameObject.Find("Inventory");
+ utilitySlot = GameObject.Find("Systems/UI/Canvas/IngamePlayerHUD/TopLeftCorner/UtilitySlot");
+
+ utilitySlot.transform.SetParent(inventory.transform);
+ utilitySlot.Find("Slot4Items/ItemOnlySlotTipText").SetActive(false);
if (isHandUiDisabled)
{
inventory.transform.SetParent(FaceCanvas.transform, false);
inventory.transform.localPosition = new Vector3(91 + xOffset, -185 + yOffset, 0);
inventory.transform.localRotation = Quaternion.identity;
+
+ // TODO: These must change
+ utilitySlot.transform.localPosition = new Vector3(91 + xOffset, -185 + yOffset, 0);
+ utilitySlot.transform.localRotation = Quaternion.identity;
}
else
{
@@ -290,6 +299,10 @@ private void Awake()
inventory.transform.localPosition = new Vector3(-28, 120, 40);
inventory.transform.localRotation = Quaternion.Euler(0, 195, 0);
inventory.transform.localScale = Vector3.one * 0.8f;
+
+ utilitySlot.transform.localPosition = new Vector3(-59, 0, 15);
+ utilitySlot.transform.localRotation = Quaternion.Euler(0, 0, 0);
+ utilitySlot.transform.localScale = Vector3.one * 1.3f;
}
// Compass: Attach to right hand (below inventory)
@@ -360,6 +373,19 @@ private void Awake()
cinematicGraphics.transform.localRotation = Quaternion.Euler(0, -9.3337f, 0);
cinematicGraphics.transform.localScale = Vector3.one;
+ // Status effects
+ var statusEffects = GameObject.Find("StatusEffects").transform;
+ var statusEffectsText = statusEffects.Find("StatusEffectText").GetComponent();
+
+ statusEffects.SetParent(FaceCanvas.transform, false);
+ statusEffects.localPosition = new Vector3(0, -300, 0);
+ statusEffects.localRotation = Quaternion.identity;
+ statusEffects.localScale = Vector3.one;
+
+ statusEffectsText.transform.localPosition = Vector3.zero;
+ statusEffectsText.transform.localRotation = Quaternion.identity;
+ statusEffectsText.alignment = TextAlignmentOptions.Center;
+
// Dialogue Box: In front of eyes
var dialogueBox = GameObject.Find("DialogueBox").transform;
@@ -388,16 +414,26 @@ private void Awake()
meteorShowerContainer.localPosition = Vector3.down * 100;
// Loading Screen: In front of eyes
- var loadingScreen = GameObject.Find("LoadingText");
+ var loadingScreen = GameObject.Find("LoadingText").transform;
loadingScreen.transform.SetParent(FaceCanvas.transform, false);
- loadingScreen.transform.localPosition = Vector3.zero;
+ loadingScreen.transform.localPosition = new Vector3(30, 120);
loadingScreen.transform.localRotation = Quaternion.identity;
loadingScreen.transform.localScale = Vector3.one;
- var darkenScreen = GameObject.Find("DarkenScreen");
+ var darkenScreen = loadingScreen.Find("DarkenScreen");
+ darkenScreen.transform.localScale = Vector3.one * 28;
+
+ var loadingScreenTextBg = loadingScreen.Find("TextBG").GetComponent();
+ loadingScreenTextBg.anchoredPosition = new Vector2(-25, -120);
+ loadingScreenTextBg.sizeDelta = new Vector2(370, 101.72f);
+
+ var loadingScreenText = loadingScreen.Find("LoadText").GetComponent();
+ loadingScreenText.text = loadingScreenText.text[..^3];
+ loadingScreenText.alignment = TextAlignmentOptions.Center;
- darkenScreen.transform.localScale = Vector3.one * 18;
+ var loadingScreenSeedText = loadingScreen.Find("LoadTextB").GetComponent();
+ loadingScreenSeedText.alignment = TextAlignmentOptions.Center;
// Fired screen: In front of eyes
var firedScreen = GameObject.Find("GameOverScreen");