From 48538b3d0347f7a04761032b3a0e26d40fca1064 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Sat, 2 Mar 2024 13:46:10 +0100 Subject: [PATCH] Bunch of bugfixes - Removed verbose logging in Actions.cs - (Hopefully) fixed an issue where timings could cause players to be muffled indefinitely - Fixed centipede not dropping for any player if the host is dead (and has the mod) - (Hopefully) fixed little girl not un-targeting the host if they die - Fixed teleporter teleporting the spectator ghost player if they have no body - Disabled debug menu in Release builds - Fixed some mods causing the teleporter timer to be below 0 - Changed a tiny bit of locomotion code, potentially fixing a slight but barely noticeable jitter when walking - Fixed dynamic resolution not working - Spectators will now see the time no matter if they're in the ship or not --- Source/Experiments/Experiments.cs | 4 +- Source/Input/Actions.cs | 8 --- Source/Networking/DNet.cs | 66 ++++++++++--------- .../Physics/Interactions/TeleporterButton.cs | 6 +- Source/Player/Spectating/EnemyPatches.cs | 29 +++++++- .../Player/Spectating/EnvironmentPatches.cs | 20 +++++- Source/Player/Spectating/HUDPatches.cs | 23 +++++++ Source/Player/VRPlayer.cs | 49 +++++++------- Source/Player/VRSession.cs | 9 +-- Source/Plugin.cs | 2 +- Source/Utils.cs | 9 +++ 11 files changed, 151 insertions(+), 74 deletions(-) create mode 100644 Source/Player/Spectating/HUDPatches.cs diff --git a/Source/Experiments/Experiments.cs b/Source/Experiments/Experiments.cs index db1359fe..0b0ac6ee 100644 --- a/Source/Experiments/Experiments.cs +++ b/Source/Experiments/Experiments.cs @@ -11,7 +11,7 @@ internal class Experiments { public static void RunExperiments() { - ShowMeTheMoney(10000); + // ShowMeTheMoney(10000); // SpawnShotgun(); // SpawnBuyableItem("Jetpack"); // SpawnBuyableItem("Spray paint"); @@ -87,6 +87,7 @@ private static T SpawnObject(GameObject @object) } } +#if DEBUG [LCVRPatch(LCVRPatchTarget.Universal)] [HarmonyPatch] internal static class ExperimentalPatches @@ -103,3 +104,4 @@ private static bool DeveloperMode(ref bool __result) return false; } } +#endif \ No newline at end of file diff --git a/Source/Input/Actions.cs b/Source/Input/Actions.cs index 87b0df09..90ea7255 100644 --- a/Source/Input/Actions.cs +++ b/Source/Input/Actions.cs @@ -184,18 +184,11 @@ private bool DownloadControllerProfile(string profile, out InputActionAsset asse private InputActionAsset GetProfile(string profile) { - Logger.LogDebug(profile); - Logger.LogDebug(profiles); - - Logger.LogDebug("A"); - if (!profiles.TryGetValue(profile, out var inputAsset)) { Logger.LogWarning($"Tried to load unknown controller profile: {profile}, falling back to default"); inputAsset = profiles["default"]; } - Logger.LogDebug("B"); - // Download external profile if configured var actions = string.IsNullOrEmpty(Plugin.Config.ControllerBindingsOverrideProfile.Value) switch { @@ -206,7 +199,6 @@ private InputActionAsset GetProfile(string profile) false => inputAsset } }; - Logger.LogDebug("C"); return actions; } diff --git a/Source/Networking/DNet.cs b/Source/Networking/DNet.cs index 3b2ff1c5..a678d94a 100644 --- a/Source/Networking/DNet.cs +++ b/Source/Networking/DNet.cs @@ -13,6 +13,7 @@ using System.IO; using System.Linq; using UnityEngine; +using Object = UnityEngine.Object; namespace LCVR.Networking; @@ -23,7 +24,7 @@ public static class DNet { private const ushort PROTOCOL_VERSION = 3; - private static readonly NamedLogger Logger = new("Networking"); + private static readonly NamedLogger logger = new("Networking"); private static DissonanceComms dissonance; private static NfgoCommsNetwork network; @@ -48,7 +49,7 @@ public static IEnumerator Initialize() // Wait for voicechat connection yield return new WaitUntil(() => LocalId.HasValue); - Logger.LogDebug("Connected to Dissonance server"); + logger.LogDebug("Connected to Dissonance server"); dissonance.OnPlayerJoinedSession += OnPlayerJoinedSession; dissonance.OnPlayerLeftSession += OnPlayerLeftSession; @@ -73,6 +74,8 @@ public static void Shutdown() players.Clear(); clients.Clear(); clientByName.Clear(); + + muffledPlayers.Clear(); } public static void BroadcastRig(Rig rig) @@ -99,7 +102,7 @@ private static void SendHandshakeResponse(ushort client) { if (!clients.TryGetValue(client, out var target)) { - Logger.LogError($"Cannot send handshake response to {client}: Client info is missing!"); + logger.LogError($"Cannot send handshake response to {client}: Client info is missing!"); return; } @@ -107,7 +110,7 @@ private static void SendHandshakeResponse(ushort client) } /// - /// Continously send handshake requests to clients that have not been negotiated with yet + /// Continuously send handshake requests to clients that have not been negotiated with yet /// private static IEnumerator SendHandshakeCoroutine() { @@ -127,17 +130,17 @@ private static IEnumerator SendHandshakeCoroutine() private static void OnPlayerJoinedSession(VoicePlayerState player) { - Logger.LogDebug("Player joined, trying to resolve client info"); + logger.LogDebug("Player joined, trying to resolve client info"); if (!peers.TryGetClientInfoByName(player.Name, out var info)) { - Logger.LogError($"Failed to resolve client info for client '{player.Name}'"); + logger.LogError($"Failed to resolve client info for client '{player.Name}'"); return; } - Logger.LogDebug($"Resolved client info"); - Logger.LogDebug($"Player Name = {player.Name}"); - Logger.LogDebug($"Player Id = {info.PlayerId}"); + logger.LogDebug($"Resolved client info"); + logger.LogDebug($"Player Name = {player.Name}"); + logger.LogDebug($"Player Id = {info.PlayerId}"); clients.Add(info.PlayerId, info); clientByName.Add(player.Name, info.PlayerId); @@ -149,15 +152,17 @@ private static void OnPlayerLeftSession(VoicePlayerState player) return; if (players.TryGetValue(id, out var networkPlayer)) - GameObject.Destroy(networkPlayer); + Object.Destroy(networkPlayer); subscribers.Remove(id); players.Remove(id); clients.Remove(id); clientByName.Remove(player.Name); - Logger.LogDebug($"Player {player.Name} left the game"); - Logger.LogDebug($"subscribers = {subscribers.Count}, players = {players.Count}, clients = {clients.Count} ({string.Join(", ", clients.Keys)}), clientByNames = {clientByName.Count} ({string.Join(", ", clientByName.Keys)})"); + muffledPlayers.Remove(id); + + logger.LogDebug($"Player {player.Name} left the game"); + logger.LogDebug($"subscribers = {subscribers.Count}, players = {players.Count}, clients = {clients.Count} ({string.Join(", ", clients.Keys)}), clientByNames = {clientByName.Count} ({string.Join(", ", clientByName.Keys)})"); } #endregion @@ -166,7 +171,7 @@ private static void OnPlayerLeftSession(VoicePlayerState player) private static void BroadcastPacket(MessageType type, byte[] payload) { - var targets = subscribers.Where(key => clients.TryGetValue(key, out var value)).Select(value => clients[value]).ToList(); + var targets = subscribers.Where(key => clients.TryGetValue(key, out _)).Select(value => clients[value]).ToList(); client.SendReliableP2P(targets, ConstructPacket(type, payload)); } @@ -235,7 +240,7 @@ private static void HandleHandshakeRequest(ushort sender, ushort protocol) if (protocol != PROTOCOL_VERSION) return; - Logger.LogDebug($"Player {sender} has initiated a handshake"); + logger.LogDebug($"Player {sender} has initiated a handshake"); SendHandshakeResponse(sender); } @@ -251,15 +256,16 @@ private static IEnumerator HandleHandshakeResponse(ushort sender, bool isInVR) // Re-initialize player if already present if (players.TryGetValue(sender, out var networkPlayer)) { - GameObject.Destroy(networkPlayer); + Object.Destroy(networkPlayer); players.Remove(sender); } - yield return new WaitUntil(() => peers.TryGetClientInfoById(sender, out var client)); + // Wait until client is a part of the peers list + yield return new WaitUntil(() => peers.TryGetClientInfoById(sender, out _)); if (!peers.TryGetClientInfoById(sender, out var client)) { - Logger.LogError($"Failed to resolve client for Player Id {sender}. No VR movements will be synchronized."); + logger.LogError($"Failed to resolve client for Player Id {sender}. No VR movements will be synchronized."); yield break; } @@ -267,17 +273,17 @@ private static IEnumerator HandleHandshakeResponse(ushort sender, bool isInVR) var player = dissonance.FindPlayer(client.PlayerName); if (player == null) { - Logger.LogError($"Failed to resolve client for Player {player.Name}. No VR movements will be synchronized."); + logger.LogError($"Failed to resolve client for Player {client.PlayerName}. No VR movements will be synchronized."); yield break; } yield return new WaitUntil(() => player.Tracker != null); - var playerObject = ((NfgoPlayer)player.Tracker).gameObject; + var playerObject = ((NfgoPlayer)player.Tracker!).gameObject; var playerController = playerObject.GetComponent(); networkPlayer = playerObject.AddComponent(); - Logger.LogInfo($"Found VR player {playerController.playerUsername}"); + logger.LogInfo($"Found VR player {playerController.playerUsername}"); players.Add(sender, networkPlayer); @@ -317,11 +323,11 @@ private static void HandleInteractWithLever(ushort sender, bool started) lever.StopInteracting(); } - private static readonly List muffledPlayers = []; + private static readonly HashSet muffledPlayers = []; public static bool IsPlayerMuffled(int playerId) { - return muffledPlayers.Any(player => (int)player.PlayerController.playerClientId == playerId); + return muffledPlayers.Any(player => player == playerId); } private static void HandleSetMuffled(ushort sender, bool muffled) @@ -329,7 +335,7 @@ private static void HandleSetMuffled(ushort sender, bool muffled) if (!players.TryGetValue(sender, out var player)) return; - Logger.Log($"{player.PlayerController.playerUsername} muffled: {muffled}"); + logger.Log($"{player.PlayerController.playerUsername} muffled: {muffled}"); if (muffled) { @@ -337,11 +343,11 @@ private static void HandleSetMuffled(ushort sender, bool muffled) occlude.overridingLowPass = true; occlude.lowPassOverride = 1000f; - muffledPlayers.Add(player); + muffledPlayers.Add(sender); } else { - muffledPlayers.Remove(player); + muffledPlayers.Remove(sender); StartOfRound.Instance.UpdatePlayerVoiceEffects(); } @@ -420,10 +426,10 @@ public static Rig Deserialize(byte[] raw) { rightHandPosition = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()), rightHandEulers = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()), - rightHandFingers = Fingers.Deserialize(br.ReadBytes(Fingers.ByteCount)), + rightHandFingers = Fingers.Deserialize(br.ReadBytes(Fingers.BYTE_COUNT)), leftHandPosition = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()), leftHandEulers = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()), - leftHandFingers = Fingers.Deserialize(br.ReadBytes(Fingers.ByteCount)), + leftHandFingers = Fingers.Deserialize(br.ReadBytes(Fingers.BYTE_COUNT)), cameraEulers = new Vector3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()), cameraPosAccounted = new Vector3(br.ReadSingle(), 0, br.ReadSingle()), modelOffset = new Vector3(br.ReadSingle(), 0, br.ReadSingle()), @@ -445,7 +451,7 @@ public enum CrouchState : byte public struct Fingers { - public const int ByteCount = 5; + public const int BYTE_COUNT = 5; public float thumb; public float index; @@ -508,7 +514,7 @@ private static void ProcessReceivedPacket(ref ArraySegment data) { try { - using var stream = new MemoryStream(data.Array, data.Offset, data.Array.Length - data.Offset); + using var stream = new MemoryStream(data.Array!, data.Offset, data.Array!.Length - data.Offset); using var reader = new BinaryReader(stream); // Check magic @@ -531,8 +537,6 @@ private static void ProcessReceivedPacket(ref ArraySegment data) { Logger.LogError(ex.Message); Logger.LogError(ex.StackTrace); - - return; } } } diff --git a/Source/Physics/Interactions/TeleporterButton.cs b/Source/Physics/Interactions/TeleporterButton.cs index 85dd90e1..2cb55933 100644 --- a/Source/Physics/Interactions/TeleporterButton.cs +++ b/Source/Physics/Interactions/TeleporterButton.cs @@ -32,12 +32,14 @@ private IEnumerator timerLoop() { while (true) { - if (teleporter.cooldownTime > 0) + var cooldown = Mathf.Max(teleporter.cooldownTime, 0); + + if (cooldown > 0) timerText.color = new Color(1f, 0.1062f, 0f, 0.4314f); else timerText.color = new Color(0.1062f, 1f, 0f, 0.4314f); - timerText.text = $"{Mathf.Floor(teleporter.cooldownTime / 60f)}:{(int)teleporter.cooldownTime % 60:D2}"; + timerText.text = $"{Mathf.Floor(cooldown / 60f)}:{(int)cooldown % 60:D2}"; yield return new WaitForSeconds(1); } } diff --git a/Source/Player/Spectating/EnemyPatches.cs b/Source/Player/Spectating/EnemyPatches.cs index 77cef67c..444c7239 100644 --- a/Source/Player/Spectating/EnemyPatches.cs +++ b/Source/Player/Spectating/EnemyPatches.cs @@ -1,5 +1,10 @@ +using System.Collections.Generic; +using System.Reflection.Emit; +using GameNetcodeStuff; using HarmonyLib; using LCVR.Patches; +using Unity.Netcode; +using static HarmonyLib.AccessTools; namespace LCVR.Player.Spectating; @@ -35,7 +40,7 @@ private static bool TriggerCentipedeFall(CentipedeAI __instance) { var networkManager = __instance.NetworkManager; - if ((networkManager.IsClient || networkManager.IsHost) && StartOfRound.Instance.localPlayerController.isPlayerDead) + if ((int)__instance.__rpc_exec_stage != 1 && (networkManager.IsClient || networkManager.IsHost) && StartOfRound.Instance.localPlayerController.isPlayerDead) { __instance.triggeredFall = false; return false; @@ -43,4 +48,26 @@ private static bool TriggerCentipedeFall(CentipedeAI __instance) return true; } + + /// + /// Fix for the dress girl AI not swapping players if the host is dead and being haunted + /// + [HarmonyPatch(typeof(DressGirlAI), nameof(DressGirlAI.Update))] + [HarmonyTranspiler] + private static IEnumerable DressGirlTargetDeadPlayerFix(IEnumerable instructions, ILGenerator generator) + { + return new CodeMatcher(instructions, generator) + .MatchForward(false, new CodeMatch(OpCodes.Call, PropertyGetter(typeof(NetworkBehaviour), nameof(NetworkBehaviour.IsServer)))) + .Advance(6) + .CreateLabel(out var label) + .Advance(-1) + .SetInstructionAndAdvance(new(OpCodes.Brfalse, label)) + .InsertAndAdvance([ + new(OpCodes.Ldarg_0), + new(OpCodes.Ldfld, Field(typeof(DressGirlAI), nameof(DressGirlAI.hauntingPlayer))), + new(OpCodes.Ldfld, Field(typeof(PlayerControllerB), nameof(PlayerControllerB.isPlayerDead))), + ]) + .InsertBranch(OpCodes.Brfalse, 21) + .InstructionEnumeration(); + } } \ No newline at end of file diff --git a/Source/Player/Spectating/EnvironmentPatches.cs b/Source/Player/Spectating/EnvironmentPatches.cs index 7d887d01..bebe98b1 100644 --- a/Source/Player/Spectating/EnvironmentPatches.cs +++ b/Source/Player/Spectating/EnvironmentPatches.cs @@ -1,4 +1,5 @@ -using GameNetcodeStuff; +using System.Collections; +using GameNetcodeStuff; using HarmonyLib; using LCVR.Patches; using System.Collections.Generic; @@ -77,4 +78,21 @@ private static bool SilenceDoorTeleport(EntranceTeleport __instance, int playerO return !networkCheck || !localPlayerCheck || !localDeadCheck; } + + /// + /// Prevent dead players from being teleported if they don't have a body to teleport + /// + [HarmonyPatch(typeof(ShipTeleporter), nameof(ShipTeleporter.beamUpPlayer))] + [HarmonyPrefix] + private static bool PreventTeleportDeadPlayer(ShipTeleporter __instance, ref IEnumerator __result) + { + var target = StartOfRound.Instance.mapScreen.targetedPlayer; + if (target != StartOfRound.Instance.localPlayerController || !target.isPlayerDead || + target.deadBody is not null) + return true; + + __instance.shipTeleporterAudio.PlayOneShot(__instance.teleporterSpinSFX); + __result = Utils.NopRoutine(); + return false; + } } diff --git a/Source/Player/Spectating/HUDPatches.cs b/Source/Player/Spectating/HUDPatches.cs new file mode 100644 index 00000000..35fa0fb1 --- /dev/null +++ b/Source/Player/Spectating/HUDPatches.cs @@ -0,0 +1,23 @@ +using HarmonyLib; +using LCVR.Patches; + +namespace LCVR.Player.Spectating; + +[LCVRPatch] +[HarmonyPatch] +internal static class HUDPatches +{ + /// + /// Make sure the clock is always visible when the player is dead, unless everyone is dead + /// + [HarmonyPatch(typeof(HUDManager), nameof(HUDManager.SetClockVisible))] + [HarmonyPrefix] + private static bool DeadPlayerClockAlwaysVisible(HUDManager __instance) + { + if (!StartOfRound.Instance.localPlayerController.isPlayerDead || StartOfRound.Instance.allPlayersDead) + return true; + + __instance.Clock.targetAlpha = 1f; + return false; + } +} diff --git a/Source/Player/VRPlayer.cs b/Source/Player/VRPlayer.cs index 45b59c75..e77a1899 100644 --- a/Source/Player/VRPlayer.cs +++ b/Source/Player/VRPlayer.cs @@ -18,6 +18,10 @@ public class VRPlayer : MonoBehaviour { private const float SCALE_FACTOR = 1.5f; private const int CAMERA_CLIP_MASK = 1 << 8 | 1 << 26; + + private const float SQR_MOVE_THRESHOLD = 1E-5f; + private const float TURN_ANGLE_THRESHOLD = 120.0f; + private const float TURN_WEIGHT_SHARP = 15.0f; private PlayerControllerB playerController; private CharacterController characterController; @@ -25,19 +29,15 @@ public class VRPlayer : MonoBehaviour private Coroutine stopSprintingCoroutine; - private float cameraFloorOffset = 0f; - private float crouchOffset = 0f; + private float cameraFloorOffset; + private float crouchOffset; private float realHeight = 2.3f; - private readonly float sqrMoveThreshold = 1E-5f; - private readonly float turnAngleThreshold = 120.0f; - private readonly float turnWeightSharp = 15.0f; + private bool isSprinting; + private bool isRoomCrouching; - private bool isSprinting = false; - private bool isRoomCrouching = false; - - private bool wasInSpecialAnimation = false; - private bool wasInEnemyAnimation = false; + private bool wasInSpecialAnimation; + private bool wasInEnemyAnimation; private Vector3 specialAnimationPositionOffset = Vector3.zero; private Camera mainCamera; @@ -403,7 +403,7 @@ private void Update() } wasInSpecialAnimation = playerController.inSpecialInteractAnimation; - wasInEnemyAnimation = playerController.inAnimationWithEnemy != null; + wasInEnemyAnimation = playerController.inAnimationWithEnemy is not null; if (playerController.inSpecialInteractAnimation) totalMovementSinceLastMove = Vector3.zero; @@ -411,18 +411,21 @@ private void Update() totalMovementSinceLastMove += movementAccounted; var controllerMovement = Actions.Instance["Movement/Move"].ReadValue(); - bool moved = controllerMovement.x > 0 || controllerMovement.y > 0; - var hit = UnityEngine.Physics.OverlapBox(mainCamera.transform.position, Vector3.one * 0.1f, Quaternion.identity, CAMERA_CLIP_MASK) - .Where(c => !c.isTrigger) - .Where(c => c.transform != transform.Find("Misc/Cube")) // Idk what this cube is used for but for some reason it starts colliding if you are not the host - .Count() > 0; + var moved = controllerMovement.x > 0 || controllerMovement.y > 0; + var hit = UnityEngine.Physics + .OverlapBox(mainCamera.transform.position, Vector3.one * 0.1f, Quaternion.identity, CAMERA_CLIP_MASK) + .Any(c => !c.isTrigger && c.transform != transform.Find("Misc/Cube")); // Move player if we're not in special interact animation if (!playerController.inSpecialInteractAnimation && (totalMovementSinceLastMove.sqrMagnitude > 0.25f || hit || moved)) { - // Also move down a small amount to prevent somehow ungrounding the character controller - characterController.Move(new Vector3(totalMovementSinceLastMove.x * SCALE_FACTOR, -0.0025f, totalMovementSinceLastMove.z * SCALE_FACTOR)); + var wasGrounded = characterController.isGrounded; + + characterController.Move(new Vector3(totalMovementSinceLastMove.x * SCALE_FACTOR, 0f, totalMovementSinceLastMove.z * SCALE_FACTOR)); totalMovementSinceLastMove = Vector3.zero; + + if (!characterController.isGrounded && wasGrounded) + characterController.Move(new Vector3(0, -0.01f, 0)); } // Update rotation offset after adding movement from frame (if not in build mode) @@ -465,12 +468,12 @@ private void Update() //Logger.LogDebug($"{transform.position} {xrOrigin.position} {leftHandVRTarget.transform.position} {rightHandVRTarget.transform.position} {cameraFloorOffset} {cameraPosAccounted}"); - if ((xrOrigin.position - lastOriginPos).sqrMagnitude > sqrMoveThreshold) // player moved + if ((xrOrigin.position - lastOriginPos).sqrMagnitude > SQR_MOVE_THRESHOLD) // player moved // Rotate body sharply but still smoothly - TurnBodyToCamera(turnWeightSharp); - else if (!playerController.inSpecialInteractAnimation && GetBodyToCameraAngle() is var angle && angle > turnAngleThreshold) + TurnBodyToCamera(TURN_WEIGHT_SHARP); + else if (!playerController.inSpecialInteractAnimation && GetBodyToCameraAngle() is var angle && angle > TURN_ANGLE_THRESHOLD) // Rotate body as smoothly as possible but prevent 360 deg head twists on quick rotations - TurnBodyToCamera(turnWeightSharp * Mathf.InverseLerp(turnAngleThreshold, 170f, angle)); + TurnBodyToCamera(TURN_WEIGHT_SHARP * Mathf.InverseLerp(TURN_ANGLE_THRESHOLD, 170f, angle)); if (!playerController.inSpecialInteractAnimation) lastFrameHMDPosition = mainCamera.transform.localPosition; @@ -585,7 +588,7 @@ private float GetBodyToCameraAngle() private Transform Find(string name, bool resetLocalPosition = false) { - var transform = base.transform.Find(name); + var transform = this.transform.Find(name); if (transform == null) return null; if (resetLocalPosition) diff --git a/Source/Player/VRSession.cs b/Source/Player/VRSession.cs index 8ebc7504..53c0e58a 100644 --- a/Source/Player/VRSession.cs +++ b/Source/Player/VRSession.cs @@ -153,12 +153,9 @@ private void InitializeVRSession() #region Apply optimization configruation var hdCamera = playerGameplayCamera.GetComponent(); - - if (Plugin.Config.EnableDLSS.Value) - { - hdCamera.allowDynamicResolution = true; - hdCamera.allowDeepLearningSuperSampling = true; - } + + hdCamera.allowDynamicResolution = Plugin.Config.EnableDynamicResolution.Value; + hdCamera.allowDeepLearningSuperSampling = Plugin.Config.EnableDLSS.Value; hdCamera.DisableQualitySetting(FrameSettingsField.DepthOfField); hdCamera.DisableQualitySetting(FrameSettingsField.SSAO); diff --git a/Source/Plugin.cs b/Source/Plugin.cs index 1c1984ac..2941d340 100644 --- a/Source/Plugin.cs +++ b/Source/Plugin.cs @@ -342,7 +342,7 @@ private bool StartDisplay() { return false; } - + displays[0].Start(); Logger.LogInfo("Started XR Display subsystem, welcome to VR!"); diff --git a/Source/Utils.cs b/Source/Utils.cs index 168f6792..00cd27e3 100644 --- a/Source/Utils.cs +++ b/Source/Utils.cs @@ -9,6 +9,7 @@ using System.Security.Cryptography; using System.Text; using System; +using System.Collections; using GameNetcodeStuff; namespace LCVR; @@ -211,4 +212,12 @@ private static InputAction TrackingState(this Hand hand) _ => throw new NotImplementedException(), }; } + + /// + /// A coroutine that does nothing + /// + public static IEnumerator NopRoutine() + { + yield break; + } }