From 452b49d88be70485d4fbceb5fe2343e7ace35268 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Tue, 10 Feb 2026 11:36:42 +0100 Subject: [PATCH 01/31] Remove TypeLoadExceptionFixer dependency - Remove references to hot switching --- CHANGELOG.md | 5 +++ Docs/Thunderstore/README.md | 7 ++-- LCVR.csproj | 4 +-- Preloader/LCVR.Preload.csproj | 4 +-- Preloader/Preload.cs | 60 ++++++++++++++++++++++++++++---- README.md | 3 +- Source/Patches/HarmonyPatcher.cs | 27 -------------- Source/Plugin.cs | 2 +- 8 files changed, 68 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9b815a3..b8224017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 1.4.7 + +**Changes**: +- Removed `TypeLoadExceptionFixer` dependency, fixing some issues with UnityExplorer + # 1.4.6 **Additions**: diff --git a/Docs/Thunderstore/README.md b/Docs/Thunderstore/README.md index ffa952fb..65f23e70 100644 --- a/Docs/Thunderstore/README.md +++ b/Docs/Thunderstore/README.md @@ -65,7 +65,8 @@ 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.4.7 *(LATEST)* | V73 | +| v1.4.6 | V73 | | v1.4.5 | V73 | | v1.4.4 | V73 | | v1.4.3 | V73 | @@ -110,10 +111,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. diff --git a/LCVR.csproj b/LCVR.csproj index be075fea..66d503cc 100644 --- a/LCVR.csproj +++ b/LCVR.csproj @@ -4,12 +4,12 @@ netstandard2.1 LCVR Collecting Scrap in VR - 1.4.6 + 1.4.7 DaXcess true 12.0 LethalCompanyVR - Copyright (c) DaXcess 2024-2025 + Copyright (c) DaXcess 2024-2026 https://lcvr.daxcess.io https://github.com/DaXcess/LCVR README.md diff --git a/Preloader/LCVR.Preload.csproj b/Preloader/LCVR.Preload.csproj index c1298dd3..a1e0bbb9 100644 --- a/Preloader/LCVR.Preload.csproj +++ b/Preloader/LCVR.Preload.csproj @@ -3,11 +3,11 @@ LCVR Preloader DaXcess - 1.4.6 + 1.4.7 true 12.0 LCVR.Preload - Copyright (c) DaXcess 2024-2025 + Copyright (c) DaXcess 2024-2026 https://lcvr.daxcess.io https://github.com/DaXcess/LCVR enable diff --git a/Preloader/Preload.cs b/Preloader/Preload.cs index 6358d826..a8d2c91b 100644 --- a/Preloader/Preload.cs +++ b/Preloader/Preload.cs @@ -1,7 +1,9 @@ using System.Reflection; using BepInEx; using BepInEx.Logging; +using HarmonyLib; using Mono.Cecil; +using MonoMod.RuntimeDetour; namespace LCVR.Preload; @@ -26,18 +28,19 @@ public static class Preload ] } """; - + private static readonly ManualLogSource Logger = BepInEx.Logging.Logger.CreateLogSource("LCVR.Preload"); - + public static void Initialize() { Logger.LogInfo("Setting up VR runtime assets"); - + SetupRuntimeAssets(); - + PatchTypeMethods(); + Logger.LogInfo("We're done here. Goodbye!"); } - + /// /// Place required runtime libraries and configuration in the game files to allow VR to be started /// @@ -70,7 +73,7 @@ private static void SetupRuntimeAssets() if (!CopyResourceFile(oxrLoader, oxrLoaderTarget)) Logger.LogWarning("Could not find plugin openxr_loader.dll, VR might not work!"); } - + /// /// Helper function for SetupRuntimeAssets() to copy resource files and return false if the source does not exist /// @@ -93,6 +96,51 @@ private static bool CopyResourceFile(string sourceFile, string destinationFile) return true; } +#pragma warning disable CS8618 + // Keep in scope just to be sure the hook stays attached + private static Hook _getTypesHook; + private static Hook _isAssignableFromHook; +#pragma warning restore CS8618 + + /// + /// Hook Assembly.GetTypes() so it won't crash if it encounters references to missing assemblies + /// + private static void PatchTypeMethods() + { + // TODO: Remove if it's determined that this is not needed + // _getTypesHook = new Hook(typeof(Assembly).GetMethod("GetTypes", BindingFlags.Instance | BindingFlags.Public), + // typeof(Preload).GetMethod(nameof(GetTypesHook))); + + _isAssignableFromHook = + new Hook( + AccessTools.Method(AccessTools.TypeByName("System.RuntimeType"), "IsAssignableFrom", [typeof(Type)]), + AccessTools.Method(typeof(Preload), nameof(IsAssignableFromHook))); + } + + private static Type[] GetTypesHook(Func orig, Assembly self) + { + try + { + return orig(self).Where(t => t != null).ToArray(); + } + catch (ReflectionTypeLoadException e) + { + return e.Types.Where(t => t != null).ToArray(); + } + } + + private static bool IsAssignableFromHook(Func orig, Type self, Type c) + { + try + { + return orig(self, c); + } + catch (TypeLoadException) + { + return false; + } + } + public static void Patch(AssemblyDefinition assembly) { // No-op diff --git a/README.md b/README.md index 56cfe5ea..5a51668b 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ Here is a list of LCVR versions and which version(s) of Lethal Company it suppor | LCVR | Lethal Company | [Configuration version](Docs/Configuration/README.md) | |-------------------|-------------------|-------------------------------------------------------| -| v1.4.6 *(LATEST)* | V74 | 1 | +| v1.4.7 *(LATEST)* | V73 | 1 | +| v1.4.6 | V73 | 1 | | v1.4.5 | V73 | 1 | | v1.4.4 | V73 | 1 | | v1.4.3 | V73 | 1 | diff --git a/Source/Patches/HarmonyPatcher.cs b/Source/Patches/HarmonyPatcher.cs index 939a71a4..729e1d97 100644 --- a/Source/Patches/HarmonyPatcher.cs +++ b/Source/Patches/HarmonyPatcher.cs @@ -73,30 +73,3 @@ internal enum LCVRPatchTarget Universal, VROnly } - -[LCVRPatch(LCVRPatchTarget.Universal)] -[HarmonyPatch] -internal static class HarmonyLibPatches -{ - private static readonly MethodInfo[] ForceUnpatchList = - [ - AccessTools.PropertySetter(typeof(Camera), nameof(Camera.targetTexture)), - AccessTools.PropertySetter(typeof(Cursor), nameof(Cursor.visible)), - AccessTools.PropertySetter(typeof(Cursor), nameof(Cursor.lockState)) - ]; - - /// - /// Ironically, patching harmony like this fixes some issues with unpatching - /// - [HarmonyPatch(typeof(MethodBaseExtensions), nameof(MethodBaseExtensions.HasMethodBody))] - [HarmonyPrefix] - private static bool OnUnpatch(MethodBase member, ref bool __result) - { - if (!ForceUnpatchList.Contains(member)) - return true; - - __result = true; - - return false; - } -} diff --git a/Source/Plugin.cs b/Source/Plugin.cs index 242c9ab3..982d1538 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -27,7 +27,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.4.7"; #if DEBUG private const string SKIP_CHECKSUM_VAR = $"--lcvr-skip-checksum={PLUGIN_VERSION}-dev"; From af4fc709bdafb2b320a7b782cfb14628a2be27f7 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Tue, 10 Feb 2026 23:19:46 +0100 Subject: [PATCH 02/31] Finish hooks --- Preloader/Preload.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Preloader/Preload.cs b/Preloader/Preload.cs index a8d2c91b..72539b70 100644 --- a/Preloader/Preload.cs +++ b/Preloader/Preload.cs @@ -103,13 +103,12 @@ private static bool CopyResourceFile(string sourceFile, string destinationFile) #pragma warning restore CS8618 /// - /// Hook Assembly.GetTypes() so it won't crash if it encounters references to missing assemblies + /// Hook multiple methods that deal with types so they won't crash if it encounters references to missing assemblies /// private static void PatchTypeMethods() { - // TODO: Remove if it's determined that this is not needed - // _getTypesHook = new Hook(typeof(Assembly).GetMethod("GetTypes", BindingFlags.Instance | BindingFlags.Public), - // typeof(Preload).GetMethod(nameof(GetTypesHook))); + _getTypesHook = new Hook(AccessTools.Method(typeof(Assembly), nameof(Assembly.GetTypes)), + AccessTools.Method(typeof(Preload), nameof(GetTypesHook))); _isAssignableFromHook = new Hook( From 001d920df4cc4e63fde311aba4c7b789d8556920 Mon Sep 17 00:00:00 2001 From: MrBub <106004257+misterbubb@users.noreply.github.com> Date: Wed, 11 Feb 2026 04:20:05 -0500 Subject: [PATCH 03/31] Refactor interaction system (#311) * Refactor interaction system to support simultaneous multi-hand interactions Refactors the core InteractionManager to support multiple hands interacting with the same object simultaneously, eliminating the architectural limitation where 'two VRInteractors can't interact with the same interaction'. - Eliminates confusing left/right ladder split where hands would lose grip crossing zones - Removes the need for multi-collider workarounds for future interactions - Enables future interactions to leverage true multi-hand support natively * Remove redundant comment --------- Co-authored-by: DaXcess --- Source/Physics/Interactions/Ladder.cs | 137 +++++++++----------------- Source/Player/VRInteractor.cs | 102 +++++++++++-------- 2 files changed, 109 insertions(+), 130 deletions(-) diff --git a/Source/Physics/Interactions/Ladder.cs b/Source/Physics/Interactions/Ladder.cs index 6bcadcf6..23eda456 100644 --- a/Source/Physics/Interactions/Ladder.cs +++ b/Source/Physics/Interactions/Ladder.cs @@ -34,11 +34,9 @@ private void Awake() private void Update() { - if (VRSession.Instance is not { } instance) + if (VRSession.Instance is not { LocalPlayer.PlayerController: var player }) return; - var player = instance.LocalPlayer.PlayerController; - if (!player.isClimbingLadder || !isActiveLadder) return; @@ -68,8 +66,9 @@ private void Update() totalMovement.z = 0; var maxMovementThisFrame = MAX_CLIMB_SPEED * Time.deltaTime; - if (Mathf.Abs(totalMovement.y) > maxMovementThisFrame) - totalMovement.y = Mathf.Sign(totalMovement.y) * maxMovementThisFrame; + totalMovement.y = Mathf.Abs(totalMovement.y) > maxMovementThisFrame + ? Mathf.Sign(totalMovement.y) * maxMovementThisFrame + : totalMovement.y; if (Mathf.Abs(totalMovement.y) > 0.001f) player.thisPlayerBody.position += totalMovement; @@ -83,27 +82,25 @@ private void Update() 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; - } + var exitPosition = ladderTrigger.useRaycastToGetTopPosition + ? GetRaycastExitPosition(player) + : ladderTrigger.topOfLadderPosition.position; StartCoroutine(ExitLadder(player, exitPosition)); } + private Vector3 GetRaycastExitPosition(GameNetcodeStuff.PlayerControllerB player) + { + var rayStart = player.transform.position + Vector3.up * 0.5f; + var rayEnd = ladderTrigger.topOfLadderPosition.position + Vector3.up * 0.5f; + + return UnityEngine.Physics.Linecast(rayStart, rayEnd, out var hit, + StartOfRound.Instance.collidersAndRoomMaskAndDefault, + QueryTriggerInteraction.Ignore) + ? hit.point + : ladderTrigger.topOfLadderPosition.position; + } + private IEnumerator ExitLadder(GameNetcodeStuff.PlayerControllerB player, Vector3 exitPosition) { leftHandGripPoint = null; @@ -156,41 +153,36 @@ private IEnumerator ExitLadder(GameNetcodeStuff.PlayerControllerB player, Vector public bool OnButtonPress(VRInteractor interactor) { - var player = VRSession.Instance.LocalPlayer.PlayerController; + if (VRSession.Instance is not { LocalPlayer.PlayerController: var player }) + return false; // Store grip point in ladder's local space if (interactor.IsRightHand) { - rightHandGripPoint = - transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.RightHandVRTarget.position); + rightHandGripPoint = transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.RightHandVRTarget.position); rightHandInteractor = interactor; } else { - leftHandGripPoint = - transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.LeftHandVRTarget.position); + 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 - { + if (ladderTrigger == null || !ladderTrigger.interactable) return false; - } + + isActiveLadder = true; + climbStartTime = Time.time; + player.isClimbingLadder = true; + player.thisController.enabled = false; + + player.takingFallDamage = false; + player.fallValue = 0f; + player.fallValueUncapped = 0f; } - else if (player.isClimbingLadder && !isActiveLadder) + else if (!isActiveLadder) { return false; } @@ -217,7 +209,8 @@ public void OnButtonRelease(VRInteractor interactor) if (leftHandGripPoint.HasValue || rightHandGripPoint.HasValue || !isActiveLadder) return; - var player = VRSession.Instance.LocalPlayer.PlayerController; + if (VRSession.Instance is not { LocalPlayer.PlayerController: var player }) + return; isActiveLadder = false; player.isClimbingLadder = false; @@ -234,19 +227,6 @@ 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 @@ -255,18 +235,11 @@ internal static class LadderPatches [HarmonyPostfix] private static void OnLadderStart(InteractTrigger __instance) { - if (!__instance.isLadder) + if (!__instance.isLadder || Plugin.Config.DisableLadderClimbingInteraction.Value) 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); + // Create single collider - InteractionManager now supports multiple hands per interactable + var collider = Object.Instantiate(AssetManager.Interactable, __instance.transform); if (__instance.topOfLadderPosition != null && __instance.bottomOfLadderPosition != null) { @@ -275,41 +248,25 @@ private static void OnLadderStart(InteractTrigger __instance) 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); + collider.transform.localPosition = midPoint + new Vector3(0f, 0f, 0.3f); + collider.transform.localScale = new Vector3(1.2f, 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); + collider.transform.localPosition = new Vector3(0f, 0f, 0.3f); + collider.transform.localScale = new Vector3(1.2f, 3f, 0.8f); } - // Both colliders reference the same ladder component - leftHandCollider.AddComponent().ladder = ladderComponent; - rightHandCollider.AddComponent().ladder = ladderComponent; + collider.AddComponent(); - foreach (var collider in __instance.GetComponents()) - collider.enabled = false; + foreach (var existingCollider in __instance.GetComponents()) + existingCollider.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; + return !__instance.isLadder || Plugin.Config.DisableLadderClimbingInteraction.Value; } } \ No newline at end of file diff --git a/Source/Player/VRInteractor.cs b/Source/Player/VRInteractor.cs index 0f15ecba..be69c751 100644 --- a/Source/Player/VRInteractor.cs +++ b/Source/Player/VRInteractor.cs @@ -1,4 +1,4 @@ -using LCVR.Assets; +using LCVR.Assets; using LCVR.Input; using LCVR.Physics; using System.Collections.Generic; @@ -176,88 +176,110 @@ private readonly struct Offset(Vector3 position, Vector3 scale, Quaternion rotat } } +/// +/// Manages VR interactions between interactors (hands) and interactables (objects). +/// Supports multiple interactors per interactable, allowing both hands to interact with the same object simultaneously. +/// public class InteractionManager { - private readonly Dictionary interactableState = []; + private readonly Dictionary> interactableStates = []; public void ReportInteractables(VRInteractor interactor, VRInteractable[] interactables) { foreach (var interactable in interactables) { - if (!interactableState.ContainsKey(interactable)) - { - // Check if this hand is allowed to interact with this object - if (interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.RightHand)) - continue; + // Initialize state dictionary for this interactable if needed + if (!interactableStates.ContainsKey(interactable)) + interactableStates[interactable] = []; - if (!interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.LeftHand)) - continue; + var states = interactableStates[interactable]; - if (interactor.isHeld && interactable.Flags.HasFlag(InteractableFlags.NotWhileHeld)) - continue; + // Check if this hand is allowed to interact + if (interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.RightHand)) + continue; + + if (!interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.LeftHand)) + continue; - interactableState.Add(interactable, new InteractableState(interactor, false)); + if (interactor.isHeld && interactable.Flags.HasFlag(InteractableFlags.NotWhileHeld)) + continue; + + // Add this interactor if not already tracking + if (!states.ContainsKey(interactor)) + { + states[interactor] = new InteractableState(false); interactable.OnColliderEnter(interactor); } - // Ignore events from hand if other hand is already interacting - if (interactableState[interactable].interactor != interactor) - continue; + var state = states[interactor]; - if (!interactableState[interactable].isHeld && !interactor.isHeld && interactor.IsPressed()) + // Handle button press + if (!state.isHeld && !interactor.isHeld && interactor.IsPressed()) { var acknowledged = interactable.OnButtonPress(interactor); - interactableState[interactable].isHeld = interactor.isHeld = acknowledged; + state.isHeld = interactor.isHeld = acknowledged; } - else if (interactableState[interactable].isHeld && !interactor.IsPressed()) + // Handle button release + else if (state.isHeld && !interactor.IsPressed()) { interactable.OnButtonRelease(interactor); - interactableState[interactable].isHeld = interactor.isHeld = false; + state.isHeld = interactor.isHeld = false; } } - foreach (var interactable in interactableState.Keys) + // Clean up interactors that left the collision zone + var interactablesToRemove = new List(); + + foreach (var (interactable, states) in interactableStates) { - // Ignore if this state is being managed by another hand - if (interactableState[interactable].interactor != interactor) + if (!states.ContainsKey(interactor)) continue; if (interactables.Contains(interactable)) continue; - // Ignore if button is still being held down - if (interactableState[interactable].isHeld) + var state = states[interactor]; + + // Release if still held + if (state.isHeld) + { if (interactor.IsPressed()) continue; - else { - interactable.OnButtonRelease(interactor); - interactor.isHeld = false; - } + + interactable.OnButtonRelease(interactor); + interactor.isHeld = false; + } - interactableState.Remove(interactable); interactable.OnColliderExit(interactor); + states.Remove(interactor); - // Break to not make C# shit itself, if more need to be removed it'll happen next frame - break; + // Clean up empty state dictionaries + if (states.Count == 0) + interactablesToRemove.Add(interactable); } + + foreach (var interactable in interactablesToRemove) + interactableStates.Remove(interactable); } public void ResetState() { - foreach (var interactable in interactableState.Keys) + foreach (var (interactable, states) in interactableStates) { - var state = interactableState[interactable]; - - if (state.isHeld) - interactable.OnButtonRelease(state.interactor); - - interactable.OnColliderExit(state.interactor); + foreach (var (interactor, state) in states) + { + if (state.isHeld) + interactable.OnButtonRelease(interactor); + + interactable.OnColliderExit(interactor); + } } + + interactableStates.Clear(); } - private class InteractableState(VRInteractor interactor, bool isHeld) + private class InteractableState(bool isHeld) { - public VRInteractor interactor = interactor; public bool isHeld = isHeld; } } \ No newline at end of file From e1d63b3ab80ba96f4cf8d6643f8624a62bf5e3cc Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 30 Mar 2026 22:20:24 +0200 Subject: [PATCH 04/31] The V80 patch --- CHANGELOG.md | 10 ++- Docs/Thunderstore/README.md | 3 +- LCVR.csproj | 8 +-- Preloader/LCVR.Preload.csproj | 2 +- README.md | 3 +- Source/Config.cs | 3 - Source/Experiments.cs | 67 +------------------ Source/Patches/EntranceTeleportPatches.cs | 11 +-- Source/Patches/HarmonyPatcher.cs | 2 - Source/Patches/IngamePlayerSettingsPatches.cs | 12 ++++ Source/Patches/InputPatches.cs | 13 +++- Source/Patches/Items/SprayPaintItemPatches.cs | 3 +- Source/Patches/PlayerControllerPatches.cs | 33 ++++++++- Source/Patches/SettingsOptionPatches.cs | 17 +++++ Source/Patches/Spectating/AIPatches.cs | 54 +++++---------- Source/Patches/XRPatches.cs | 16 +---- Source/Player/VRPlayer.cs | 2 +- Source/Plugin.cs | 2 +- Source/UI/VRHUD.cs | 15 ++++- 19 files changed, 129 insertions(+), 147 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8224017..c5571166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ -# 1.4.7 +# 1.5.0 + +**V80's terrain rendering is not supported by LCVR! Game may crash upon loading a moon!** + +**Additions**: +- Added support for the new utility slot, which is accessible like a normal inventory slot **Changes**: +- 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 # 1.4.6 diff --git a/Docs/Thunderstore/README.md b/Docs/Thunderstore/README.md index 65f23e70..00a5d75e 100644 --- a/Docs/Thunderstore/README.md +++ b/Docs/Thunderstore/README.md @@ -65,7 +65,8 @@ Here is a list of LCVR versions and which version(s) of Lethal Company it suppor | LCVR | Lethal Company | |-------------------|-------------------| -| v1.4.7 *(LATEST)* | V73 | +| v1.5.0 *(LATEST)* | V80 | +| v1.4.7 | V73 | | v1.4.6 | V73 | | v1.4.5 | V73 | | v1.4.4 | V73 | diff --git a/LCVR.csproj b/LCVR.csproj index 66d503cc..06fea1e9 100644 --- a/LCVR.csproj +++ b/LCVR.csproj @@ -4,7 +4,7 @@ netstandard2.1 LCVR Collecting Scrap in VR - 1.4.7 + 1.5.0 DaXcess true 12.0 @@ -37,7 +37,7 @@ - + @@ -48,8 +48,8 @@ - - + + diff --git a/Preloader/LCVR.Preload.csproj b/Preloader/LCVR.Preload.csproj index a1e0bbb9..c5860db1 100644 --- a/Preloader/LCVR.Preload.csproj +++ b/Preloader/LCVR.Preload.csproj @@ -3,7 +3,7 @@ LCVR Preloader DaXcess - 1.4.7 + 1.5.0 true 12.0 LCVR.Preload diff --git a/README.md b/README.md index 5a51668b..00ecef33 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ Here is a list of LCVR versions and which version(s) of Lethal Company it suppor | LCVR | Lethal Company | [Configuration version](Docs/Configuration/README.md) | |-------------------|-------------------|-------------------------------------------------------| -| v1.4.7 *(LATEST)* | V73 | 1 | +| v1.5.0 *(LATEST)* | V80 | 1 | +| v1.4.7 | V73 | 1 | | v1.4.6 | V73 | 1 | | v1.4.5 | V73 | 1 | | v1.4.4 | V73 | 1 | diff --git a/Source/Config.cs b/Source/Config.cs index 45481f44..387bbdfb 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -82,9 +82,6 @@ public class Config(string assemblyPath, ConfigFile file) "The amount of rotation that is applied when performing a snap turn. Requires turn provider to be set to snap.", new AcceptableValueRange(10, 180))); - public ConfigEntry ToggleSprint { get; } = file.Bind("Input", "ToggleSprint", false, - "Whether the sprint button should toggle sprint instead of having to hold it down."); - public ConfigEntry MovementSprintToggleCooldown { get; } = file.Bind("Input", "MovementSprintToggleCooldown", 1f, new ConfigDescription( diff --git a/Source/Experiments.cs b/Source/Experiments.cs index a2d5d90e..8f696d18 100644 --- a/Source/Experiments.cs +++ b/Source/Experiments.cs @@ -118,71 +118,6 @@ private static IEnumerable CanEnableDebugMenu(IEnumerable Debug_KillLocalPlayer(IEnumerable instructions) - { - return PatchIsEditor(instructions); - } - - [HarmonyPatch(typeof(QuickMenuManager), nameof(QuickMenuManager.Debug_SpawnEnemy))] - [HarmonyTranspiler] - private static IEnumerable Debug_SpawnEnemy(IEnumerable instructions) - { - return PatchIsEditor(instructions); - } - - [HarmonyPatch(typeof(QuickMenuManager), nameof(QuickMenuManager.Debug_SpawnItem))] - [HarmonyTranspiler] - private static IEnumerable Debug_SpawnItem(IEnumerable instructions) - { - return PatchIsEditor(instructions); - } - - [HarmonyPatch(typeof(QuickMenuManager), nameof(QuickMenuManager.Debug_SpawnTruck))] - [HarmonyTranspiler] - private static IEnumerable Debug_SpawnTruck(IEnumerable instructions) - { - return PatchIsEditor(instructions); - } - - [HarmonyPatch(typeof(QuickMenuManager), nameof(QuickMenuManager.Debug_ToggleAllowDeath))] - [HarmonyTranspiler] - private static IEnumerable Debug_ToggleAllowDeath(IEnumerable instructions) - { - return PatchIsEditor(instructions); - } - - [HarmonyPatch(typeof(QuickMenuManager), nameof(QuickMenuManager.Debug_ToggleTestRoom))] - [HarmonyTranspiler] - private static IEnumerable Debug_ToggleTestRoom(IEnumerable instructions) - { - return PatchIsEditor(instructions); - } - - [HarmonyPatch(typeof(StartOfRound), nameof(StartOfRound.Debug_EnableTestRoomServerRpc))] - [HarmonyTranspiler] - private static IEnumerable Debug_EnableTestRoomServerRpc(IEnumerable instructions) - { - return PatchIsEditor(instructions); - } - - [HarmonyPatch(typeof(StartOfRound), nameof(StartOfRound.Debug_ReviveAllPlayersServerRpc))] - [HarmonyTranspiler] - private static IEnumerable Debug_ReviveAllPlayersServerRpc( - IEnumerable instructions) - { - return PatchIsEditor(instructions); - } - - [HarmonyPatch(typeof(StartOfRound), nameof(StartOfRound.Debug_ToggleAllowDeathServerRpc))] - [HarmonyTranspiler] - private static IEnumerable Debug_ToggleAllowDeathServerRpc( - IEnumerable instructions) - { - return PatchIsEditor(instructions); - } } #endif @@ -271,7 +206,7 @@ private static LineRenderer CreateRenderer() lineRenderer.widthCurve.keys = [new Keyframe(0, 1)]; lineRenderer.widthMultiplier = 0.005f; lineRenderer.positionCount = 2; - lineRenderer.SetPositions(new[] { Vector3.zero, Vector3.zero }); + lineRenderer.SetPositions([Vector3.zero, Vector3.zero]); lineRenderer.numCornerVertices = 4; lineRenderer.numCapVertices = 4; lineRenderer.alignment = LineAlignment.View; diff --git a/Source/Patches/EntranceTeleportPatches.cs b/Source/Patches/EntranceTeleportPatches.cs index 0d3bcff3..02b39510 100644 --- a/Source/Patches/EntranceTeleportPatches.cs +++ b/Source/Patches/EntranceTeleportPatches.cs @@ -1,7 +1,5 @@ -using System.Linq; using HarmonyLib; using LCVR.Managers; -using UnityEngine; namespace LCVR.Patches; @@ -16,13 +14,8 @@ internal static class EntranceTeleportPatches [HarmonyPostfix] private static void OnTeleportPlayer(EntranceTeleport __instance) { - var doorPosition = Object.FindObjectsOfType().First(e => - e.entranceId == __instance.entranceId && e.isEntranceToBuilding != __instance.isEntranceToBuilding) - .transform.position; - var direction = (__instance.exitPoint.position - doorPosition).normalized; - var angle = Vector3.SignedAngle(Vector3.forward, direction, Vector3.up) / 90; - var roundedAngle = (Mathf.Sign(angle) > 0 ? Mathf.Ceil(angle) : Mathf.Floor(angle)) * 90; - var rotation = roundedAngle - VRSession.Instance.MainCamera.transform.parent.localEulerAngles.y; + var entrancePoint = __instance.exitScript.entrancePoint; + var rotation = entrancePoint.eulerAngles.y - VRSession.Instance.MainCamera.transform.parent.localEulerAngles.y; VRSession.Instance.LocalPlayer.TurningProvider.SetOffset(rotation); } diff --git a/Source/Patches/HarmonyPatcher.cs b/Source/Patches/HarmonyPatcher.cs index 729e1d97..869df3b1 100644 --- a/Source/Patches/HarmonyPatcher.cs +++ b/Source/Patches/HarmonyPatcher.cs @@ -1,8 +1,6 @@ using HarmonyLib; using System; -using System.Linq; using System.Reflection; -using UnityEngine; namespace LCVR.Patches; diff --git a/Source/Patches/IngamePlayerSettingsPatches.cs b/Source/Patches/IngamePlayerSettingsPatches.cs index f6b74eed..ab23d209 100644 --- a/Source/Patches/IngamePlayerSettingsPatches.cs +++ b/Source/Patches/IngamePlayerSettingsPatches.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Reflection.Emit; using HarmonyLib; +using UnityEngine.Rendering.HighDefinition; namespace LCVR.Patches; @@ -22,4 +23,15 @@ private static IEnumerable IgnorePreInitScript(IEnumerable + /// 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 fed03e3a..a3ad3b4a 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/SprayPaintItemPatches.cs b/Source/Patches/Items/SprayPaintItemPatches.cs index 0a7c8d92..0c00a316 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 5514d1c2..36bde54b 100644 --- a/Source/Patches/PlayerControllerPatches.cs +++ b/Source/Patches/PlayerControllerPatches.cs @@ -84,9 +84,9 @@ private static IEnumerable SprintPatch(IEnumerable)GetSprintValue).Method) - .RemoveInstructions(6) + .RemoveInstructions(4) .InstructionEnumeration(); [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -424,6 +424,35 @@ 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; + } } [LCVRPatch(LCVRPatchTarget.Universal)] diff --git a/Source/Patches/SettingsOptionPatches.cs b/Source/Patches/SettingsOptionPatches.cs index a14abc45..389ef5fe 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 c1519345..5f6e30c2 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/XRPatches.cs b/Source/Patches/XRPatches.cs index 96b7161e..869a017c 100644 --- a/Source/Patches/XRPatches.cs +++ b/Source/Patches/XRPatches.cs @@ -4,7 +4,7 @@ using UnityEngine; using UnityEngine.Experimental.Rendering; using UnityEngine.InputSystem.XR; -using UnityEngine.Rendering.HighDefinition; + using static HarmonyLib.AccessTools; namespace LCVR.Patches; @@ -58,18 +58,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/Player/VRPlayer.cs b/Source/Player/VRPlayer.cs index a95e5136..882938c3 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 982d1538..626b0880 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -27,7 +27,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.7"; + public const string PLUGIN_VERSION = "1.5.0"; #if DEBUG private const string SKIP_CHECKSUM_VAR = $"--lcvr-skip-checksum={PLUGIN_VERSION}-dev"; diff --git a/Source/UI/VRHUD.cs b/Source/UI/VRHUD.cs index beee70b2..17c059ee 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) From 476598c05ac2cdd12807803f86d1795b0e6545e8 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Sun, 5 Apr 2026 14:25:12 +0200 Subject: [PATCH 05/31] The SPI patch --- CHANGELOG.md | 4 +- Preloader/Preload.cs | 77 +-------------------------- Source/OpenXR.cs | 16 ++++-- Source/Patches/XRPatches.cs | 25 +++++---- Source/Plugin.cs | 13 ++--- Source/Rendering/VolumeManager.cs | 2 +- Source/UI/Settings/SettingsManager.cs | 2 +- 7 files changed, 34 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5571166..82af76fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,17 @@ # 1.5.0 -**V80's terrain rendering is not supported by LCVR! Game may crash upon loading a moon!** +**The posterization shader is not yet compatible with VR! Left and right eye will not match!** **Additions**: - Added support for the new utility slot, which is accessible like a normal inventory slot **Changes**: +- Changed rendering to Single Pass Instanced, this may break some 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 # 1.4.6 diff --git a/Preloader/Preload.cs b/Preloader/Preload.cs index 72539b70..6454aaba 100644 --- a/Preloader/Preload.cs +++ b/Preloader/Preload.cs @@ -1,5 +1,4 @@ using System.Reflection; -using BepInEx; using BepInEx.Logging; using HarmonyLib; using Mono.Cecil; @@ -11,91 +10,17 @@ public static class Preload { public static IEnumerable TargetDLLs { get; } = []; - private const string VR_MANIFEST = """ - { - "name": "OpenXR XR Plugin", - "version": "1.8.2", - "libraryName": "UnityOpenXR", - "displays": [ - { - "id": "OpenXR Display" - } - ], - "inputs": [ - { - "id": "OpenXR Input" - } - ] - } - """; - private static readonly ManualLogSource Logger = BepInEx.Logging.Logger.CreateLogSource("LCVR.Preload"); public static void Initialize() { - Logger.LogInfo("Setting up VR runtime assets"); + Logger.LogInfo("Patching soft dependencies"); - SetupRuntimeAssets(); PatchTypeMethods(); Logger.LogInfo("We're done here. Goodbye!"); } - /// - /// Place required runtime libraries and configuration in the game files to allow VR to be started - /// - private static void SetupRuntimeAssets() - { - var root = Path.Combine(Paths.GameRootPath, "Lethal Company_Data"); - var subsystems = Path.Combine(root, "UnitySubsystems"); - if (!Directory.Exists(subsystems)) - Directory.CreateDirectory(subsystems); - - var openXr = Path.Combine(subsystems, "UnityOpenXR"); - if (!Directory.Exists(openXr)) - Directory.CreateDirectory(openXr); - - var manifest = Path.Combine(openXr, "UnitySubsystemsManifest.json"); - if (!File.Exists(manifest)) - File.WriteAllText(manifest, VR_MANIFEST); - - var plugins = Path.Combine(root, "Plugins"); - var oxrPluginTarget = Path.Combine(plugins, "UnityOpenXR.dll"); - var oxrLoaderTarget = Path.Combine(plugins, "openxr_loader.dll"); - - var current = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; - var oxrPlugin = Path.Combine(current, "RuntimeDeps/UnityOpenXR.dll"); - var oxrLoader = Path.Combine(current, "RuntimeDeps/openxr_loader.dll"); - - if (!CopyResourceFile(oxrPlugin, oxrPluginTarget)) - Logger.LogWarning("Could not find plugin UnityOpenXR.dll, VR might not work!"); - - if (!CopyResourceFile(oxrLoader, oxrLoaderTarget)) - Logger.LogWarning("Could not find plugin openxr_loader.dll, VR might not work!"); - } - - /// - /// Helper function for SetupRuntimeAssets() to copy resource files and return false if the source does not exist - /// - private static bool CopyResourceFile(string sourceFile, string destinationFile) - { - if (!File.Exists(sourceFile)) - return false; - - if (File.Exists(destinationFile)) - { - var sourceHash = Utils.ComputeHash(File.ReadAllBytes(sourceFile)); - var destHash = Utils.ComputeHash(File.ReadAllBytes(destinationFile)); - - if (sourceHash.SequenceEqual(destHash)) - return true; - } - - File.Copy(sourceFile, destinationFile, true); - - return true; - } - #pragma warning disable CS8618 // Keep in scope just to be sure the hook stays attached private static Hook _getTypesHook; diff --git a/Source/OpenXR.cs b/Source/OpenXR.cs index 93cc9db9..191d6797 100644 --- a/Source/OpenXR.cs +++ b/Source/OpenXR.cs @@ -14,6 +14,7 @@ using UnityEngine.XR; using UnityEngine.XR.Management; using UnityEngine.XR.OpenXR; +using UnityEngine.XR.OpenXR.Features; using UnityEngine.XR.OpenXR.Features.Interactions; namespace LCVR; @@ -435,6 +436,8 @@ private static bool InitializeXR(Runtime? runtime) return displays.Count > 0; } + private static OpenXRFeature[] s_Features = []; + private static void InitializeScripts() { xrGeneralSettings ??= ScriptableObject.CreateInstance(); @@ -446,10 +449,10 @@ private static void InitializeScripts() ((List)xrManagerSettings.activeLoaders).Clear(); ((List)xrManagerSettings.activeLoaders).Add(xrLoader); - OpenXRSettings.Instance.renderMode = OpenXRSettings.RenderMode.MultiPass; + OpenXRSettings.Instance.renderMode = OpenXRSettings.RenderMode.SinglePassInstanced; OpenXRSettings.Instance.depthSubmissionMode = OpenXRSettings.DepthSubmissionMode.None; - if (OpenXRSettings.Instance.features.Length != 0) + if (s_Features.Length > 0) return; var valveIndex = ScriptableObject.CreateInstance(); @@ -468,7 +471,7 @@ private static void InitializeScripts() metaQuestTouch.enabled = true; oculusTouch.enabled = true; - OpenXRSettings.Instance.features = + s_Features = [ valveIndex, hpReverb, @@ -478,6 +481,13 @@ private static void InitializeScripts() metaQuestTouch, oculusTouch ]; + OpenXRSettings.Instance.features = s_Features; + } + + // The vanilla game now tries to overwrite these features, so restore them at a certain suitable point during loading + internal static void UpdateFeatures() + { + OpenXRSettings.Instance.features = s_Features; } } } \ No newline at end of file diff --git a/Source/Patches/XRPatches.cs b/Source/Patches/XRPatches.cs index 869a017c..cca6361f 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.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 /// diff --git a/Source/Plugin.cs b/Source/Plugin.cs index 626b0880..475eb255 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -15,6 +15,7 @@ using UnityEngine.SceneManagement; using UnityEngine.XR.Interaction.Toolkit.Inputs.Composites; using UnityEngine.XR.Interaction.Toolkit.Inputs.Interactions; +using UnityEngine.XR.OpenXR; using DependencyFlags = BepInEx.BepInDependency.DependencyFlags; using VolumeManager = LCVR.Rendering.VolumeManager; @@ -39,7 +40,7 @@ public class Plugin : BaseUnityPlugin private readonly string[] GAME_ASSEMBLY_HASHES = [ - "8A8B86FF5BB655BB8B81CE05586D24F6D530E6632C272D9FB59D2243F42E088E", // V73 + "5CDBE2C86347F5183A6AEA2A20C34AB2B5D09EBC7D5FE959C043F7EB600E0823", // V81 ]; public new static Config Config { get; private set; } @@ -51,10 +52,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(); @@ -257,10 +254,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 @@ -319,7 +312,7 @@ private static bool InitializeVR() settings.supportMotionVectors = true; settings.xrSettings.occlusionMesh = Config.EnableOcclusionMesh.Value; - settings.xrSettings.singlePass = false; + settings.xrSettings.singlePass = true; settings.lodBias = new FloatScalableSetting([Config.LODBias.Value, Config.LODBias.Value, Config.LODBias.Value], ScalableSettingSchemaId.With3Levels); diff --git a/Source/Rendering/VolumeManager.cs b/Source/Rendering/VolumeManager.cs index 20dd3382..d4acc544 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/Settings/SettingsManager.cs b/Source/UI/Settings/SettingsManager.cs index 9a94bbae..4c5acadc 100644 --- a/Source/UI/Settings/SettingsManager.cs +++ b/Source/UI/Settings/SettingsManager.cs @@ -262,7 +262,7 @@ public void ConfirmSettings() settings.supportMotionVectors = true; settings.xrSettings.occlusionMesh = Plugin.Config.EnableOcclusionMesh.Value; - settings.xrSettings.singlePass = false; + settings.xrSettings.singlePass = true; settings.lodBias = new FloatScalableSetting( From 06c5a596bbf7c817def5aa3f213abd8e6062dab6 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Sun, 5 Apr 2026 17:07:54 +0200 Subject: [PATCH 06/31] Actions fix --- .github/workflows/build-debug.yaml | 3 +++ .github/workflows/build-release.yaml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build-debug.yaml b/.github/workflows/build-debug.yaml index 48bc60fd..8ce67f9a 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 bb1de7d5..9d485329 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/ From 2291e01a0f9ee15f49465d9e46b884f8411152c5 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 6 Apr 2026 12:43:34 +0200 Subject: [PATCH 07/31] Shader and bug fixes --- Source/Assets/AssetManager.cs | 6 +- Source/Input/Actions.cs | 24 +++-- Source/Managers/VRSession.cs | 4 + Source/Patches/Items/BeltItemPatches.cs | 28 ++++++ Source/Player/VRController.cs | 122 +++++++++++------------- Source/Plugin.cs | 2 +- 6 files changed, 106 insertions(+), 80 deletions(-) diff --git a/Source/Assets/AssetManager.cs b/Source/Assets/AssetManager.cs index 29627de9..e20a6d46 100644 --- a/Source/Assets/AssetManager.cs +++ b/Source/Assets/AssetManager.cs @@ -28,10 +28,11 @@ public static class AssetManager public static Material SplashMaterial; public static Material DefaultRayMat; + public static Material PosterizationShaderMat; public static Shader TMPAlwaysOnTop; public static Shader VignettePostProcess; - + public static InputActionAsset VRActions; public static InputActionAsset DefaultXRActions; public static InputActionAsset NullActions; @@ -82,12 +83,13 @@ internal static bool LoadAssets() TMPAlwaysOnTop = assetsBundle.LoadAsset("TextMeshPro Always On Top"); VignettePostProcess = assetsBundle.LoadAsset("Vignette"); - + RemappableControls = assetsBundle.LoadAsset("Remappable Controls").GetComponent(); SplashMaterial = assetsBundle.LoadAsset("Splash"); DefaultRayMat = assetsBundle.LoadAsset("Default Ray"); + PosterizationShaderMat = assetsBundle.LoadAsset("FullScreen_SpongePosterizeNew"); GithubImage = assetsBundle.LoadAsset("Github"); KofiImage = assetsBundle.LoadAsset("Ko-Fi"); diff --git a/Source/Input/Actions.cs b/Source/Input/Actions.cs index afebac19..79624036 100644 --- a/Source/Input/Actions.cs +++ b/Source/Input/Actions.cs @@ -7,17 +7,19 @@ public class Actions { public static Actions Instance { get; private set; } = new(); - public InputAction HeadPosition { get; private set; } - public InputAction HeadRotation { get; private set; } - public InputAction HeadTrackingState { get; private set; } + public readonly InputAction HeadPosition; + public readonly InputAction HeadRotation; + public readonly InputAction HeadTrackingState; - public InputAction LeftHandPosition { get; private set; } - public InputAction LeftHandRotation { get; private set; } - public InputAction LeftHandTrackingState { get; private set; } + public readonly InputAction LeftHandPosition; + public readonly InputAction LeftHandRotation; + public readonly InputAction LeftHandVelocity; + public readonly InputAction LeftHandTrackingState; - public InputAction RightHandPosition { get; private set; } - public InputAction RightHandRotation { get; private set; } - public InputAction RightHandTrackingState { get; private set; } + public readonly InputAction RightHandPosition; + public readonly InputAction RightHandRotation; + public readonly InputAction RightHandVelocity; + public readonly InputAction RightHandTrackingState; private Actions() { @@ -27,11 +29,15 @@ private Actions() LeftHandPosition = AssetManager.DefaultXRActions.FindAction("Left/Position"); LeftHandRotation = AssetManager.DefaultXRActions.FindAction("Left/Rotation"); + LeftHandVelocity = AssetManager.DefaultXRActions.FindAction("Left/Velocity"); LeftHandTrackingState = AssetManager.DefaultXRActions.FindAction("Left/Tracking State"); RightHandPosition = AssetManager.DefaultXRActions.FindAction("Right/Position"); RightHandRotation = AssetManager.DefaultXRActions.FindAction("Right/Rotation"); + RightHandVelocity = AssetManager.DefaultXRActions.FindAction("Right/Velocity"); RightHandTrackingState = AssetManager.DefaultXRActions.FindAction("Right/Tracking State"); + + AssetManager.DefaultXRActions.Enable(); } public InputAction this[string name] => IngamePlayerSettings.Instance.playerInput.actions[name]; diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs index ba92c48d..14bc7cbe 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -207,6 +207,10 @@ private void InitializeVRSession() distortionFilters.ForEach(filter => filter.active = false); + // Replace posterization shader + if (HUDManager.Instance.mainCustomPass.customPasses[0] is FullScreenCustomPass pass) + pass.fullscreenPassMaterial = AssetManager.PosterizationShaderMat; + // Initialize secondary custom camera if (Plugin.Config.EnableCustomCamera.Value) InitializeCustomCamera(); diff --git a/Source/Patches/Items/BeltItemPatches.cs b/Source/Patches/Items/BeltItemPatches.cs index 0af3a941..e0ae82db 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/Player/VRController.cs b/Source/Player/VRController.cs index 19b597fd..0e28650b 100644 --- a/Source/Player/VRController.cs +++ b/Source/Player/VRController.cs @@ -113,54 +113,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) + 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; + + // Prevent picking up when two-handed + if (PlayerController.twoHanded && (!PlayerController.hoveringOverTrigger.twoHandedItemAllowed || + PlayerController.hoveringOverTrigger.specialCharacterAnimation)) + return; - if (!PlayerController.InteractTriggerUseConditionsMet()) return; + if (!PlayerController.InteractTriggerUseConditionsMet()) + return; - PlayerController.hoveringOverTrigger.Interact(PlayerController.thisPlayerBody); - } - catch (Exception ex) - { - Debug.LogError(ex.Message); - Debug.LogError(ex.StackTrace); - } + PlayerController.hoveringOverTrigger.Interact(PlayerController.thisPlayerBody); } private void ClickHoldInteraction() @@ -250,6 +243,7 @@ private void LateUpdate() if (component != PlayerController.previousHoveringOverTrigger && PlayerController.previousHoveringOverTrigger != null) { + PlayerController.previousHoveringOverTrigger.StopInteraction(); PlayerController.previousHoveringOverTrigger.isBeingHeldByPlayer = false; } @@ -316,13 +310,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 +340,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 +380,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 +401,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 +417,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 @@ -440,10 +432,4 @@ public void GrabItem(GrabbableObject item) PlayerController.grabObjectCoroutine = PlayerController.StartCoroutine(PlayerController.GrabObject()); } } - - private static bool CanBeInsertedIntoBag(GrabbableObject item) - { - return !item.itemProperties.isScrap && !item.isHeld && !item.isHeldByEnemy && - item.itemProperties.itemId is not (123984 or 819501); - } } \ No newline at end of file diff --git a/Source/Plugin.cs b/Source/Plugin.cs index 475eb255..6cac28cc 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -40,7 +40,7 @@ public class Plugin : BaseUnityPlugin private readonly string[] GAME_ASSEMBLY_HASHES = [ - "5CDBE2C86347F5183A6AEA2A20C34AB2B5D09EBC7D5FE959C043F7EB600E0823", // V81 + "774EFCF54CF475373938643AC32D5529755F12C86F3FA29735E2F7D00C2309F7", // V81 ]; public new static Config Config { get; private set; } From 070edcc9722c91ff9f40b11616d999d220e92dfd Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 6 Apr 2026 15:00:20 +0200 Subject: [PATCH 08/31] A few more little fixes --- LCVR.csproj | 2 +- Source/Player/VRController.cs | 2 +- Source/UI/VRHUD.cs | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/LCVR.csproj b/LCVR.csproj index 06fea1e9..bd2c20f6 100644 --- a/LCVR.csproj +++ b/LCVR.csproj @@ -37,7 +37,7 @@ - + diff --git a/Source/Player/VRController.cs b/Source/Player/VRController.cs index 0e28650b..7aded2f9 100644 --- a/Source/Player/VRController.cs +++ b/Source/Player/VRController.cs @@ -135,7 +135,7 @@ private void OnInteractPerformed(InputAction.CallbackContext context) if (StartOfRound.Instance.suckingPlayersOutOfShip) return; - if (!PlayerController.activatingItem && !PlayerController.waitingToDropItem) + if (!PlayerController.activatingItem && !PlayerController.waitingToDropItem && !PlayerController.isPlayerDead) BeginGrabObject(); // Ignore hold triggers diff --git a/Source/UI/VRHUD.cs b/Source/UI/VRHUD.cs index 17c059ee..9db36dec 100644 --- a/Source/UI/VRHUD.cs +++ b/Source/UI/VRHUD.cs @@ -401,17 +401,27 @@ 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 * 18; + 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; + + var loadingScreenSeedText = loadingScreen.Find("LoadTextB").GetComponent(); + loadingScreenSeedText.alignment = TextAlignmentOptions.Center; + // Fired screen: In front of eyes var firedScreen = GameObject.Find("GameOverScreen"); From 471c9c9bd904799d027fe2581b8ad513546e954c Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 6 Apr 2026 16:56:13 +0200 Subject: [PATCH 09/31] Add status effect and lever slapping --- Source/Assets/AssetManager.cs | 6 +++ Source/Physics/Interactions/ShipLever.cs | 55 ++++++++++++++++++++---- Source/UI/VRHUD.cs | 13 ++++++ 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/Source/Assets/AssetManager.cs b/Source/Assets/AssetManager.cs index e20a6d46..0b364df5 100644 --- a/Source/Assets/AssetManager.cs +++ b/Source/Assets/AssetManager.cs @@ -46,6 +46,9 @@ public static class AssetManager public static Sprite SprintImage; public static AudioClip DoorLocked; + public static AudioClip LeverShove; + + public static RuntimeAnimatorController IntroLeverAnimator; internal static bool LoadAssets() { @@ -98,6 +101,9 @@ internal static bool LoadAssets() SprintImage = assetsBundle.LoadAsset("Aguy"); DoorLocked = assetsBundle.LoadAsset("doorlocked"); + LeverShove = assetsBundle.LoadAsset("EndGameShoveLever"); + + IntroLeverAnimator = assetsBundle.LoadAsset("HangarDoorLeverAlt"); if (RemappableControls == null || RemappableControls.controls == null) { diff --git a/Source/Physics/Interactions/ShipLever.cs b/Source/Physics/Interactions/ShipLever.cs index 2fd304e3..d6f29cab 100644 --- a/Source/Physics/Interactions/ShipLever.cs +++ b/Source/Physics/Interactions/ShipLever.cs @@ -3,6 +3,7 @@ using LCVR.Player; using System.Collections; using System.IO; +using LCVR.Input; using LCVR.Managers; using UnityEngine; @@ -49,12 +50,28 @@ public void OnButtonRelease(VRInteractor interactor) 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; @@ -63,6 +80,7 @@ public class ShipLever : MonoBehaviour private Channel channel; public bool CanInteract => lever.triggerScript.interactable && currentActor != Actor.Other; + public bool InOrbit => lever.playersManager.inShipPhase; private void Awake() { @@ -102,6 +120,13 @@ private void Update() transform.eulerAngles = eulerAngles; } + public void ShoveLever(bool isLocal = true) + { + StartCoroutine(PerformLeverAction(isLocal, true)); + + channel.SendPacket([2]); + } + public void StartInteracting(Transform target, Actor actor) { currentActor = actor; @@ -136,30 +161,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: StopInteracting(); + ShoveLever(false); break; } } @@ -168,6 +205,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/UI/VRHUD.cs b/Source/UI/VRHUD.cs index 9db36dec..e3ff70f5 100644 --- a/Source/UI/VRHUD.cs +++ b/Source/UI/VRHUD.cs @@ -373,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; From 85dde553ea477b353d34de994d26d4b9f32f3dd3 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 6 Apr 2026 17:44:34 +0200 Subject: [PATCH 10/31] Remove dynamic resolution support --- CHANGELOG.md | 6 ++++-- Source/Config.cs | 17 ----------------- Source/Managers/VRSession.cs | 2 -- Source/Player/VRController.cs | 1 - Source/Plugin.cs | 7 ------- Source/UI/Settings/SettingsManager.cs | 6 ------ 6 files changed, 4 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82af76fc..ec380593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,8 @@ # 1.5.0 -**The posterization shader is not yet compatible with VR! Left and right eye will not match!** - **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 **Changes**: - Changed rendering to Single Pass Instanced, this may break some mods @@ -13,6 +12,9 @@ - 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 +**Removals**: +- Removed dynamic resolution support as it doesn't seem to function properly with SPI rendering + # 1.4.6 **Additions**: diff --git a/Source/Config.cs b/Source/Config.cs index 387bbdfb..c787b89d 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -4,7 +4,6 @@ using System.Reflection; using HarmonyLib; using Newtonsoft.Json; -using UnityEngine.Rendering; namespace LCVR; @@ -41,22 +40,6 @@ public class Config(string assemblyPath, ConfigFile file) public ConfigEntry EnableOcclusionMesh { get; } = file.Bind("Performance", "EnableOcclusionMesh", true, "The occlusion mesh will cause the game to stop rendering pixels outside of the lens views, which increases performance."); - public ConfigEntry EnableDynamicResolution { get; } = file.Bind("Performance", "EnableDynamicResolution", - false, - "Whether or not dynamic resolution should be enabled. Required for most of these settings to have an effect."); - - public ConfigEntry DynamicResolutionUpscaleFilter { get; } = file.Bind("Performance", - "DynamicResolutionUpscaleFilter", DynamicResUpscaleFilter.EdgeAdaptiveScalingUpres, - new ConfigDescription( - "The filter/algorithm that will be used to perform dynamic resolution upscaling. Defaulted to FSR (Edge Adaptive Scaling).", - new AcceptableValueEnum())); - - public ConfigEntry DynamicResolutionPercentage { get; } = file.Bind("Performance", - "DynamicResolutionPercentage", 80f, - new ConfigDescription( - "The percentage of resolution to scale the game down to. The lower the value, the harder the upscale filter has to work which will result in quality loss.", - new AcceptableValueRange(10, 100))); - public ConfigEntry CameraResolution { get; } = file.Bind("Performance", "CameraResolution", 0.75f, new ConfigDescription( "This setting configures the resolution scale of the game, lower values are more performant, but will make the game look worse. From around 0.8 the difference is negligible (on a Quest 2, with dynamic resolution disabled).", diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs index 14bc7cbe..b85ab40f 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -161,8 +161,6 @@ private void InitializeVRSession() // Apply optimization configuration var hdCamera = MainCamera.GetComponent(); - hdCamera.allowDynamicResolution = Plugin.Config.EnableDynamicResolution.Value; - hdCamera.DisableQualitySetting(FrameSettingsField.DepthOfField); hdCamera.DisableQualitySetting(FrameSettingsField.SSAO); hdCamera.DisableQualitySetting(FrameSettingsField.SSAOAsync); diff --git a/Source/Player/VRController.cs b/Source/Player/VRController.cs index 7aded2f9..3575ab1b 100644 --- a/Source/Player/VRController.cs +++ b/Source/Player/VRController.cs @@ -1,5 +1,4 @@ using GameNetcodeStuff; -using System; using UnityEngine.InputSystem; using UnityEngine; using UnityEngine.XR; diff --git a/Source/Plugin.cs b/Source/Plugin.cs index 6cac28cc..571de68e 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -10,12 +10,10 @@ 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; -using UnityEngine.XR.OpenXR; using DependencyFlags = BepInEx.BepInDependency.DependencyFlags; using VolumeManager = LCVR.Rendering.VolumeManager; @@ -304,11 +302,6 @@ private static bool InitializeVR() 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; diff --git a/Source/UI/Settings/SettingsManager.cs b/Source/UI/Settings/SettingsManager.cs index 4c5acadc..3e838c41 100644 --- a/Source/UI/Settings/SettingsManager.cs +++ b/Source/UI/Settings/SettingsManager.cs @@ -6,7 +6,6 @@ using TMPro; using UnityEngine; using UnityEngine.InputSystem; -using UnityEngine.Rendering; using UnityEngine.Rendering.HighDefinition; using UnityEngine.UI; @@ -254,11 +253,6 @@ public void ConfirmSettings() 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; From 300be1fd0df0ca2002289dd737a1cd1cbfb61746 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 6 Apr 2026 18:51:26 +0200 Subject: [PATCH 11/31] Add item jiggle shaking interaction --- Source/Input/ShakeDetector.cs | 10 ++++++++-- Source/Player/VRController.cs | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Source/Input/ShakeDetector.cs b/Source/Input/ShakeDetector.cs index 9d2a57d4..9dc13b86 100644 --- a/Source/Input/ShakeDetector.cs +++ b/Source/Input/ShakeDetector.cs @@ -3,7 +3,7 @@ namespace LCVR.Input; -public class ShakeDetector(Transform source, float threshold, bool useLocalPosition = false) +public class ShakeDetector(Transform source, float threshold, bool useLocalPosition = false, float delay = 0.0f) { private const float AccelerometerUpdateInterval = 1.0f / 60.0f; private const float LowPassKernelWidthInSeconds = 1.0f; @@ -17,14 +17,20 @@ public class ShakeDetector(Transform source, float threshold, bool useLocalPosit private Vector3 Position => useLocalPosition ? source.localPosition : source.position; + private float lastTrigger; + public void Update() { var acceleration = Position - previousPosition; lowPassValue = Vector3.Lerp(lowPassValue, acceleration, lowPassFilterFactor); var deltaAcceleration = acceleration - lowPassValue; - if (deltaAcceleration.sqrMagnitude >= shakeDetectionThreshold) + if (deltaAcceleration.sqrMagnitude >= shakeDetectionThreshold && + Time.realtimeSinceStartup - lastTrigger > delay) + { onShake?.Invoke(); + lastTrigger = Time.realtimeSinceStartup; + } previousPosition = Position; } diff --git a/Source/Player/VRController.cs b/Source/Player/VRController.cs index 3575ab1b..276da643 100644 --- a/Source/Player/VRController.cs +++ b/Source/Player/VRController.cs @@ -25,6 +25,8 @@ public class VRController : MonoBehaviour private LineRenderer debugLineRenderer; + private ShakeDetector jiggleDetector; + private static string CursorTip { set @@ -37,6 +39,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"); @@ -65,11 +68,19 @@ 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.03f, true, 0.25f); + } + + private void OnEnable() + { + jiggleDetector.onShake += OnJiggleDetected; } private void OnDisable() { IsHovering = false; + jiggleDetector.onShake -= OnJiggleDetected; if (!PlayerController) return; @@ -203,6 +214,8 @@ private void ClickHoldInteraction() private void Update() { + jiggleDetector.Update(); + if (PlayerController.IsOwner && PlayerController.isPlayerControlled) ClickHoldInteraction(); } @@ -431,4 +444,12 @@ public void GrabItem(GrabbableObject item) PlayerController.grabObjectCoroutine = PlayerController.StartCoroutine(PlayerController.GrabObject()); } } + + private void OnJiggleDetected() + { + if (PlayerController.currentlyHeldObjectServer is not {} item || item.isPocketed) + return; + + item.JiggleItemEffect(Actions.Instance.RightHandVelocity.ReadValue().magnitude); + } } \ No newline at end of file From 2ec36c969104180a9f6460d3d374d7947dd68705 Mon Sep 17 00:00:00 2001 From: Daniel <46288749+DaXcess@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:33:08 +0200 Subject: [PATCH 12/31] Revert "Refactor interaction system (#311)" (#315) This reverts commit 001d920df4cc4e63fde311aba4c7b789d8556920. --- Source/Physics/Interactions/Ladder.cs | 137 +++++++++++++++++--------- Source/Player/VRInteractor.cs | 102 ++++++++----------- 2 files changed, 130 insertions(+), 109 deletions(-) diff --git a/Source/Physics/Interactions/Ladder.cs b/Source/Physics/Interactions/Ladder.cs index 23eda456..6bcadcf6 100644 --- a/Source/Physics/Interactions/Ladder.cs +++ b/Source/Physics/Interactions/Ladder.cs @@ -34,9 +34,11 @@ private void Awake() private void Update() { - if (VRSession.Instance is not { LocalPlayer.PlayerController: var player }) + if (VRSession.Instance is not { } instance) return; + var player = instance.LocalPlayer.PlayerController; + if (!player.isClimbingLadder || !isActiveLadder) return; @@ -66,9 +68,8 @@ private void Update() totalMovement.z = 0; var maxMovementThisFrame = MAX_CLIMB_SPEED * Time.deltaTime; - totalMovement.y = Mathf.Abs(totalMovement.y) > maxMovementThisFrame - ? Mathf.Sign(totalMovement.y) * maxMovementThisFrame - : totalMovement.y; + if (Mathf.Abs(totalMovement.y) > maxMovementThisFrame) + totalMovement.y = Mathf.Sign(totalMovement.y) * maxMovementThisFrame; if (Mathf.Abs(totalMovement.y) > 0.001f) player.thisPlayerBody.position += totalMovement; @@ -82,23 +83,25 @@ private void Update() if (playerHeadY < topY - 0.3f) return; - var exitPosition = ladderTrigger.useRaycastToGetTopPosition - ? GetRaycastExitPosition(player) - : ladderTrigger.topOfLadderPosition.position; + Vector3 exitPosition; - StartCoroutine(ExitLadder(player, 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; + } - private Vector3 GetRaycastExitPosition(GameNetcodeStuff.PlayerControllerB player) - { - var rayStart = player.transform.position + Vector3.up * 0.5f; - var rayEnd = ladderTrigger.topOfLadderPosition.position + Vector3.up * 0.5f; - - return UnityEngine.Physics.Linecast(rayStart, rayEnd, out var hit, - StartOfRound.Instance.collidersAndRoomMaskAndDefault, - QueryTriggerInteraction.Ignore) - ? hit.point - : ladderTrigger.topOfLadderPosition.position; + StartCoroutine(ExitLadder(player, exitPosition)); } private IEnumerator ExitLadder(GameNetcodeStuff.PlayerControllerB player, Vector3 exitPosition) @@ -153,36 +156,41 @@ private IEnumerator ExitLadder(GameNetcodeStuff.PlayerControllerB player, Vector public bool OnButtonPress(VRInteractor interactor) { - if (VRSession.Instance is not { LocalPlayer.PlayerController: var player }) - return false; + 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); + rightHandGripPoint = + transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.RightHandVRTarget.position); rightHandInteractor = interactor; } else { - leftHandGripPoint = transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.LeftHandVRTarget.position); + leftHandGripPoint = + transform.InverseTransformPoint(VRSession.Instance.LocalPlayer.LeftHandVRTarget.position); leftHandInteractor = interactor; } if (!player.isClimbingLadder) { - if (ladderTrigger == null || !ladderTrigger.interactable) + 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; - - isActiveLadder = true; - climbStartTime = Time.time; - player.isClimbingLadder = true; - player.thisController.enabled = false; - - player.takingFallDamage = false; - player.fallValue = 0f; - player.fallValueUncapped = 0f; + } } - else if (!isActiveLadder) + else if (player.isClimbingLadder && !isActiveLadder) { return false; } @@ -209,8 +217,7 @@ public void OnButtonRelease(VRInteractor interactor) if (leftHandGripPoint.HasValue || rightHandGripPoint.HasValue || !isActiveLadder) return; - if (VRSession.Instance is not { LocalPlayer.PlayerController: var player }) - return; + var player = VRSession.Instance.LocalPlayer.PlayerController; isActiveLadder = false; player.isClimbingLadder = false; @@ -227,6 +234,19 @@ 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 @@ -235,11 +255,18 @@ internal static class LadderPatches [HarmonyPostfix] private static void OnLadderStart(InteractTrigger __instance) { - if (!__instance.isLadder || Plugin.Config.DisableLadderClimbingInteraction.Value) + if (!__instance.isLadder) return; - // Create single collider - InteractionManager now supports multiple hands per interactable - var collider = Object.Instantiate(AssetManager.Interactable, __instance.transform); + 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) { @@ -248,25 +275,41 @@ private static void OnLadderStart(InteractTrigger __instance) var midPoint = (topPos + bottomPos) / 2f; var height = Mathf.Abs(topPos.y - bottomPos.y); - collider.transform.localPosition = midPoint + new Vector3(0f, 0f, 0.3f); - collider.transform.localScale = new Vector3(1.2f, height, 0.8f); + // 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 { - collider.transform.localPosition = new Vector3(0f, 0f, 0.3f); - collider.transform.localScale = new Vector3(1.2f, 3f, 0.8f); + 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); } - collider.AddComponent(); + // Both colliders reference the same ladder component + leftHandCollider.AddComponent().ladder = ladderComponent; + rightHandCollider.AddComponent().ladder = ladderComponent; - foreach (var existingCollider in __instance.GetComponents()) - existingCollider.enabled = false; + foreach (var collider in __instance.GetComponents()) + collider.enabled = false; } [HarmonyPatch(typeof(InteractTrigger), nameof(InteractTrigger.Interact))] [HarmonyPrefix] private static bool PreventLadderInteract(InteractTrigger __instance) { - return !__instance.isLadder || Plugin.Config.DisableLadderClimbingInteraction.Value; + if (!__instance.isLadder) + return true; + + if (Plugin.Config.DisableLadderClimbingInteraction.Value) + return true; + + return false; } } \ No newline at end of file diff --git a/Source/Player/VRInteractor.cs b/Source/Player/VRInteractor.cs index be69c751..0f15ecba 100644 --- a/Source/Player/VRInteractor.cs +++ b/Source/Player/VRInteractor.cs @@ -1,4 +1,4 @@ -using LCVR.Assets; +using LCVR.Assets; using LCVR.Input; using LCVR.Physics; using System.Collections.Generic; @@ -176,110 +176,88 @@ private readonly struct Offset(Vector3 position, Vector3 scale, Quaternion rotat } } -/// -/// Manages VR interactions between interactors (hands) and interactables (objects). -/// Supports multiple interactors per interactable, allowing both hands to interact with the same object simultaneously. -/// public class InteractionManager { - private readonly Dictionary> interactableStates = []; + private readonly Dictionary interactableState = []; public void ReportInteractables(VRInteractor interactor, VRInteractable[] interactables) { foreach (var interactable in interactables) { - // Initialize state dictionary for this interactable if needed - if (!interactableStates.ContainsKey(interactable)) - interactableStates[interactable] = []; - - var states = interactableStates[interactable]; - - // Check if this hand is allowed to interact - if (interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.RightHand)) - continue; + if (!interactableState.ContainsKey(interactable)) + { + // Check if this hand is allowed to interact with this object + if (interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.RightHand)) + continue; - if (!interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.LeftHand)) - continue; + if (!interactor.IsRightHand && !interactable.Flags.HasFlag(InteractableFlags.LeftHand)) + continue; - if (interactor.isHeld && interactable.Flags.HasFlag(InteractableFlags.NotWhileHeld)) - continue; + if (interactor.isHeld && interactable.Flags.HasFlag(InteractableFlags.NotWhileHeld)) + continue; - // Add this interactor if not already tracking - if (!states.ContainsKey(interactor)) - { - states[interactor] = new InteractableState(false); + interactableState.Add(interactable, new InteractableState(interactor, false)); interactable.OnColliderEnter(interactor); } - var state = states[interactor]; + // Ignore events from hand if other hand is already interacting + if (interactableState[interactable].interactor != interactor) + continue; - // Handle button press - if (!state.isHeld && !interactor.isHeld && interactor.IsPressed()) + if (!interactableState[interactable].isHeld && !interactor.isHeld && interactor.IsPressed()) { var acknowledged = interactable.OnButtonPress(interactor); - state.isHeld = interactor.isHeld = acknowledged; + interactableState[interactable].isHeld = interactor.isHeld = acknowledged; } - // Handle button release - else if (state.isHeld && !interactor.IsPressed()) + else if (interactableState[interactable].isHeld && !interactor.IsPressed()) { interactable.OnButtonRelease(interactor); - state.isHeld = interactor.isHeld = false; + interactableState[interactable].isHeld = interactor.isHeld = false; } } - // Clean up interactors that left the collision zone - var interactablesToRemove = new List(); - - foreach (var (interactable, states) in interactableStates) + foreach (var interactable in interactableState.Keys) { - if (!states.ContainsKey(interactor)) + // Ignore if this state is being managed by another hand + if (interactableState[interactable].interactor != interactor) continue; if (interactables.Contains(interactable)) continue; - var state = states[interactor]; - - // Release if still held - if (state.isHeld) - { + // Ignore if button is still being held down + if (interactableState[interactable].isHeld) if (interactor.IsPressed()) continue; - - interactable.OnButtonRelease(interactor); - interactor.isHeld = false; - } + else { + interactable.OnButtonRelease(interactor); + interactor.isHeld = false; + } + interactableState.Remove(interactable); interactable.OnColliderExit(interactor); - states.Remove(interactor); - // Clean up empty state dictionaries - if (states.Count == 0) - interactablesToRemove.Add(interactable); + // Break to not make C# shit itself, if more need to be removed it'll happen next frame + break; } - - foreach (var interactable in interactablesToRemove) - interactableStates.Remove(interactable); } public void ResetState() { - foreach (var (interactable, states) in interactableStates) + foreach (var interactable in interactableState.Keys) { - foreach (var (interactor, state) in states) - { - if (state.isHeld) - interactable.OnButtonRelease(interactor); - - interactable.OnColliderExit(interactor); - } + var state = interactableState[interactable]; + + if (state.isHeld) + interactable.OnButtonRelease(state.interactor); + + interactable.OnColliderExit(state.interactor); } - - interactableStates.Clear(); } - private class InteractableState(bool isHeld) + private class InteractableState(VRInteractor interactor, bool isHeld) { + public VRInteractor interactor = interactor; public bool isHeld = isHeld; } } \ No newline at end of file From 2cef1d2ad9c5a3af64d9c621d130b4e12007b0de Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 6 Apr 2026 21:12:21 +0200 Subject: [PATCH 13/31] lever fix --- Docs/Thunderstore/README.md | 2 +- README.md | 2 +- Source/Physics/Interactions/ShipLever.cs | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Docs/Thunderstore/README.md b/Docs/Thunderstore/README.md index 00a5d75e..fb04afab 100644 --- a/Docs/Thunderstore/README.md +++ b/Docs/Thunderstore/README.md @@ -65,7 +65,7 @@ Here is a list of LCVR versions and which version(s) of Lethal Company it suppor | LCVR | Lethal Company | |-------------------|-------------------| -| v1.5.0 *(LATEST)* | V80 | +| v1.5.0 *(LATEST)* | V81 | | v1.4.7 | V73 | | v1.4.6 | V73 | | v1.4.5 | V73 | diff --git a/README.md b/README.md index 00ecef33..d750f9bb 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Here is a list of LCVR versions and which version(s) of Lethal Company it suppor | LCVR | Lethal Company | [Configuration version](Docs/Configuration/README.md) | |-------------------|-------------------|-------------------------------------------------------| -| v1.5.0 *(LATEST)* | V80 | 1 | +| v1.5.0 *(LATEST)* | V81 | 1 | | v1.4.7 | V73 | 1 | | v1.4.6 | V73 | 1 | | v1.4.5 | V73 | 1 | diff --git a/Source/Physics/Interactions/ShipLever.cs b/Source/Physics/Interactions/ShipLever.cs index d6f29cab..52edc642 100644 --- a/Source/Physics/Interactions/ShipLever.cs +++ b/Source/Physics/Interactions/ShipLever.cs @@ -79,7 +79,7 @@ public class ShipLever : MonoBehaviour private Actor currentActor; private Channel channel; - public bool CanInteract => lever.triggerScript.interactable && currentActor != Actor.Other; + public bool CanInteract => lever.triggerScript.interactable; public bool InOrbit => lever.playersManager.inShipPhase; private void Awake() @@ -195,7 +195,6 @@ private void OnOtherInteractWithLever(ushort other, BinaryReader reader) StopInteracting(); break; case 2: - StopInteracting(); ShoveLever(false); break; } From becdd7ff22462a7da5c4506dbf41f36baadd70dd Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 6 Apr 2026 21:35:29 +0200 Subject: [PATCH 14/31] more lever fix --- Source/Physics/Interactions/ShipLever.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Physics/Interactions/ShipLever.cs b/Source/Physics/Interactions/ShipLever.cs index 52edc642..29b56edc 100644 --- a/Source/Physics/Interactions/ShipLever.cs +++ b/Source/Physics/Interactions/ShipLever.cs @@ -120,11 +120,12 @@ private void Update() transform.eulerAngles = eulerAngles; } - public void ShoveLever(bool isLocal = true) + public void ShoveLever(bool isOwner = true) { - StartCoroutine(PerformLeverAction(isLocal, true)); + StartCoroutine(PerformLeverAction(isOwner, true)); - channel.SendPacket([2]); + if (isOwner) + channel.SendPacket([2]); } public void StartInteracting(Transform target, Actor actor) From 4a9ab05c2c87172bb715c6b8ced960b7442f5383 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 6 Apr 2026 22:02:21 +0200 Subject: [PATCH 15/31] add commit info --- Source/Plugin.cs | 2 +- Source/UI/Environment/MainMenuEnvironment.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Plugin.cs b/Source/Plugin.cs index 571de68e..f9591455 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -167,7 +167,7 @@ private void Awake() Native.BringGameWindowToFront(); } - private static string GetCommitHash() + internal static string GetCommitHash() { try { diff --git a/Source/UI/Environment/MainMenuEnvironment.cs b/Source/UI/Environment/MainMenuEnvironment.cs index 17329d99..70246db5 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(); From b014207807762149514e3552d3160f175ec7561a Mon Sep 17 00:00:00 2001 From: DaXcess Date: Tue, 7 Apr 2026 11:56:39 +0200 Subject: [PATCH 16/31] Add automatic log collection - Disabled in release builds - Disabled when playing in LAN mode --- Source/Managers/LogSharingManager.cs | 59 ++++++++++++++++++++++++++++ Source/Patches/MenuManagerPatches.cs | 22 +++++++++++ 2 files changed, 81 insertions(+) create mode 100644 Source/Managers/LogSharingManager.cs create mode 100644 Source/Patches/MenuManagerPatches.cs diff --git a/Source/Managers/LogSharingManager.cs b/Source/Managers/LogSharingManager.cs new file mode 100644 index 00000000..cf255ed2 --- /dev/null +++ b/Source/Managers/LogSharingManager.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Net.Http; +using Steamworks; +using UnityEngine; + +namespace LCVR.Managers; + +/// +/// Automatically share log files in dev builds +/// +public class LogSharingManager : MonoBehaviour +{ + private static LogSharingManager _instance; + + private AuthTicket authTicket; + + private void Awake() + { + if (_instance != null) + { + Destroy(gameObject); + return; + } + +#if DEBUG + _instance = this; + DontDestroyOnLoad(this); + + authTicket = SteamUser.GetAuthSessionTicket(); + + Application.quitting += OnGameClosing; +#else + Destroy(gameObject); +#endif + } + + private void OnGameClosing() + { + var authData = $"{SteamClient.SteamId.Value}.{Convert.ToBase64String(authTicket.Data)}"; + var client = new HttpClient(); + client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", authData); + + var data = File.Open(Application.consoleLogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + if (data.Length > 25 * 1024 * 1024) + return; + + var buffer = new byte[data.Length]; + var read = data.Read(buffer, 0, buffer.Length); + + var formData = new MultipartFormDataContent(); + formData.Add(new ByteArrayContent(buffer[..read]), "log"); + + Logger.LogInfo("Sending log file to log collector"); + + var url = Environment.GetEnvironmentVariable("LCVR_LOGS_UPLOAD_URL") ?? "https://lcvr-logs.daxcess.io/upload"; + _ = client.PostAsync(url, formData).Result; + } +} \ No newline at end of file diff --git a/Source/Patches/MenuManagerPatches.cs b/Source/Patches/MenuManagerPatches.cs new file mode 100644 index 00000000..89705941 --- /dev/null +++ b/Source/Patches/MenuManagerPatches.cs @@ -0,0 +1,22 @@ +#if DEBUG +using HarmonyLib; +using LCVR.Managers; +using UnityEngine; + +namespace LCVR.Patches; + +[LCVRPatch(LCVRPatchTarget.Universal)] +[HarmonyPatch] +internal static class MenuManagerPatches +{ + [HarmonyPatch(typeof(MenuManager), nameof(MenuManager.Start))] + [HarmonyPostfix] + private static void OnMenuStart() + { + if (GameNetworkManager.Instance.disableSteam) + return; + + new GameObject("LCVR Log Sharing Manager").AddComponent(); + } +} +#endif \ No newline at end of file From 6fc35fc7511a9c43b878a187c6d107215a824c70 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Tue, 7 Apr 2026 12:17:57 +0200 Subject: [PATCH 17/31] Fix interactions not being allowed --- Source/Player/VRController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Player/VRController.cs b/Source/Player/VRController.cs index 276da643..8c139b84 100644 --- a/Source/Player/VRController.cs +++ b/Source/Player/VRController.cs @@ -152,7 +152,7 @@ private void OnInteractPerformed(InputAction.CallbackContext context) if (PlayerController.hoveringOverTrigger == null || PlayerController.hoveringOverTrigger.holdInteraction) return; - if (PlayerController.isHoldingObject && PlayerController.hoveringOverTrigger.oneHandedItemAllowed) + if (PlayerController.isHoldingObject && !PlayerController.hoveringOverTrigger.oneHandedItemAllowed) return; // Prevent picking up when two-handed From f99b16b52f8cacfd3240b0e7f256c13d63abe8c1 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Tue, 7 Apr 2026 14:01:59 +0200 Subject: [PATCH 18/31] Fix custom cam bugs and remove ladder climb - Ladder climb interaction will be reintroduced at a later time as a more fleshed out feature --- Source/Config.cs | 3 - Source/Managers/SpectatingManager.cs | 13 +- Source/Managers/VRSession.cs | 22 +- Source/Physics/Interactions/Face.cs | 2 +- Source/Physics/Interactions/Ladder.cs | 315 -------------------------- 5 files changed, 25 insertions(+), 330 deletions(-) delete mode 100644 Source/Physics/Interactions/Ladder.cs diff --git a/Source/Config.cs b/Source/Config.cs index c787b89d..e0a99642 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -178,9 +178,6 @@ public class Config(string assemblyPath, ConfigFile file) public ConfigEntry DisableElevatorButtonInteraction { get; } = file.Bind("Interaction", "DisableElevatorButtonInteraction", false, "Disables needing to physically press the elevator buttons"); - public ConfigEntry DisableLadderClimbingInteraction { get; } = file.Bind("Interaction", - "DisableLadderClimbingInteraction", true, "Disables needing to physically climb ladders by gripping and pulling"); - // Car interaction configuration public ConfigEntry DisableCarSteeringWheelInteraction { get; } = file.Bind("Car", diff --git a/Source/Managers/SpectatingManager.cs b/Source/Managers/SpectatingManager.cs index e903e90e..fbe20ce5 100644 --- a/Source/Managers/SpectatingManager.cs +++ b/Source/Managers/SpectatingManager.cs @@ -1,5 +1,6 @@ using System.Collections; using GameNetcodeStuff; +using HarmonyLib; using LCVR.Assets; using LCVR.Compatibility.MoreCompany; using LCVR.Input; @@ -303,12 +304,18 @@ internal void ToggleFog(bool? enable = null) else FogDisabled = !FogDisabled; - var hdCamera = VRSession.Instance.MainCamera.GetComponent(); + HDAdditionalCameraData[] cameras = Plugin.Config.EnableCustomCamera.Value + ? + [ + VRSession.Instance.MainCamera.GetComponent(), + VRSession.Instance.CustomCamera.GetComponent() + ] + : [VRSession.Instance.MainCamera.GetComponent()]; if (FogDisabled) - hdCamera.DisableQualitySetting(FrameSettingsField.Volumetrics); + cameras.Do(camera => camera.DisableQualitySetting(FrameSettingsField.Volumetrics)); else - hdCamera.EnableQualitySetting(FrameSettingsField.Volumetrics); + cameras.Do(camera => camera.EnableQualitySetting(FrameSettingsField.Volumetrics)); } private void TeleportLocalPlayer(Vector3 position, bool inFactory, bool inElevator, bool inHangar) diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs index b85ab40f..4c0cb6c1 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -43,6 +43,7 @@ public class VRSession : MonoBehaviour public VRHUD HUD { get; private set; } public Camera MainCamera { get; private set; } + public Camera CustomCamera => customCamera; public Rendering.VolumeManager VolumeManager { get; private set; } public CameraShake CameraShake { get; private set; } @@ -159,22 +160,27 @@ private void InitializeVRSession() CameraShake = MainCamera.gameObject.AddComponent(); // Apply optimization configuration - var hdCamera = MainCamera.GetComponent(); - - hdCamera.DisableQualitySetting(FrameSettingsField.DepthOfField); - hdCamera.DisableQualitySetting(FrameSettingsField.SSAO); - hdCamera.DisableQualitySetting(FrameSettingsField.SSAOAsync); + HDAdditionalCameraData[] hdCameras = Plugin.Config.EnableCustomCamera.Value + ? + [ + MainCamera.GetComponent(), CustomCamera.GetComponent() + ] + : [MainCamera.GetComponent()]; + + hdCameras.Do(camera => camera.DisableQualitySetting(FrameSettingsField.DepthOfField)); + hdCameras.Do(camera => camera.DisableQualitySetting(FrameSettingsField.SSAO)); + hdCameras.Do(camera => camera.DisableQualitySetting(FrameSettingsField.SSAOAsync)); if (Plugin.Config.DisableVolumetrics.Value) - hdCamera.DisableQualitySetting(FrameSettingsField.Volumetrics); + hdCameras.Do(camera => camera.DisableQualitySetting(FrameSettingsField.Volumetrics)); // Handle volumetric setting change Plugin.Config.DisableVolumetrics.SettingChanged += (_, _) => { if (Plugin.Config.DisableVolumetrics.Value) - hdCamera.DisableQualitySetting(FrameSettingsField.Volumetrics); + hdCameras.Do(camera => camera.DisableQualitySetting(FrameSettingsField.Volumetrics)); else - hdCamera.EnableQualitySetting(FrameSettingsField.Volumetrics); + hdCameras.Do(camera => camera.EnableQualitySetting(FrameSettingsField.Volumetrics)); }; XRSettings.eyeTextureResolutionScale = Plugin.Config.CameraResolution.Value; diff --git a/Source/Physics/Interactions/Face.cs b/Source/Physics/Interactions/Face.cs index 9e51638f..380bc431 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 6bcadcf6..00000000 --- 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 From e9bfe0ba8d16799dfe237ace6b54910fff95e727 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Tue, 7 Apr 2026 18:05:05 +0200 Subject: [PATCH 19/31] Fog fixes --- CHANGELOG.md | 2 + Source/Config.cs | 61 +++++++++++++++++++++++++- Source/Managers/SpectatingManager.cs | 2 +- Source/Managers/VRSession.cs | 12 ----- Source/Patches/VolumePatches.cs | 19 ++++++++ Source/Plugin.cs | 16 +------ Source/UI/Settings/SettingsManager.cs | 28 +----------- Source/UI/Spectating/SpectatingMenu.cs | 2 +- 8 files changed, 85 insertions(+), 57 deletions(-) create mode 100644 Source/Patches/VolumePatches.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index ec380593..a8a63724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ **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 **Changes**: - Changed rendering to Single Pass Instanced, this may break some mods @@ -14,6 +15,7 @@ **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 # 1.4.6 diff --git a/Source/Config.cs b/Source/Config.cs index e0a99642..53de8957 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -2,8 +2,13 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Runtime.InteropServices; using HarmonyLib; using Newtonsoft.Json; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.Rendering.HighDefinition; namespace LCVR; @@ -45,8 +50,10 @@ public class Config(string assemblyPath, ConfigFile file) "This setting configures the resolution scale of the game, lower values are more performant, but will make the game look worse. From around 0.8 the difference is negligible (on a Quest 2, with dynamic resolution disabled).", new AcceptableValueRange(0.05f, 1.5f))); - public ConfigEntry DisableVolumetrics { get; } = file.Bind("Performance", "DisableVolumetrics", false, - "Disables volumetrics in the game, which significantly improves performance, but removes all fog and may be considered cheating."); + public ConfigEntry FogQuality { get; } = file.Bind("Performance", "FogQuality", + FogQualityOption.Default, + new ConfigDescription("Specify the quality of volumetrics (fog) in the game", + new AcceptableValueEnum())); // Input configuration @@ -310,12 +317,62 @@ public void DeserializeFromES3() } } + public static void ApplySettings() + { + var asset = QualitySettings.renderPipeline as HDRenderPipelineAsset; + + if (!asset) + { + Logger.LogError("Failed to apply render pipeline changes: Render pipeline is null??"); + return; + } + + ref var settings = ref asset.m_RenderPipelineSettings; + + settings.supportMotionVectors = true; + + settings.xrSettings.occlusionMesh = Plugin.Config.EnableOcclusionMesh.Value; + settings.xrSettings.singlePass = true; + + settings.lodBias = + new FloatScalableSetting( + [Plugin.Config.LODBias.Value, Plugin.Config.LODBias.Value, Plugin.Config.LODBias.Value], + ScalableSettingSchemaId.With3Levels); + + // Volumetrics + settings.supportVolumetrics = Plugin.Config.FogQuality.Value != FogQualityOption.Disabled; + settings.lightingQualitySettings.Fog_Budget = Plugin.Config.FogQuality.Value switch + { + FogQualityOption.Disabled => [0f, 0f, 0f], + FogQualityOption.Potato => [0.08f, 0.08f, 0.08f], + FogQualityOption.Default => [0.1666f, 0.1666f, 0.1666f], + FogQualityOption.Lite => [0.24f, 0.24f, 0.24f], + FogQualityOption.Medium => [0.333f, 0.333f, 0.333f], + FogQualityOption.High => [0.5f, 0.5f, 0.5f], + FogQualityOption.Ultra => [0.75f, 0.75f, 0.75f], + _ => [0.1666f, 0.1666f, 0.1666f] // Vanilla values + }; + + InputSystem.settings.defaultButtonPressPoint = Plugin.Config.ButtonPressPoint.Value; + } + public enum TurnProviderOption { Snap, Smooth, Disabled } + + public enum FogQualityOption + { + Ultra, + High, + Medium, + Lite, + Default, + Potato, + Disabled + } } internal class AcceptableValueEnum() : AcceptableValueBase(typeof(T)) diff --git a/Source/Managers/SpectatingManager.cs b/Source/Managers/SpectatingManager.cs index fbe20ce5..e619adb6 100644 --- a/Source/Managers/SpectatingManager.cs +++ b/Source/Managers/SpectatingManager.cs @@ -296,7 +296,7 @@ internal void ToggleLights(bool? enable = null) internal void ToggleFog(bool? enable = null) { - if (Plugin.Config.DisableVolumetrics.Value) + if (Plugin.Config.FogQuality.Value == Config.FogQualityOption.Disabled) return; if (enable.HasValue) diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs index 4c0cb6c1..08d63a12 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -171,18 +171,6 @@ private void InitializeVRSession() hdCameras.Do(camera => camera.DisableQualitySetting(FrameSettingsField.SSAO)); hdCameras.Do(camera => camera.DisableQualitySetting(FrameSettingsField.SSAOAsync)); - if (Plugin.Config.DisableVolumetrics.Value) - hdCameras.Do(camera => camera.DisableQualitySetting(FrameSettingsField.Volumetrics)); - - // Handle volumetric setting change - Plugin.Config.DisableVolumetrics.SettingChanged += (_, _) => - { - if (Plugin.Config.DisableVolumetrics.Value) - hdCameras.Do(camera => camera.DisableQualitySetting(FrameSettingsField.Volumetrics)); - else - hdCameras.Do(camera => camera.EnableQualitySetting(FrameSettingsField.Volumetrics)); - }; - XRSettings.eyeTextureResolutionScale = Plugin.Config.CameraResolution.Value; // Disable lens distortion effects diff --git a/Source/Patches/VolumePatches.cs b/Source/Patches/VolumePatches.cs new file mode 100644 index 00000000..e216ea85 --- /dev/null +++ b/Source/Patches/VolumePatches.cs @@ -0,0 +1,19 @@ +using HarmonyLib; +using UnityEngine.Rendering.HighDefinition; + +namespace LCVR.Patches; + +[LCVRPatch] +[HarmonyPatch] +internal static class VolumePatches +{ + /// + /// 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; + } +} diff --git a/Source/Plugin.cs b/Source/Plugin.cs index f9591455..7a8af1d7 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -298,20 +298,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.supportMotionVectors = true; - - settings.xrSettings.occlusionMesh = Config.EnableOcclusionMesh.Value; - settings.xrSettings.singlePass = true; - - 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/UI/Settings/SettingsManager.cs b/Source/UI/Settings/SettingsManager.cs index 3e838c41..7e1ec5ae 100644 --- a/Source/UI/Settings/SettingsManager.cs +++ b/Source/UI/Settings/SettingsManager.cs @@ -241,32 +241,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.supportMotionVectors = true; - - settings.xrSettings.occlusionMesh = Plugin.Config.EnableOcclusionMesh.Value; - settings.xrSettings.singlePass = true; - - 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 b99b3e39..09540395 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 From 376d3a460d88d7128300b83ea910a1e46ca8d5c2 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Tue, 7 Apr 2026 22:47:50 +0200 Subject: [PATCH 20/31] Fix helmet option breaking after rejoining --- Source/Managers/VRSession.cs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs index 08d63a12..f46cd398 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System; +using System.Collections; using HarmonyLib; using LCVR.Assets; using LCVR.Physics.Interactions; @@ -58,6 +59,7 @@ public class VRSession : MonoBehaviour #endregion private PauseMenuEnvironment pauseMenuEnvironment; + private GameObject helmetModel; private void Awake() { @@ -115,6 +117,14 @@ private void LateUpdate() } } + private void OnDestroy() + { + if (!InVR) + return; + + Plugin.Config.EnableHelmetVisor.SettingChanged -= HelmetVisorSettingChanged; + } + private void InitializeVRSession() { // Disable base UI input system @@ -126,15 +136,14 @@ private void InitializeVRSession() // Move around the volumetric plane var helmetContainer = GameObject.Find("Systems/Rendering/PlayerHUDHelmetModel"); - var helmetModel = helmetContainer.Find("ScavengerHelmet"); + helmetModel = helmetContainer.Find("ScavengerHelmet"); helmetModel.transform.Find("Plane").SetParent(helmetContainer.transform); // Toggle helmet visor visibility helmetModel.SetActive(Plugin.Config.EnableHelmetVisor.Value); // Listen to setting change for helmet model - Plugin.Config.EnableHelmetVisor.SettingChanged += - (_, _) => helmetModel.SetActive(Plugin.Config.EnableHelmetVisor.Value); + Plugin.Config.EnableHelmetVisor.SettingChanged += HelmetVisorSettingChanged; // Move helmet model to child of target point var helmetTarget = StartOfRound.Instance.localPlayerController.gameObject @@ -158,6 +167,10 @@ private void InitializeVRSession() // Add camera shaking CameraShake = MainCamera.gameObject.AddComponent(); + + // Initialize secondary custom camera + if (Plugin.Config.EnableCustomCamera.Value) + InitializeCustomCamera(); // Apply optimization configuration HDAdditionalCameraData[] hdCameras = Plugin.Config.EnableCustomCamera.Value @@ -203,10 +216,6 @@ private void InitializeVRSession() if (HUDManager.Instance.mainCustomPass.customPasses[0] is FullScreenCustomPass pass) pass.fullscreenPassMaterial = AssetManager.PosterizationShaderMat; - // Initialize secondary custom camera - if (Plugin.Config.EnableCustomCamera.Value) - InitializeCustomCamera(); - // Add keyboard to Terminal var terminal = FindObjectOfType(); @@ -382,6 +391,11 @@ private void InitializeVRSession() VolumeManager = Instantiate(AssetManager.VolumeManager, transform).GetComponent(); } + private void HelmetVisorSettingChanged(object sender, EventArgs e) + { + helmetModel.SetActive(Plugin.Config.EnableHelmetVisor.Value); + } + #region VR private void InitializeCustomCamera() From cc664e364c3eda1ba9a1fdb5df20aec729243674 Mon Sep 17 00:00:00 2001 From: Daniel <46288749+DaXcess@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:32:32 +0200 Subject: [PATCH 21/31] Add SPI compatibility docs --- Docs/COMPATIBILITY.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Docs/COMPATIBILITY.md diff --git a/Docs/COMPATIBILITY.md b/Docs/COMPATIBILITY.md new file mode 100644 index 00000000..20e54d3d --- /dev/null +++ b/Docs/COMPATIBILITY.md @@ -0,0 +1,33 @@ +# Compatibility with LCVR + +> This document is specifically for mods that add anything that can be rendered by the game and rendering, 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. + +image +> 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 From a77db8e72b8c2405d3e6e649748ddccd02c432df Mon Sep 17 00:00:00 2001 From: DaXcess Date: Wed, 8 Apr 2026 14:41:36 +0200 Subject: [PATCH 22/31] Smooth camera teleport fix --- CHANGELOG.md | 3 +++ Source/Managers/VRSession.cs | 9 +++++++++ Source/Patches/PlayerControllerPatches.cs | 13 +++++++++++++ Source/Player/VRController.cs | 2 +- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8a63724..0d31c9f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ - 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 + # 1.4.6 **Additions**: diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs index f46cd398..4a79b308 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -434,6 +434,15 @@ private void InitializeCustomCamera() customCameraLerpFactor = Mathf.Clamp(Plugin.Config.CustomCameraLerpFactor.Value, 0.01f, 1f); } + public void ForceUpdateCamera() + { + if (!customCameraEnabled) + return; + + customCamera.transform.position = MainCamera.transform.position; + customCamera.transform.rotation = MainCamera.transform.rotation; + } + public void OnEnterTerminal() { HUD.TerminalKeyboard.PresentKeyboard(); diff --git a/Source/Patches/PlayerControllerPatches.cs b/Source/Patches/PlayerControllerPatches.cs index 36bde54b..2d267e4c 100644 --- a/Source/Patches/PlayerControllerPatches.cs +++ b/Source/Patches/PlayerControllerPatches.cs @@ -453,6 +453,19 @@ private static bool ScrollToUtilitySlot(PlayerControllerB __instance, bool forwa // 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/Player/VRController.cs b/Source/Player/VRController.cs index 8c139b84..a27a0b75 100644 --- a/Source/Player/VRController.cs +++ b/Source/Player/VRController.cs @@ -69,7 +69,7 @@ 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.03f, true, 0.25f); + jiggleDetector = new ShakeDetector(transform, 0.005f, true, 0.25f); } private void OnEnable() From 2f9e33e3293fc899649bdd0272b2b9e66c56445a Mon Sep 17 00:00:00 2001 From: DaXcess Date: Wed, 8 Apr 2026 19:24:54 +0200 Subject: [PATCH 23/31] Fix growths not visible in custom cam --- Source/Managers/VRSession.cs | 21 ++++++++------- Source/Patches/RenderingPatches.cs | 41 ++++++++++++++++++++++++++++++ Source/Patches/VolumePatches.cs | 19 -------------- 3 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 Source/Patches/RenderingPatches.cs delete mode 100644 Source/Patches/VolumePatches.cs diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs index 4a79b308..c61a053c 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -35,6 +35,7 @@ public class VRSession : MonoBehaviour private bool customCameraEnabled; private Camera customCamera; private float customCameraLerpFactor; + private int cameraForceUpdateFrames; #endregion @@ -109,12 +110,14 @@ private void LateUpdate() if (!InVR) return; - if (customCameraEnabled) - { - customCamera.transform.position = MainCamera.transform.position; - customCamera.transform.rotation = Quaternion.Lerp(customCamera.transform.rotation, - MainCamera.transform.rotation, customCameraLerpFactor); - } + if (!customCameraEnabled) + return; + + customCamera.transform.position = MainCamera.transform.position; + customCamera.transform.rotation = Quaternion.Lerp(customCamera.transform.rotation, + MainCamera.transform.rotation, cameraForceUpdateFrames > 0 ? 1 : customCameraLerpFactor); + + cameraForceUpdateFrames = Math.Max(cameraForceUpdateFrames - 1, 0); } private void OnDestroy() @@ -436,11 +439,7 @@ private void InitializeCustomCamera() public void ForceUpdateCamera() { - if (!customCameraEnabled) - return; - - customCamera.transform.position = MainCamera.transform.position; - customCamera.transform.rotation = MainCamera.transform.rotation; + cameraForceUpdateFrames = 3; } public void OnEnterTerminal() diff --git a/Source/Patches/RenderingPatches.cs b/Source/Patches/RenderingPatches.cs new file mode 100644 index 00000000..9db8aced --- /dev/null +++ b/Source/Patches/RenderingPatches.cs @@ -0,0 +1,41 @@ +using HarmonyLib; +using LCVR.Managers; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.HighDefinition; + +namespace LCVR.Patches; + +[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/VolumePatches.cs b/Source/Patches/VolumePatches.cs deleted file mode 100644 index e216ea85..00000000 --- a/Source/Patches/VolumePatches.cs +++ /dev/null @@ -1,19 +0,0 @@ -using HarmonyLib; -using UnityEngine.Rendering.HighDefinition; - -namespace LCVR.Patches; - -[LCVRPatch] -[HarmonyPatch] -internal static class VolumePatches -{ - /// - /// 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; - } -} From 172f295e158137773eb8507adf9962f579ed5eea Mon Sep 17 00:00:00 2001 From: DaXcess Date: Thu, 9 Apr 2026 14:44:22 +0200 Subject: [PATCH 24/31] Fix few bugs - Fixed nutcracker following spectators after kicking them - Fixed loading screen not covering custom camera - Fixed not being visible in VR --- Source/Managers/SpectatingManager.cs | 13 ---------- Source/Managers/VRSession.cs | 9 ++++--- Source/Patches/Spectating/EnemyPatches.cs | 30 +++++++++++++++++++++++ Source/UI/VRHUD.cs | 2 +- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/Source/Managers/SpectatingManager.cs b/Source/Managers/SpectatingManager.cs index e619adb6..546a5ba5 100644 --- a/Source/Managers/SpectatingManager.cs +++ b/Source/Managers/SpectatingManager.cs @@ -134,7 +134,6 @@ internal void PlayerDeath() EnableLargeDoorCollisions(false); EnableDoorCollisions(false); EnableShipDoorCollisions(false); - SpecialFixEnemies(); // Clear spectator text HUDManager.Instance.spectatingPlayerText.text = ""; @@ -415,18 +414,6 @@ private void EnableShipDoorCollisions(bool enable) shipDoorWall.GetComponent().isTrigger = !enable; } - /// - /// Some special fixes for specific enemies - /// - private void SpecialFixEnemies() - { - // Force nutcrackers to lose aggro - var nutcrackers = FindObjectsOfType(); - foreach (var nutcracker in nutcrackers) - if (nutcracker.lastPlayerSeenMoving == (int)localPlayer.playerClientId) - nutcracker.lastPlayerSeenMoving = -1; - } - private void OnSpectateNext(InputAction.CallbackContext ctx) { if (!ctx.performed || !localPlayer.isPlayerDead || VRSession.Instance.HUD.SpectatingMenu.IsOpen) diff --git a/Source/Managers/VRSession.cs b/Source/Managers/VRSession.cs index c61a053c..b8043a3c 100644 --- a/Source/Managers/VRSession.cs +++ b/Source/Managers/VRSession.cs @@ -60,7 +60,7 @@ public class VRSession : MonoBehaviour #endregion private PauseMenuEnvironment pauseMenuEnvironment; - private GameObject helmetModel; + private MeshRenderer helmetModel; private void Awake() { @@ -139,11 +139,12 @@ private void InitializeVRSession() // Move around the volumetric plane var helmetContainer = GameObject.Find("Systems/Rendering/PlayerHUDHelmetModel"); - helmetModel = helmetContainer.Find("ScavengerHelmet"); + helmetModel = helmetContainer.Find("ScavengerHelmet").GetComponent(); helmetModel.transform.Find("Plane").SetParent(helmetContainer.transform); + helmetModel.transform.Find("ScreenHelmetGoop").localScale = new Vector3(1, 0.65f, 1); // Toggle helmet visor visibility - helmetModel.SetActive(Plugin.Config.EnableHelmetVisor.Value); + helmetModel.enabled = Plugin.Config.EnableHelmetVisor.Value; // Listen to setting change for helmet model Plugin.Config.EnableHelmetVisor.SettingChanged += HelmetVisorSettingChanged; @@ -396,7 +397,7 @@ private void InitializeVRSession() private void HelmetVisorSettingChanged(object sender, EventArgs e) { - helmetModel.SetActive(Plugin.Config.EnableHelmetVisor.Value); + helmetModel.enabled = Plugin.Config.EnableHelmetVisor.Value; } #region VR diff --git a/Source/Patches/Spectating/EnemyPatches.cs b/Source/Patches/Spectating/EnemyPatches.cs index ceac0db6..022f735f 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 /// diff --git a/Source/UI/VRHUD.cs b/Source/UI/VRHUD.cs index e3ff70f5..0ef7a966 100644 --- a/Source/UI/VRHUD.cs +++ b/Source/UI/VRHUD.cs @@ -422,7 +422,7 @@ private void Awake() loadingScreen.transform.localScale = Vector3.one; var darkenScreen = loadingScreen.Find("DarkenScreen"); - darkenScreen.transform.localScale = Vector3.one * 18; + darkenScreen.transform.localScale = Vector3.one * 28; var loadingScreenTextBg = loadingScreen.Find("TextBG").GetComponent(); loadingScreenTextBg.anchoredPosition = new Vector2(-25, -120); From e405fc49c63f67665fb4b6f3abf32c3cee4817ea Mon Sep 17 00:00:00 2001 From: DaXcess Date: Thu, 9 Apr 2026 14:46:38 +0200 Subject: [PATCH 25/31] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d31c9f8..8fd19f7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - 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 +- Made some visual changes to some assets **Removals**: - Removed dynamic resolution support as it doesn't seem to function properly with SPI rendering @@ -19,6 +20,8 @@ **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 From 227853200d0ccb3a839c96df72288b86f92fea17 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Fri, 10 Apr 2026 10:13:41 +0200 Subject: [PATCH 26/31] Multiple bug fixes --- CHANGELOG.md | 4 ++- Source/Assets/AssetManager.cs | 2 +- Source/Managers/SpectatingManager.cs | 3 +- Source/Patches/EntranceTeleportPatches.cs | 28 +++++++++++++++ Source/Patches/Rendering/MaterialPatches.cs | 34 ++++++++++++++++++ .../{ => Rendering}/RenderingPatches.cs | 2 +- .../Patches/Spectating/EnvironmentPatches.cs | 36 +++++++++++-------- Source/Physics/Interactions/ShipLever.cs | 14 +++++--- Source/Player/VRController.cs | 2 +- 9 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 Source/Patches/Rendering/MaterialPatches.cs rename Source/Patches/{ => Rendering}/RenderingPatches.cs (97%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd19f7c..05937abb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,16 @@ - 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 may break some mods +- 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**: diff --git a/Source/Assets/AssetManager.cs b/Source/Assets/AssetManager.cs index 0b364df5..ba07919f 100644 --- a/Source/Assets/AssetManager.cs +++ b/Source/Assets/AssetManager.cs @@ -7,7 +7,7 @@ namespace LCVR.Assets; public static class AssetManager { - private static AssetBundle assetsBundle; + public static AssetBundle assetsBundle; private static AssetBundle scenesBundle; public static GameObject Interactable; diff --git a/Source/Managers/SpectatingManager.cs b/Source/Managers/SpectatingManager.cs index 546a5ba5..8cbe161d 100644 --- a/Source/Managers/SpectatingManager.cs +++ b/Source/Managers/SpectatingManager.cs @@ -138,7 +138,8 @@ internal void PlayerDeath() // Clear spectator text HUDManager.Instance.spectatingPlayerText.text = ""; - // Clear fear effect + // Clear effetcs + HUDManager.Instance.SetCracksOnVisor(100); StartOfRound.Instance.fearLevel = 0; // Disable interactors diff --git a/Source/Patches/EntranceTeleportPatches.cs b/Source/Patches/EntranceTeleportPatches.cs index 02b39510..2727bc5e 100644 --- a/Source/Patches/EntranceTeleportPatches.cs +++ b/Source/Patches/EntranceTeleportPatches.cs @@ -1,6 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Emit; using HarmonyLib; using LCVR.Managers; +using static HarmonyLib.AccessTools; + namespace LCVR.Patches; [LCVRPatch] @@ -19,4 +24,27 @@ private static void OnTeleportPlayer(EntranceTeleport __instance) VRSession.Instance.LocalPlayer.TurningProvider.SetOffset(rotation); } + + /// + /// Prevent spectators from broadcasting that they're opening the main entrance doors + /// + [HarmonyPatch(typeof(EntranceTeleport), nameof(EntranceTeleport.StartOpeningEntrance))] + [HarmonyTranspiler] + private static IEnumerable SpectatorDisableDoorOpen(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, + new CodeMatch(OpCodes.Call, + Method(typeof(EntranceTeleport), nameof(EntranceTeleport.SyncStartOpeningDoorRpc)))) + .Set(OpCodes.Call, ((Action)SyncDoorIfNotDead).Method) + .InstructionEnumeration(); + + static void SyncDoorIfNotDead(EntranceTeleport entrance) + { + if (StartOfRound.Instance.localPlayerController.isPlayerDead) + return; + + entrance.SyncStartOpeningDoorRpc(); + } + } } \ No newline at end of file diff --git a/Source/Patches/Rendering/MaterialPatches.cs b/Source/Patches/Rendering/MaterialPatches.cs new file mode 100644 index 00000000..9338c1d7 --- /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/RenderingPatches.cs b/Source/Patches/Rendering/RenderingPatches.cs similarity index 97% rename from Source/Patches/RenderingPatches.cs rename to Source/Patches/Rendering/RenderingPatches.cs index 9db8aced..4a6e5df5 100644 --- a/Source/Patches/RenderingPatches.cs +++ b/Source/Patches/Rendering/RenderingPatches.cs @@ -4,7 +4,7 @@ using UnityEngine.Rendering; using UnityEngine.Rendering.HighDefinition; -namespace LCVR.Patches; +namespace LCVR.Patches.Rendering; [LCVRPatch] [HarmonyPatch] diff --git a/Source/Patches/Spectating/EnvironmentPatches.cs b/Source/Patches/Spectating/EnvironmentPatches.cs index e7c87635..6cc3af67 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/Physics/Interactions/ShipLever.cs b/Source/Physics/Interactions/ShipLever.cs index 29b56edc..22e53946 100644 --- a/Source/Physics/Interactions/ShipLever.cs +++ b/Source/Physics/Interactions/ShipLever.cs @@ -34,9 +34,10 @@ public bool OnButtonPress(VRInteractor interactor) currentInteractor = interactor; + interactor.SnapTo(transform); interactor.FingerCurler.ForceFist(true); - lever.StartInteracting(interactor.transform, ShipLever.Actor.Self); + lever.StartInteracting(interactor.TrackedController, ShipLever.Actor.Self); return true; } @@ -45,6 +46,7 @@ public void OnButtonRelease(VRInteractor interactor) { currentInteractor = null; + interactor.SnapTo(null); interactor.FingerCurler.ForceFist(false); lever.StopInteracting(); @@ -79,7 +81,7 @@ public class ShipLever : MonoBehaviour private Actor currentActor; private Channel channel; - public bool CanInteract => lever.triggerScript.interactable; + public bool CanInteract => currentActor != Actor.Other && lever.triggerScript.interactable; public bool InOrbit => lever.playersManager.inShipPhase; private void Awake() @@ -122,6 +124,10 @@ private void Update() 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) @@ -135,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() @@ -153,7 +159,7 @@ public void StopInteracting() finally { if (currentActor == Actor.Self) - channel.SendPacket([0]); + channel.SendPacket([1]); // Always reset at the end rotateTo = null; diff --git a/Source/Player/VRController.cs b/Source/Player/VRController.cs index a27a0b75..14b2e8dd 100644 --- a/Source/Player/VRController.cs +++ b/Source/Player/VRController.cs @@ -82,7 +82,7 @@ private void OnDisable() IsHovering = false; jiggleDetector.onShake -= OnJiggleDetected; - if (!PlayerController) + if (PlayerController == null) return; PlayerController.cursorIcon.enabled = false; From 734266a988c5606cd95438b9b581786164550376 Mon Sep 17 00:00:00 2001 From: Daniel <46288749+DaXcess@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:35:08 +0200 Subject: [PATCH 27/31] Update COMPATIBILITY.md --- Docs/COMPATIBILITY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Docs/COMPATIBILITY.md b/Docs/COMPATIBILITY.md index 20e54d3d..fd5c1b40 100644 --- a/Docs/COMPATIBILITY.md +++ b/Docs/COMPATIBILITY.md @@ -26,6 +26,7 @@ Even if your shaders already support SPI rendering (whether you're only using HD To tell Unity to also build in SPI support when exporting your assets, you will have to install the Unity OpenXR plugin. image + > 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. From 7f932079d41a341981d9c468e669436dfe42401e Mon Sep 17 00:00:00 2001 From: Daniel <46288749+DaXcess@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:36:34 +0200 Subject: [PATCH 28/31] Update COMPATIBILITY.md --- Docs/COMPATIBILITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/COMPATIBILITY.md b/Docs/COMPATIBILITY.md index fd5c1b40..7e9d3514 100644 --- a/Docs/COMPATIBILITY.md +++ b/Docs/COMPATIBILITY.md @@ -1,6 +1,6 @@ # Compatibility with LCVR -> This document is specifically for mods that add anything that can be rendered by the game and rendering, it is not about code or hooking into the VR mod. +> 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. From db91e8c869a4fa5dd9be7399df2d6f11dbdac908 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Fri, 10 Apr 2026 21:47:05 +0200 Subject: [PATCH 29/31] Fix spectator door and enemy interaction --- Source/Config.cs | 2 - Source/Managers/LogSharingManager.cs | 59 ----------------------- Source/Patches/EntranceTeleportPatches.cs | 23 +++++++++ Source/Patches/MenuManagerPatches.cs | 22 --------- Source/Patches/Spectating/EnemyPatches.cs | 30 ++++++++++++ Source/Plugin.cs | 1 - Source/UI/Settings/SettingsManager.cs | 2 - 7 files changed, 53 insertions(+), 86 deletions(-) delete mode 100644 Source/Managers/LogSharingManager.cs delete mode 100644 Source/Patches/MenuManagerPatches.cs diff --git a/Source/Config.cs b/Source/Config.cs index 53de8957..b1fc4c35 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -2,10 +2,8 @@ using System; using System.Collections.Generic; using System.Reflection; -using System.Runtime.InteropServices; using HarmonyLib; using Newtonsoft.Json; -using Unity.Collections.LowLevel.Unsafe; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.Rendering.HighDefinition; diff --git a/Source/Managers/LogSharingManager.cs b/Source/Managers/LogSharingManager.cs deleted file mode 100644 index cf255ed2..00000000 --- a/Source/Managers/LogSharingManager.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.IO; -using System.Net.Http; -using Steamworks; -using UnityEngine; - -namespace LCVR.Managers; - -/// -/// Automatically share log files in dev builds -/// -public class LogSharingManager : MonoBehaviour -{ - private static LogSharingManager _instance; - - private AuthTicket authTicket; - - private void Awake() - { - if (_instance != null) - { - Destroy(gameObject); - return; - } - -#if DEBUG - _instance = this; - DontDestroyOnLoad(this); - - authTicket = SteamUser.GetAuthSessionTicket(); - - Application.quitting += OnGameClosing; -#else - Destroy(gameObject); -#endif - } - - private void OnGameClosing() - { - var authData = $"{SteamClient.SteamId.Value}.{Convert.ToBase64String(authTicket.Data)}"; - var client = new HttpClient(); - client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", authData); - - var data = File.Open(Application.consoleLogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - if (data.Length > 25 * 1024 * 1024) - return; - - var buffer = new byte[data.Length]; - var read = data.Read(buffer, 0, buffer.Length); - - var formData = new MultipartFormDataContent(); - formData.Add(new ByteArrayContent(buffer[..read]), "log"); - - Logger.LogInfo("Sending log file to log collector"); - - var url = Environment.GetEnvironmentVariable("LCVR_LOGS_UPLOAD_URL") ?? "https://lcvr-logs.daxcess.io/upload"; - _ = client.PostAsync(url, formData).Result; - } -} \ No newline at end of file diff --git a/Source/Patches/EntranceTeleportPatches.cs b/Source/Patches/EntranceTeleportPatches.cs index 2727bc5e..a1f537d0 100644 --- a/Source/Patches/EntranceTeleportPatches.cs +++ b/Source/Patches/EntranceTeleportPatches.cs @@ -47,4 +47,27 @@ static void SyncDoorIfNotDead(EntranceTeleport entrance) entrance.SyncStartOpeningDoorRpc(); } } + + /// + /// Prevent spectators from broadcasting that they're opening the main entrance doors + /// + [HarmonyPatch(typeof(EntranceTeleport), nameof(EntranceTeleport.FinishOpeningEntrance))] + [HarmonyTranspiler] + private static IEnumerable SpectatorDisableDoorClose(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, + new CodeMatch(OpCodes.Call, + Method(typeof(EntranceTeleport), nameof(EntranceTeleport.SyncFinishOpeningEntranceRpc)))) + .Set(OpCodes.Call, ((Action)SyncDoorIfNotDead).Method) + .InstructionEnumeration(); + + static void SyncDoorIfNotDead(EntranceTeleport entrance) + { + if (StartOfRound.Instance.localPlayerController.isPlayerDead) + return; + + entrance.SyncFinishOpeningEntranceRpc(); + } + } } \ No newline at end of file diff --git a/Source/Patches/MenuManagerPatches.cs b/Source/Patches/MenuManagerPatches.cs deleted file mode 100644 index 89705941..00000000 --- a/Source/Patches/MenuManagerPatches.cs +++ /dev/null @@ -1,22 +0,0 @@ -#if DEBUG -using HarmonyLib; -using LCVR.Managers; -using UnityEngine; - -namespace LCVR.Patches; - -[LCVRPatch(LCVRPatchTarget.Universal)] -[HarmonyPatch] -internal static class MenuManagerPatches -{ - [HarmonyPatch(typeof(MenuManager), nameof(MenuManager.Start))] - [HarmonyPostfix] - private static void OnMenuStart() - { - if (GameNetworkManager.Instance.disableSteam) - return; - - new GameObject("LCVR Log Sharing Manager").AddComponent(); - } -} -#endif \ No newline at end of file diff --git a/Source/Patches/Spectating/EnemyPatches.cs b/Source/Patches/Spectating/EnemyPatches.cs index 022f735f..5f19dd44 100644 --- a/Source/Patches/Spectating/EnemyPatches.cs +++ b/Source/Patches/Spectating/EnemyPatches.cs @@ -178,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/Plugin.cs b/Source/Plugin.cs index 7a8af1d7..7b2a9a10 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -10,7 +10,6 @@ using System.Reflection; using UnityEngine; using UnityEngine.InputSystem; -using UnityEngine.Rendering.HighDefinition; using UnityEngine.SceneManagement; using UnityEngine.XR.Interaction.Toolkit.Inputs.Composites; using UnityEngine.XR.Interaction.Toolkit.Inputs.Interactions; diff --git a/Source/UI/Settings/SettingsManager.cs b/Source/UI/Settings/SettingsManager.cs index 7e1ec5ae..eb9807b4 100644 --- a/Source/UI/Settings/SettingsManager.cs +++ b/Source/UI/Settings/SettingsManager.cs @@ -5,8 +5,6 @@ using LCVR.Managers; using TMPro; using UnityEngine; -using UnityEngine.InputSystem; -using UnityEngine.Rendering.HighDefinition; using UnityEngine.UI; namespace LCVR.UI.Settings; From 074c5e16aacc616e035708e10116ed125e1517ba Mon Sep 17 00:00:00 2001 From: DaXcess Date: Sat, 11 Apr 2026 14:53:22 +0200 Subject: [PATCH 30/31] Fix bugs and add item sound sync --- Source/Networking/Channel.cs | 1 + Source/Patches/PlayerControllerPatches.cs | 5 +++++ Source/Physics/Interactions/ShipLever.cs | 4 ++-- Source/Player/VRController.cs | 26 +++++++++++++++++++++-- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Source/Networking/Channel.cs b/Source/Networking/Channel.cs index 329496a3..573ee53a 100644 --- a/Source/Networking/Channel.cs +++ b/Source/Networking/Channel.cs @@ -55,4 +55,5 @@ public enum ChannelType : byte Muffle, VehicleSteeringWheel, VehicleGearStick, + ItemJiggle } \ No newline at end of file diff --git a/Source/Patches/PlayerControllerPatches.cs b/Source/Patches/PlayerControllerPatches.cs index 2d267e4c..27162d54 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(-1) .SetAndAdvance(OpCodes.Call, ((Func)GetSprintValue).Method) .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)] diff --git a/Source/Physics/Interactions/ShipLever.cs b/Source/Physics/Interactions/ShipLever.cs index 22e53946..98c11b7d 100644 --- a/Source/Physics/Interactions/ShipLever.cs +++ b/Source/Physics/Interactions/ShipLever.cs @@ -9,7 +9,7 @@ namespace LCVR.Physics.Interactions; -internal class ShipLeverInteractable : MonoBehaviour, VRInteractable +public class ShipLeverInteractable : MonoBehaviour, VRInteractable { private ShipLever lever; private VRInteractor currentInteractor; @@ -34,7 +34,7 @@ public bool OnButtonPress(VRInteractor interactor) currentInteractor = interactor; - interactor.SnapTo(transform); + interactor.SnapTo(transform, rotationOffset: new Vector3(0, 0, -90)); interactor.FingerCurler.ForceFist(true); lever.StartInteracting(interactor.TrackedController, ShipLever.Actor.Self); diff --git a/Source/Player/VRController.cs b/Source/Player/VRController.cs index 14b2e8dd..c52ab650 100644 --- a/Source/Player/VRController.cs +++ b/Source/Player/VRController.cs @@ -1,11 +1,14 @@ -using GameNetcodeStuff; +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 +29,7 @@ public class VRController : MonoBehaviour private LineRenderer debugLineRenderer; private ShakeDetector jiggleDetector; + private Channel jiggleChannel; private static string CursorTip { @@ -70,17 +74,20 @@ private void Awake() 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 == null) return; @@ -450,6 +457,21 @@ private void OnJiggleDetected() if (PlayerController.currentlyHeldObjectServer is not {} item || item.isPocketed) return; - item.JiggleItemEffect(Actions.Instance.RightHandVelocity.ReadValue().magnitude); + var loudness = Actions.Instance.RightHandVelocity.ReadValue().magnitude; + item.JiggleItemEffect(loudness); + + jiggleChannel.SendPacket(BitConverter.GetBytes(loudness)); + } + + private static void OnJiggleReceived(ushort playerId, BinaryReader reader) + { + 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 From be04734d0d545632b90c08bd76b6ef47a0045f5a Mon Sep 17 00:00:00 2001 From: Daniel <46288749+DaXcess@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:49:17 +0200 Subject: [PATCH 31/31] Update README.md --- Docs/Thunderstore/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/Thunderstore/README.md b/Docs/Thunderstore/README.md index fb04afab..3370c001 100644 --- a/Docs/Thunderstore/README.md +++ b/Docs/Thunderstore/README.md @@ -237,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.