diff --git a/CHANGELOG.md b/CHANGELOG.md index b40acfb1..86edaf35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# 1.2.4 + +**Bug Fixes**: +- Fixed some of the doors on Artiface not using the new VR interactions +- Leaving the game while spectating will no longer prevent spectating to work in the next game +- Fixed some issues on the main menu when certain mods are active + +**Additions**: +- Added VR motion controls to the knife (you can now stabby stab) +- Added VR interactions to the big doors on Artiface + +**Changes**: +- Reworked the OpenXR loader, which will now attempt every runtime instead of only the default/preconfigured runtime +- Moved startup logic to a prefix, fixing an issue where occasionally the camera would be black when loading in + +**Removals**: +- Removed detection for `UnityExplorer` +- Removed ghost girl from the main/pause menus + # 1.2.3 **Bug Fixes**: diff --git a/LCVR.csproj b/LCVR.csproj index f38c8bfc..94e897c2 100644 --- a/LCVR.csproj +++ b/LCVR.csproj @@ -4,7 +4,7 @@ netstandard2.1 LCVR Collecting Scrap in VR - 1.2.3 + 1.2.4 true 12.0 LethalCompanyVR diff --git a/Resources/lethalcompanyvr b/Resources/lethalcompanyvr index 085fd07f..ea2394d6 100644 Binary files a/Resources/lethalcompanyvr and b/Resources/lethalcompanyvr differ diff --git a/Source/Assets/AssetManager.cs b/Source/Assets/AssetManager.cs index 51379305..be817343 100644 --- a/Source/Assets/AssetManager.cs +++ b/Source/Assets/AssetManager.cs @@ -49,7 +49,7 @@ public static bool LoadAssets() keyboard = assetBundle.LoadAsset("NonNativeKeyboard"); settingsPanel = assetBundle.LoadAsset("Panel"); volumeManager = assetBundle.LoadAsset("Volume Manager"); - enemyPrefab = assetBundle.LoadAsset("DressGirl"); + enemyPrefab = assetBundle.LoadAsset("Flowerman"); spectatorLight = assetBundle.LoadAsset("Spectator Light"); spectatorGhost = assetBundle.LoadAsset("SpectatorGhost"); diff --git a/Source/Config.cs b/Source/Config.cs index 51ddc59f..e04d280f 100644 --- a/Source/Config.cs +++ b/Source/Config.cs @@ -21,7 +21,7 @@ public class Config(ConfigFile file) 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(0, 100))); - public ConfigEntry EnableDLSS { get; } = file.Bind("Performance", "EnableDLSS", false, "(Not recommended!) Enable DLSS support for the game. Requires dynamic resolution to be enabled. DLSS will override the upscale filter used."); + public ConfigEntry EnableDLSS { get; } = file.Bind("Performance", "EnableDLSS", false, "[DEPRECATED] DLSS support will be removed in a future release!"); 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).", new AcceptableValueRange(0.05f, 1f))); 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."); @@ -61,6 +61,8 @@ public class Config(ConfigFile file) public ConfigEntry DisableBreakerBoxInteraction { get; } = file.Bind("Interaction", "DisableBreakerBoxInteraction", false, "Disabled needing to physically open the breaker box and flip the switches with your finger."); public ConfigEntry DisableDoorInteraction { get; } = file.Bind("Interaction", "DisableDoorInteraction", false, "Disable needing to physically open and close doors by interacting with the door handles. Will also disable the need to use keys and lockpickers physically on the door handle."); + public ConfigEntry DisableHangarLeverInteraction { get; } = file.Bind("Interaction", "DisableHangarLeverInteraction", false, "Disable needing to physically pull the lever for the big doors on Artiface"); + public ConfigEntry DisableMuffleInteraction { get; } = file.Bind("Interaction", "DisableMuffleInteraction", false, "Disables the self-muffling feature, which makes it so that holding your hand in front of your mouth will no longer make you inaudible to enemies."); public ConfigEntry DisableFaceInteractions { get; } = file.Bind("Interaction", "DisableFaceInteractions", false, "Disables the functionality to hold certain items up to your face to use them."); diff --git a/Source/EntryPoint.cs b/Source/Entrypoint.cs similarity index 52% rename from Source/EntryPoint.cs rename to Source/Entrypoint.cs index 11694956..1dd49c75 100644 --- a/Source/EntryPoint.cs +++ b/Source/Entrypoint.cs @@ -7,34 +7,12 @@ namespace LCVR; -[LCVRPatch] -[HarmonyPatch] -internal class VREntryPoint -{ - /// - /// The entrypoint for when you join a game - /// - [HarmonyPatch(typeof(StartOfRound), "Start")] - [HarmonyPostfix] - private static void OnGameEntered() - { - StartOfRound.Instance.StartCoroutine(Start()); - } - - private static IEnumerator Start() - { - Logger.Log("Hello from VR!"); - - yield return new WaitUntil(() => StartOfRound.Instance.activeCamera != null); - } -} - [LCVRPatch(LCVRPatchTarget.Universal)] [HarmonyPatch] -internal class UniversalEntryPoint +internal static class Entrypoint { - [HarmonyPatch(typeof(StartOfRound), "Start")] - [HarmonyPostfix] + [HarmonyPatch(typeof(StartOfRound), nameof(StartOfRound.Start))] + [HarmonyPrefix] private static void OnGameEntered() { StartOfRound.Instance.StartCoroutine(Start()); @@ -42,7 +20,7 @@ private static void OnGameEntered() private static IEnumerator Start() { - Logger.Log("Hello from universal!"); + Logger.Log("Hello game, I am going to initialize now!"); yield return new WaitUntil(() => StartOfRound.Instance.activeCamera != null); @@ -53,7 +31,7 @@ private static IEnumerator Start() yield return DNet.Initialize(); } - [HarmonyPatch(typeof(StartOfRound), "OnDestroy")] + [HarmonyPatch(typeof(StartOfRound), nameof(StartOfRound.OnDestroy))] [HarmonyPostfix] private static void OnGameLeave() { diff --git a/Source/Items/VRKnife.cs b/Source/Items/VRKnife.cs new file mode 100644 index 00000000..b7f64c98 --- /dev/null +++ b/Source/Items/VRKnife.cs @@ -0,0 +1,86 @@ +using System.Linq; +using LCVR.Assets; +using LCVR.Player; +using UnityEngine; + +namespace LCVR.Items; + +public class VRKnife : VRItem +{ + private GameObject interactionTarget; + private GameObject knifeCollider; + + private Vector3 previous; + private float attackTimer; + + public float Speed { get; private set; } + private Vector3 Position => VRSession.Instance.LocalPlayer.transform.InverseTransformPoint(transform.position); + + private new void Awake() + { + base.Awake(); + + if (!IsLocal) + return; + + interactionTarget = Instantiate(AssetManager.interactable, VRSession.Instance.MainCamera.transform); + interactionTarget.transform.localPosition = new Vector3(0, 0, 0.5f); + interactionTarget.transform.localScale = Vector3.one * 0.3f; + interactionTarget.AddComponent(); + interactionTarget.AddComponent().isKinematic = true; + + knifeCollider = Instantiate(AssetManager.interactable, transform); + knifeCollider.transform.localPosition = new Vector3(0, 0, 7.25f); + knifeCollider.transform.localScale = new Vector3(1.2f, 3, 12.9f); + + previous = Position; + } + + protected override void OnUpdate() + { + if (!IsLocal) + return; + + Speed = (Position - previous).magnitude / Time.deltaTime; + previous = Position; + } + + private void OnDestroy() + { + Destroy(interactionTarget); + Destroy(knifeCollider); + } + + internal void Attack() + { + if (Time.realtimeSinceStartup < attackTimer) + return; + + attackTimer = Time.realtimeSinceStartup + 0.15f; + item.ItemActivate(true); + } + + internal static RaycastHit[] GetKnifeHits(KnifeItem knife) + { + var tf = knife.transform; + + var forwardHits = UnityEngine.Physics.SphereCastAll(tf.position, 0.3f, tf.forward, 0.75f, knife.knifeMask, + QueryTriggerInteraction.Collide); + var upHits = UnityEngine.Physics.SphereCastAll(tf.position, 0.3f, -tf.up, 0.75f, knife.knifeMask, + QueryTriggerInteraction.Collide); + + RaycastHit[] allHits = [..forwardHits, ..upHits]; + + return allHits.GroupBy(x => x.collider).Select(x => x.First()).ToArray(); + } +} + +public class KnifeInteractor : MonoBehaviour +{ + private void OnTriggerEnter(Collider other) + { + var knife = other.GetComponentInParent(); + if (knife?.Speed > 6) + knife.Attack(); + } +} \ No newline at end of file diff --git a/Source/Items/VRShovelItem.cs b/Source/Items/VRShovelItem.cs index 70f2089a..a63aa221 100644 --- a/Source/Items/VRShovelItem.cs +++ b/Source/Items/VRShovelItem.cs @@ -14,10 +14,10 @@ internal class VRShovelItem : VRItem private readonly Queue positions = new(); private Vector3 lastPosition = Vector3.zero; - private bool isHitting = false; - private bool hasSwung = false; - private float lastActionTime = 0; - private float timeNotReeledUp = 0; + private bool isHitting; + private bool hasSwung; + private float lastActionTime; + private float timeNotReeledUp; private new void Awake() { diff --git a/Source/Native.cs b/Source/Native.cs index 165b7fc8..b97ae910 100644 --- a/Source/Native.cs +++ b/Source/Native.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Security; namespace LCVR; @@ -41,25 +43,59 @@ internal static class Native public static readonly IntPtr HKEY_LOCAL_MACHINE = new(0x80000002); [DllImport("Advapi32.dll", EntryPoint = "RegOpenKeyExA", CharSet = CharSet.Ansi)] - public static extern int RegOpenKeyEx(IntPtr hKey, [In] string lpSubKey, int ulOptions, int samDesired, out IntPtr phkResult); + public static extern int RegOpenKeyEx(IntPtr hKey, [In] string lpSubKey, int ulOptions, int samDesired, + out IntPtr phkResult); [DllImport("advapi32.dll", CharSet = CharSet.Ansi)] - public static extern int RegQueryValueEx(IntPtr hKey, string lpValueName, int lpReserved, out uint lpType, StringBuilder lpData, ref uint lpcbData); + public static extern int RegQueryValueEx(IntPtr hKey, string lpValueName, int lpReserved, out uint lpType, + StringBuilder lpData, ref uint lpcbData); [DllImport("advapi32.dll", CharSet = CharSet.Ansi)] - public static extern int RegQueryInfoKey(IntPtr hKey, StringBuilder lpClass, IntPtr lpcbClass, IntPtr lpReserved, out uint lpcSubKeys, out uint lpcbMaxSubKeyLen, out uint lpcbMaxClassLen, out uint lpcValues, out uint lpcbMaxValueNameLen, out uint lpcbMaxValueLen, IntPtr lpSecurityDescriptor, IntPtr lpftLastWriteTime); + public static extern int RegQueryInfoKey(IntPtr hKey, StringBuilder lpClass, IntPtr lpcbClass, IntPtr lpReserved, + out uint lpcSubKeys, out uint lpcbMaxSubKeyLen, out uint lpcbMaxClassLen, out uint lpcValues, + out uint lpcbMaxValueNameLen, out uint lpcbMaxValueLen, IntPtr lpSecurityDescriptor, IntPtr lpftLastWriteTime); [DllImport("advapi32.dll", EntryPoint = "RegEnumValueA", CharSet = CharSet.Ansi)] - public static extern int RegEnumValue(IntPtr hKey, uint dwIndex, StringBuilder lpValueName, ref uint lpcchValueName, IntPtr lpReserved, IntPtr lpType, IntPtr lpData, IntPtr lpcbData); + public static extern int RegEnumValue(IntPtr hKey, uint dwIndex, StringBuilder lpValueName, ref uint lpcchValueName, + IntPtr lpReserved, IntPtr lpType, IntPtr lpData, IntPtr lpcbData); [DllImport("advapi32.dll")] public static extern int RegCloseKey(IntPtr hKey); [DllImport("Shlwapi.dll", CharSet = CharSet.Ansi)] - public static extern int ShellMessageBox(IntPtr hAppInst, IntPtr hWnd, string lpcText, string lpcTitle, uint fuStyle); + public static extern int ShellMessageBox(IntPtr hAppInst, IntPtr hWnd, string lpcText, string lpcTitle, + uint fuStyle); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetCurrentProcess(); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool OpenProcessToken(IntPtr hProcess, uint dwAccess, out IntPtr hToken); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool GetTokenInformation(IntPtr hToken, uint tokenInformationClass, IntPtr lpData, + uint tokenInformationLength, out uint returnLength); + + [DllImport("kernel32.dll", SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + [SuppressUnmanagedCodeSecurity] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr handle); + + public static bool RegOpenSubKey(ref IntPtr hKey, string lpSubKey, int samDesired) + { + var result = RegOpenKeyEx(hKey, lpSubKey, 0, samDesired, out var hNewKey) == 0; + if (!result) + return false; + + RegCloseKey(hKey); + hKey = hNewKey; + + return true; + } private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - + private static string GetWindowText(IntPtr hWnd) { int size = GetWindowTextLength(hWnd); @@ -79,7 +115,7 @@ private static IEnumerable FindWindows(EnumWindowsProc filter) IntPtr found = IntPtr.Zero; List windows = []; - EnumWindows(delegate (IntPtr wnd, IntPtr param) + EnumWindows(delegate(IntPtr wnd, IntPtr param) { if (filter(wnd, param)) { @@ -96,7 +132,7 @@ public static void BringGameWindowToFront() { var currentPid = GetCurrentProcessId(); - var gameWindows = FindWindows(delegate (IntPtr hWnd, IntPtr lParam) + var gameWindows = FindWindows(delegate(IntPtr hWnd, IntPtr lParam) { GetWindowThreadProcessId(hWnd, out var pid); @@ -119,4 +155,30 @@ public static void BringGameWindowToFront() BringWindowToTop(targetWindow); AttachThreadInput(foregroundPid, currentThreadId, false); } + + public static bool IsElevated() + { + var hToken = IntPtr.Zero; + var data = IntPtr.Zero; + + try + { + if (!OpenProcessToken(GetCurrentProcess(), 0x0008, out hToken)) + return false; + + data = Marshal.AllocHGlobal(4); + if (!GetTokenInformation(hToken, 20, data, 4, out _)) + return false; + + return Marshal.ReadIntPtr(data).ToInt32() != 0; + } + finally + { + if (hToken != IntPtr.Zero) + CloseHandle(hToken); + + if (data != IntPtr.Zero) + Marshal.FreeHGlobal(data); + } + } } diff --git a/Source/OpenXR.cs b/Source/OpenXR.cs index a29c0e04..e1ee7b5d 100644 --- a/Source/OpenXR.cs +++ b/Source/OpenXR.cs @@ -1,208 +1,340 @@ using Newtonsoft.Json; using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Text.RegularExpressions; +using BepInEx.Logging; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; +using UnityEngine; +using UnityEngine.XR; +using UnityEngine.XR.Management; +using UnityEngine.XR.OpenXR; +using UnityEngine.XR.OpenXR.Features.Interactions; namespace LCVR; -internal class OpenXR +internal static class OpenXR { - [DllImport("UnityOpenXR", EntryPoint = "DiagnosticReport_GenerateReport")] - private static extern IntPtr Internal_GenerateReport(); - - [DllImport("UnityOpenXR", EntryPoint = "DiagnosticReport_ReleaseReport")] - private static extern void Internal_ReleaseReport(IntPtr report); - [DllImport("UnityOpenXR", EntryPoint = "NativeConfig_GetRuntimeName")] private static extern bool Internal_GetRuntimeName(out IntPtr runtimeNamePtr); [DllImport("UnityOpenXR", EntryPoint = "NativeConfig_GetRuntimeVersion")] private static extern bool Internal_GetRuntimeVersion(out ushort major, out ushort minor, out ushort patch); - public static string GenerateReport() + /// + /// Attempt to enumerate installed OpenXR runtimes as described by the OpenXR standard. + /// + public static bool GetRuntimes(out Runtimes runtimes) { - string result = ""; - IntPtr intPtr = Internal_GenerateReport(); - if (intPtr != IntPtr.Zero) + runtimes = null; + + if (Native.RegOpenKeyEx(Native.HKEY_LOCAL_MACHINE, "SOFTWARE\\Khronos\\OpenXR\\1", 0, 0x20019, out var hKey) != 0) + return false; + + var defaultRuntimePath = ""; + + var cbData = 0u; + if (Native.RegQueryValueEx(hKey, "ActiveRuntime", 0, out var type, null, ref cbData) == 0 && type == 0x1) { - result = Marshal.PtrToStringAnsi(intPtr); - Internal_ReleaseReport(intPtr); + var data = new StringBuilder((int)cbData); + if (Native.RegQueryValueEx(hKey, "ActiveRuntime", 0, out type, data, ref cbData) == 0) + defaultRuntimePath = data.ToString(); } - return result; - } + var files = new List(); + if (!Native.RegOpenSubKey(ref hKey, "AvailableRuntimes", 0x20019) || !EnumRuntimeFiles(hKey, files)) + { + // Only return the default runtime - public static bool GetDiagnosticReport(out OpenXRReport report) - { - report = null; + try + { + var runtimeInfo = JsonConvert.DeserializeObject(File.ReadAllText(defaultRuntimePath))["runtime"]; + + runtimes = new Runtimes([ + new Runtime() + { + Name = runtimeInfo?["name"]?.ToObject(), + Path = defaultRuntimePath, + Default = true + } + ]); + + return true; + } + catch + { + return false; + } + } - var sectionRegex = new Regex("^==== ([A-z0-9-_ ]+) ====$", RegexOptions.Multiline); - var errorRegex = new Regex("^\\[FAILURE\\] [A-z]+: ([A-Z_]+) \\(\\d+x\\)$"); + if (!files.Contains(defaultRuntimePath)) + files.Add(defaultRuntimePath); - string raw = GenerateReport(); + var rtList = new List(); + foreach (var file in files) + { + try + { + var runtimeInfo = JsonConvert.DeserializeObject(File.ReadAllText(file))["runtime"]; + + rtList.Add(new Runtime() + { + Name = runtimeInfo?["name"]?.ToObject(), + Path = file, + Default = file == defaultRuntimePath + }); + } + catch + { + // ignore errors + } + } - var rawSections = sectionRegex.Split(raw).Skip(1).Select(v => v.Trim()).ToArray(); - var sections = new Dictionary(); + runtimes = new Runtimes(rtList.ToArray()); - for (var i = 0; i < rawSections.Length; i += 2) - sections.Add(rawSections[i], rawSections[i + 1]); + return true; + } - if (!sections.TryGetValue("OpenXR Runtime Info", out string section)) + private static bool EnumRuntimeFiles(IntPtr hKey, List files) + { + if (Native.RegQueryInfoKey(hKey, null, IntPtr.Zero, IntPtr.Zero, out _, out _, out _, out var valueCount, + out var maxValueNameLength, out _, IntPtr.Zero, IntPtr.Zero) != 0) return false; - var lines = section.Split('\n'); + for (uint i = 0; i < valueCount; i++) + { + var valueName = new StringBuilder((int)maxValueNameLength + 1); + var cbValueName = maxValueNameLength + 1; - var runtimeName = lines.FirstOrDefault(line => line.StartsWith("Runtime Name: ")); - var runtimeVersion = lines.FirstOrDefault(line => line.StartsWith("Runtime Version: ")); + if (Native.RegEnumValue(hKey, i, valueName, ref cbValueName, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero) != 0) + continue; - if (runtimeName == default || runtimeVersion == default) - { - runtimeName = ""; - runtimeVersion = ""; - } - else - { - runtimeName = runtimeName.Split(": ")[1]; - runtimeVersion = runtimeVersion.Split(": ")[1]; + files.Add(valueName.ToString()); } - if (!sections.TryGetValue("Last 20 non-XR_SUCCESS returns", out section)) - return false; + return true; + } + + public static bool GetActiveRuntimeName(out string name) + { + name = null; - var match = errorRegex.Match(section.Split('\n')[0].Trim()); - if (match == null) + if (!Internal_GetRuntimeName(out var ptr)) return false; - var error = match.Groups[1].Value; + if (ptr == IntPtr.Zero) + return false; - report = new OpenXRReport(runtimeName, runtimeVersion, error); + name = Marshal.PtrToStringAnsi(ptr); return true; } - public static Dictionary DetectOpenXRRuntimes(out string @default) + public static bool GetActiveRuntimeVersion(out ushort major, out ushort minor, out ushort patch) { - var list = new Dictionary(); - - @default = null; + return Internal_GetRuntimeVersion(out major, out minor, out patch); + } - var hKey = IntPtr.Zero; - var cbData = 0u; + public class Runtimes(Runtime[] runtimes) : IReadOnlyCollection + { + public Runtime Default => runtimes.First(rt => rt.Default); + public int Count => runtimes.Length; - try + public bool TryGetRuntime(string name, out Runtime runtime) { - if (Native.RegOpenKeyEx(Native.HKEY_LOCAL_MACHINE, "SOFTWARE\\Khronos\\OpenXR\\1", 0, 0x20019, out hKey) != - 0) - throw new Exception("Failed to open registry key HKLM\\SOFTWARE\\Khronos\\OpenXR\\1"); + runtime = default; - if (Native.RegQueryValueEx(hKey, "ActiveRuntime", 0, out var type, null, ref cbData) != 0) - throw new Exception("Failed to query ActiveRuntime value"); + try + { + runtime = runtimes.First(rt => rt.Name == name); + return true; + } + catch + { + return false; + } + } - var data = new StringBuilder((int)cbData); + public bool TryGetRuntimeByPath(string path, out Runtime runtime) + { + runtime = default; - if (Native.RegQueryValueEx(hKey, "ActiveRuntime", 0, out type, data, ref cbData) != 0) - throw new Exception("Failed to query ActiveRuntime value"); + try + { + runtime = runtimes.First(rt => rt.Path == path); + return true; + } + catch + { + return false; + } + } - var path = data.ToString(); - @default = JsonConvert.DeserializeObject(File.ReadAllText(path)).runtime.name; + public IEnumerator GetEnumerator() + { + // ReSharper disable once NotDisposedResourceIsReturned + return ((IEnumerable)runtimes).GetEnumerator(); + } - if (Native.RegOpenKeyEx(hKey, "AvailableRuntimes", 0, 0x20019, out hKey) != 0) - throw new Exception("Failed to open AvailableRuntimes registry key"); + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } - if (Native.RegQueryInfoKey(hKey, null, IntPtr.Zero, IntPtr.Zero, out _, out _, out _, out var valueCount, - out var maxValueNameLength, out _, IntPtr.Zero, IntPtr.Zero) != 0) - throw new Exception("Failed to query AvailableRuntimes registry key"); + public struct Runtime + { + public string Name { get; set; } + public string Path { get; set; } + public bool Default { get; set; } + } - var values = new List(); + public static class Loader + { + private static XRGeneralSettings xrGeneralSettings; + private static XRManagerSettings xrManagerSettings; + private static OpenXRLoader xrLoader; + + private static readonly ManualLogSource Logger = new("OpenXR Loader"); - for (uint i = 0; i < valueCount; i++) + static Loader() + { + BepInEx.Logging.Logger.Sources.Add(Logger); + } + + public static bool InitializeXR() + { + InitializeScripts(); + + if (Native.IsElevated()) { - try - { - var valueName = new StringBuilder((int)maxValueNameLength + 1); - var cbValueName = maxValueNameLength + 1; + Logger.LogWarning( + "Application is elevated! Unable to override the XR runtime! Only the system default OpenXR runtime will be available."); - int result = Native.RegEnumValue(hKey, i, valueName, ref cbValueName, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + return InitializeXR(null); + } - if (result != 0) - continue; + if (!GetRuntimes(out var runtimes)) + { + // On failure, revert back to pre 1.2.4 behavior (Default runtime or the one specified by the config) + return InitializeXR(string.IsNullOrEmpty(Plugin.Config.OpenXRRuntimeFile.Value) + ? null + : new Runtime() + { + Name = "LCVR OpenXR Override", + Path = Plugin.Config.OpenXRRuntimeFile.Value + }); + } - values.Add(valueName.ToString()); - } - catch { } + if (!string.IsNullOrEmpty(Plugin.Config.OpenXRRuntimeFile.Value)) + { + var rtFound = runtimes.TryGetRuntimeByPath(Plugin.Config.OpenXRRuntimeFile.Value, out var rt); + + if (InitializeXR(rtFound + ? rt + : new Runtime() + { + Name = "LCVR OpenXR Override", + Path = Plugin.Config.OpenXRRuntimeFile.Value + })) + return true; + + Logger.LogWarning("Loading OpenXR using override failed, falling back to automatic enumeration..."); } - foreach (var file in values) + // Make sure the default runtime is first (unless it's the override which already failed at this point) + if (runtimes.Default.Path != Plugin.Config.OpenXRRuntimeFile.Value) { - var i = 0; - - var name = JsonConvert.DeserializeObject(File.ReadAllText(file)).runtime.name; - var resultName = name; - - while (list.ContainsKey(resultName)) - { - i++; - resultName = $"{name} ({i})"; - } - + if (InitializeXR(runtimes.Default)) + return true; + } - list.Add(name, file); + foreach (var runtime in runtimes.Where( + rt => rt.Path != Plugin.Config.OpenXRRuntimeFile.Value && !rt.Default)) + { + if (InitializeXR(runtime)) + return true; } - return list; - } - catch (Exception ex) - { - Logger.LogWarning($"Failed to query runtimes: {ex.Message}"); - return null; + Logger.LogError("All available runtimes were attempted but none worked. Aborting..."); + return false; } - finally + + public static void DeinitializeXR() { - if (hKey != IntPtr.Zero) - Native.RegCloseKey(hKey); + xrManagerSettings.DeinitializeLoader(); + xrGeneralSettings.StopXRSDK(); } - } - public static bool GetRuntimeName(out string name) - { - name = null; - - if (!Internal_GetRuntimeName(out var ptr)) - return false; - - if (ptr == IntPtr.Zero) - return false; - - name = Marshal.PtrToStringAnsi(ptr); - - return true; - } + private static bool InitializeXR(Runtime? runtime) + { + if (runtime is { } rt) + { + Logger.LogInfo($"Attempting to initialize OpenXR on {rt.Name}"); + Environment.SetEnvironmentVariable("XR_RUNTIME_JSON", rt.Path); + } + else + { + Logger.LogInfo("Attempting to initialize OpenXR using default runtime"); + Environment.SetEnvironmentVariable("XR_RUNTIME_JSON", null); + } - public static bool GetRuntimeVersion(out ushort major, out ushort minor, out ushort patch) - { - return Internal_GetRuntimeVersion(out major, out minor, out patch); - } + xrGeneralSettings.InitXRSDK(); + xrGeneralSettings.Start(); - public class OpenXRReport(string runtimeName, string runtimeVersion, string error) - { - public string RuntimeName { get; } = runtimeName; - public string RuntimeVersion { get; } = runtimeVersion; - public string Error { get; } = error; - } + var displays = new List(); + SubsystemManager.GetInstances(displays); - [Serializable] - private struct OpenXRRuntime - { - public RuntimeInfo runtime; - } + return displays.Count > 0; + } - [Serializable] - private struct RuntimeInfo - { - public string name; + private static void InitializeScripts() + { + xrGeneralSettings ??= ScriptableObject.CreateInstance(); + xrManagerSettings ??= ScriptableObject.CreateInstance(); + xrLoader ??= ScriptableObject.CreateInstance(); + + xrGeneralSettings.Manager = xrManagerSettings; + + ((List)xrManagerSettings.activeLoaders).Clear(); + ((List)xrManagerSettings.activeLoaders).Add(xrLoader); + + OpenXRSettings.Instance.renderMode = OpenXRSettings.RenderMode.MultiPass; + OpenXRSettings.Instance.depthSubmissionMode = OpenXRSettings.DepthSubmissionMode.None; + + if (OpenXRSettings.Instance.features.Length != 0) + return; + + var valveIndex = ScriptableObject.CreateInstance(); + var hpReverb = ScriptableObject.CreateInstance(); + var htcVive = ScriptableObject.CreateInstance(); + var mmController = ScriptableObject.CreateInstance(); + var khrSimple = ScriptableObject.CreateInstance(); + var metaQuestTouch = ScriptableObject.CreateInstance(); + var oculusTouch = ScriptableObject.CreateInstance(); + + valveIndex.enabled = true; + hpReverb.enabled = true; + htcVive.enabled = true; + mmController.enabled = true; + khrSimple.enabled = true; + metaQuestTouch.enabled = true; + oculusTouch.enabled = true; + + OpenXRSettings.Instance.features = + [ + valveIndex, + hpReverb, + htcVive, + mmController, + khrSimple, + metaQuestTouch, + oculusTouch + ]; + } } } diff --git a/Source/Patches/Items/KnifeItemPatches.cs b/Source/Patches/Items/KnifeItemPatches.cs new file mode 100644 index 00000000..9ab5535f --- /dev/null +++ b/Source/Patches/Items/KnifeItemPatches.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Reflection.Emit; +using HarmonyLib; +using LCVR.Items; +using UnityEngine; +using static HarmonyLib.AccessTools; + +namespace LCVR.Patches.Items; + +[LCVRPatch] +[HarmonyPatch] +internal static class KnifeItemPatches +{ + [HarmonyPatch(typeof(KnifeItem), nameof(KnifeItem.HitKnife))] + [HarmonyTranspiler] + private static IEnumerable HitKnifeVRPatch(IEnumerable instructions) + { + return new CodeMatcher(instructions) + .MatchForward(false, + [ + new CodeMatch(OpCodes.Call, + Method(typeof(UnityEngine.Physics), nameof(UnityEngine.Physics.SphereCastAll), + [ + typeof(Vector3), typeof(float), typeof(Vector3), typeof(float), typeof(int), + typeof(QueryTriggerInteraction) + ])) + ]) + .Advance(-23) + .RemoveInstructions(24) + .InsertAndAdvance( + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Call, Method(typeof(VRKnife), nameof(VRKnife.GetKnifeHits))) + ) + .InstructionEnumeration(); + } +} diff --git a/Source/Patches/Spectating/Patches.cs b/Source/Patches/Spectating/Patches.cs index 704c6037..af1da1ea 100644 --- a/Source/Patches/Spectating/Patches.cs +++ b/Source/Patches/Spectating/Patches.cs @@ -25,6 +25,16 @@ internal static class SpectatorPlayerPatches private static int lastSpectatedIndex = -1; private static bool allowSpectatorActions; + + /// + /// Initialize values when joining a new game, since this class is static and values persist across games + /// + [HarmonyPatch(typeof(StartOfRound), nameof(StartOfRound.Start))] + [HarmonyPatch] + private static void OnGameJoined() + { + isSpectating = false; + } /// /// Store some fields that need to be restored after death @@ -48,8 +58,6 @@ private static void BeforePlayerDeath(PlayerControllerB __instance) [HarmonyPostfix] private static void OnPlayerDeath(PlayerControllerB __instance) { - Logger.LogDebug($"{__instance.IsOwner}, {isSpectating}, {__instance.AllowPlayerDeath()}"); - if (!__instance.IsOwner || isSpectating || !__instance.AllowPlayerDeath()) return; diff --git a/Source/Patches/UIPatches.cs b/Source/Patches/UIPatches.cs index d19c1090..19a3de2d 100644 --- a/Source/Patches/UIPatches.cs +++ b/Source/Patches/UIPatches.cs @@ -22,34 +22,19 @@ internal static class UIPatches /// [HarmonyPatch(typeof(PreInitSceneScript), nameof(PreInitSceneScript.Start))] [HarmonyPostfix] - private static void OnPreInitMenuShown() + private static void OnPreInitMenuShown(PreInitSceneScript __instance) { - InitMenuScene(); + var canvas = __instance.launchSettingsPanelsContainer.GetComponentInParent(); - var canvas = GameObject.Find("Canvas"); - - if (Plugin.Flags.HasFlag(Flags.UnityExplorerDetected)) - { - var textObject = Object.Instantiate(canvas.Find("GameObject/LANOrOnline/OnlineButton/Text (TMP) (1)")); - var text = textObject.GetComponent(); - - text.transform.parent = canvas.Find("GameObject").transform; - text.transform.localPosition = new Vector3(200, -100, 0); - text.transform.localScale = Vector3.one; - text.text = "Unity Explorer Detected!\nUI controls are most likely nonfunctional!"; - text.autoSizeTextContainer = true; - text.color = new Color(0.9434f, 0.9434f, 0.0434f, 1); - text.alignment = TextAlignmentOptions.Center; - text.fontSize = 18; - text.raycastTarget = false; - } + InitMenuScene(canvas); if (Plugin.Flags.HasFlag(Flags.InvalidGameAssembly)) { - var textObject = Object.Instantiate(canvas.Find("GameObject/LANOrOnline/OnlineButton/Text (TMP) (1)")); + var textObject = + Object.Instantiate(canvas.gameObject.Find("GameObject/LANOrOnline/OnlineButton/Text (TMP) (1)")); var text = textObject.GetComponent(); - text.transform.parent = canvas.Find("GameObject").transform; + text.transform.parent = __instance.launchSettingsPanelsContainer.transform; text.transform.localPosition = new Vector3(200, -30, 0); text.transform.localScale = Vector3.one; text.text = "Invalid Game Assembly Detected!\nYou are using an unsupported version of the game!"; @@ -68,25 +53,24 @@ private static void OnPreInitMenuShown() [HarmonyPrefix] private static void OnMainMenuShown(MenuManager __instance) { - InitMenuScene(); + var canvas = __instance.menuButtons.GetComponentInParent(); + + InitMenuScene(canvas); + + if (Plugin.Compatibility.IsLoaded("MoreCompany")) + Compatibility.MoreCompany.MoreCompanyCompatibility.SetupMoreCompanyUI(); if (__instance.isInitScene) return; - DisableKeybindsSetting(); - if (!Plugin.Config.IntroScreenSeen.Value) InjectIntroScreen(); - if (Plugin.Compatibility.IsLoaded("MoreCompany")) - Compatibility.MoreCompany.MoreCompanyCompatibility.SetupMoreCompanyUI(); - - InitializeKeyboard(); + InitializeKeyboard(canvas); } - private static void InitMenuScene() + private static void InitMenuScene(Canvas canvas) { - var canvas = GameObject.Find("Canvas")?.GetComponent(); var input = GameObject.Find("EventSystem")?.GetComponent(); if (input != null) @@ -135,23 +119,11 @@ private static void InitMenuScene() rightControllerInteractor.rayOriginTransform.localRotation = Quaternion.Euler(60, 347, 270); } - private static void DisableKeybindsSetting() - { - var menuContainer = GameObject.Find("MenuContainer"); - var keybindingsButton = menuContainer.Find("SettingsPanel/KeybindingsButton")?.GetComponent