From 8f06658c69bd2b430da0446f598ae0fbcee232fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Lower?= Date: Tue, 6 Feb 2024 16:40:14 +0100 Subject: [PATCH 1/3] Add support for UEVR nightly auto updates --- Chihuahua.cs | 83 +++++++++++++++++++++++++++++----------------------- Helpers.cs | 48 ++++++++++++++++++++++-------- 2 files changed, 82 insertions(+), 49 deletions(-) diff --git a/Chihuahua.cs b/Chihuahua.cs index 1fb2a3c..3039399 100644 --- a/Chihuahua.cs +++ b/Chihuahua.cs @@ -51,15 +51,17 @@ private static int ParseArgs(string[] args) { var gameExeArgument = new Argument(name: "full path to game.exe", description: "Unreal Engine executable to spawn and inject"); var verboseOption = new Option(aliases: ["--verbose", "-v"], description: "enable debug output"); var runtimeOption = new Option(name: "--runtime", getDefaultValue: () => RuntimeType.Auto, description: "VR runtime to use"); + var uevrBuildOption = new Option(name: "--uevr-build", getDefaultValue: () => UEVRBuild.Release, description: "UEVR build to use for auto updates"); var rootCommand = new RootCommand("No frills UEVR injector. Chihuahua goes where bigger dogs won't.") { - delayOption, - launchCmdOption, - launchCmdArgsOption, - gameExeArgument, - verboseOption, - runtimeOption, - }; + delayOption, + launchCmdOption, + launchCmdArgsOption, + gameExeArgument, + verboseOption, + runtimeOption, + uevrBuildOption, + }; var parser = new CommandLineBuilder(rootCommand) .UseDefaults() @@ -103,21 +105,29 @@ private static int ParseArgs(string[] args) { }) .Build(); - rootCommand.SetHandler(async (gameExe, delay, launchCmd, launchCmdArgs, verbose, runtime) => { + rootCommand.SetHandler(async (gameExe, delay, launchCmd, launchCmdArgs, verbose, runtime, uevrBuild) => { Logger.verbose = verbose; if (!gameExe.Exists) { Helpers.ExitWithMessage($"Game executable: \"{gameExe.FullName}\" not found."); } - await RunAndInject(gameExe.FullName, launchCmd, launchCmdArgs, delay, runtime); + await RunAndInject(new LaunchOptions { + gameExe = gameExe.FullName, + launchCmd = launchCmd, + launchCmdArgs = launchCmdArgs, + injectionDelayS = delay, + runtime = runtime, + uevrBuild = uevrBuild + }); }, gameExeArgument, delayOption, launchCmdOption, launchCmdArgsOption, verboseOption, - runtimeOption + runtimeOption, + uevrBuildOption ); return parser.Invoke(args); @@ -136,13 +146,12 @@ private static int Main(string[] args) { return 0; } - private static async Task RunAndInject(string gameExe, string? launchCmd, string? launchCmdArgs, int injectionDelayS, - RuntimeType runtime = RuntimeType.Auto, int waitForGameTimeoutS = 60) { + private static async Task RunAndInject(LaunchOptions options, int waitForGameTimeoutS = 60) { var ownExePath = Path.GetDirectoryName(Environment.ProcessPath); if (!Helpers.CheckDLLsPresent(ownExePath ?? "")) { Logger.Info("Attempting to download missing files..."); - if (await Helpers.UpdateUEVRAsync(forceDownload: true) == false) { + if (await Helpers.UpdateUEVRAsync(forceDownload: true, uevrBuild: options.uevrBuild) == false) { Helpers.ExitWithMessage($"UEVR download failed."); } @@ -150,28 +159,28 @@ private static async Task RunAndInject(string gameExe, string? launchCmd, string Helpers.ExitWithMessage($"Files still missing after download, you may want to add [dim]{ownExePath}[/] to your antivirus exceptions"); } } else { - if (await Helpers.UpdateUEVRAsync() == false) { + if (await Helpers.UpdateUEVRAsync(uevrBuild: options.uevrBuild) == false) { Logger.Warn("Failed to check UEVR updates"); } } - var mainGameExe = Helpers.TryFindMainExecutable(gameExe); + var mainGameExe = Helpers.TryFindMainExecutable(options.gameExe); if (mainGameExe == null) { - Helpers.ExitWithMessage($"[dim]{Path.GetFileName(gameExe)}[/] does not look like UE game executable and no suitable candidate was found."); + Helpers.ExitWithMessage($"[dim]{Path.GetFileName(options.gameExe)}[/] does not look like UE game executable and no suitable candidate was found."); } - if (mainGameExe != gameExe) { - Logger.Debug($"Detected [dim white]{mainGameExe}[/] as main executable for [dim white]{gameExe}[/]"); + if (mainGameExe != options.gameExe) { + Logger.Debug($"Detected [dim white]{mainGameExe}[/] as main executable for [dim white]{options.gameExe}[/]"); } - gameExe = mainGameExe; + options.gameExe = mainGameExe; - Helpers.RemoveUnwantedPlugins(gameExe); + Helpers.RemoveUnwantedPlugins(options.gameExe); - if (runtime == RuntimeType.Auto) { - runtime = Helpers.DetectRuntimeType(); + if (options.runtime == RuntimeType.Auto) { + options.runtime = Helpers.DetectRuntimeType(); - if (runtime == RuntimeType.Auto) { + if (options.runtime == RuntimeType.Auto) { Helpers.ExitWithMessage("Failed to detect VR runtime type. You can set it manually with --runtime command line parameter."); } } @@ -182,43 +191,43 @@ await AnsiConsole.Status() .StartAsync("About to start game...", async ctx => { Process.Start(new ProcessStartInfo { - FileName = launchCmd ?? gameExe, - Arguments = launchCmdArgs ?? "", + FileName = options.launchCmd ?? options.gameExe, + Arguments = options.launchCmdArgs ?? "", UseShellExecute = true, }); - Logger.Info($"Waiting for [dim]{Path.GetFileName(gameExe)}[/] to spawn"); + Logger.Info($"Waiting for [dim]{Path.GetFileName(options.gameExe)}[/] to spawn"); ctx.Status("Launching game..."); - if (await Helpers.WaitForGameProcessAsync(gameExe, waitForGameTimeoutS)) { + if (await Helpers.WaitForGameProcessAsync(options.gameExe, waitForGameTimeoutS)) { Logger.Debug("Game process found"); } else { Helpers.ExitWithMessage(ctx, "Timed out while waiting for game process"); } - Helpers.focusGameWindow(gameExe); + Helpers.focusGameWindow(options.gameExe); - ConsoleCtrlEventArgs.gameExe = gameExe; + ConsoleCtrlEventArgs.gameExe = options.gameExe; if (SetConsoleCtrlHandler(ConsoleCloseHandler, true) == false) { Helpers.ExitWithMessage(ctx, "Failed to attach console close handler."); } - Logger.Info($"Waiting [dim]{injectionDelayS}s[/] before injection."); - await Helpers.WaitBeforeInjectionAsync(ctx, injectionDelayS); + Logger.Info($"Waiting [dim]{options.injectionDelayS}s[/] before injection."); + await Helpers.WaitBeforeInjectionAsync(ctx, options.injectionDelayS); - var mainGameProcess = Helpers.GetMainGameProcess(gameExe); + var mainGameProcess = Helpers.GetMainGameProcess(options.gameExe); if (mainGameProcess == null) { - Helpers.ExitWithMessage(ctx, $"[dim]{Path.GetFileName(gameExe)}[/] exited before it could be injected."); + Helpers.ExitWithMessage(ctx, $"[dim]{Path.GetFileName(options.gameExe)}[/] exited before it could be injected."); } Helpers.NullifyPlugins(mainGameProcess.Id); - Helpers.InjectRuntime(mainGameProcess.Id, runtime); + Helpers.InjectRuntime(mainGameProcess.Id, options.runtime); Helpers.InjectDll(mainGameProcess.Id, "UEVRBackend.dll"); Logger.Info("Injection done, close this window to kill game process."); ctx.Status("[green]Game injected and running, close this window to kill game process...[/]"); - Helpers.focusGameWindow(gameExe); + Helpers.focusGameWindow(options.gameExe); while (Helpers.IsProcessRunning(mainGameProcess.Id)) { await Task.Delay(100); @@ -227,9 +236,9 @@ await AnsiConsole.Status() Logger.Info("Game has exited."); ctx.Status("Cleaning up before exit..."); - if (Helpers.GetGameProcesses(gameExe).Length > 0) { + if (Helpers.GetGameProcesses(options.gameExe).Length > 0) { Logger.Debug("Leftover game process detected. Terminating..."); - Helpers.TryCloseGame(gameExe); + Helpers.TryCloseGame(options.gameExe); } }); } diff --git a/Helpers.cs b/Helpers.cs index e6f87b5..92d9165 100644 --- a/Helpers.cs +++ b/Helpers.cs @@ -19,6 +19,20 @@ internal enum RuntimeType { Auto } + internal enum UEVRBuild { + Release, + Nightly + } + + internal struct LaunchOptions { + public string gameExe; + public string? launchCmd; + public string? launchCmdArgs; + public int injectionDelayS; + public RuntimeType runtime; + public UEVRBuild uevrBuild; + }; + internal static class Helpers { private static readonly string[] uevr_dlls = [ @@ -103,7 +117,7 @@ public static string FileSizeToHumanReadable(long bytes) { return (readable / 1024).ToString("0.## ", CultureInfo.InvariantCulture) + suffix; } - public static async Task DownloadUEVRAsync(string downloadURL, string tagName = "") { + public static async Task DownloadUEVRAsync(string downloadURL, string uevrRelease = "") { try { Logger.Debug($"Downloading UEVR release from: [dim white]{downloadURL}[/]"); @@ -121,7 +135,7 @@ public static async Task DownloadUEVRAsync(string downloadURL, string tagN var handler = new HttpClientHandler() { AllowAutoRedirect = true }; var ph = new ProgressMessageHandler(handler); - var downloadTask = ctx.AddTask($"Downloading UEVR {tagName}"); + var downloadTask = ctx.AddTask($"Downloading {uevrRelease}"); long lastBytesTransferred = 0; @@ -143,7 +157,7 @@ public static async Task DownloadUEVRAsync(string downloadURL, string tagN downloadTask.StopTask(); - var unpackTask = ctx.AddTask($"Unpacking UEVR {tagName}"); + var unpackTask = ctx.AddTask($"Unpacking {uevrRelease}"); unpackTask.MaxValue = uevr_dlls.Length; var filesToUnpack = zipArchive.Entries.Where(entry => uevr_dlls.Contains(entry.Name)); @@ -167,7 +181,7 @@ public static async Task DownloadUEVRAsync(string downloadURL, string tagN } } - public static async Task UpdateUEVRAsync(bool forceDownload = false) { + public static async Task UpdateUEVRAsync(bool forceDownload = false, UEVRBuild uevrBuild = UEVRBuild.Release) { try { var uevrVersionPath = Path.Join(Path.GetDirectoryName(Environment.ProcessPath), "uevr.version"); var currentVersion = ""; @@ -176,15 +190,22 @@ public static async Task UpdateUEVRAsync(bool forceDownload = false) { currentVersion = File.ReadAllLines(uevrVersionPath)[0].Trim().Replace("\n", ""); } catch (Exception) { } - var releases = await new GitHubClient(RequestAdapter.Create(new AnonymousAuthenticationProvider())).Repos["praydog"]["UEVR"].Releases.GetAsync(); + var repoOwner = "praydog"; + var repoName = "UEVR"; + + if (uevrBuild == UEVRBuild.Nightly) { + repoName = "UEVR-nightly"; + } + + var releases = await new GitHubClient(RequestAdapter.Create(new AnonymousAuthenticationProvider())).Repos[repoOwner][repoName].Releases.GetAsync(); if (releases == null) { - Logger.Error("Github responded with empty UEVR releases"); + Logger.Error($"{repoOwner}/{repoName} responded with empty UEVR releases"); return false; } var latestRelease = releases.First(); if (latestRelease.Assets == null) { - Logger.Error("Latest UEVR release does not contain any assets"); + Logger.Error($"Latest {repoOwner}/{repoName} release does not contain any assets"); return false; } @@ -198,20 +219,23 @@ public static async Task UpdateUEVRAsync(bool forceDownload = false) { Logger.Debug("Forced update"); } - Logger.Debug($"Latest version: {latestRelease.TagName}"); + Logger.Debug($"Latest {repoOwner}/{repoName} version: {latestRelease.TagName}"); // don't update if version file is missing. Allows manual unpacking UEVR to chihuahua directory if (forceDownload || ((currentVersion != latestRelease.TagName) && (currentVersion != ""))) { - Logger.Info($"Updating UEVR to {latestRelease.TagName}"); + Logger.Info($"Updating {repoOwner}/{repoName} to {latestRelease.TagName}"); - var uevrAssets = latestRelease.Assets.Where(asset => asset.Name == "UEVR.zip"); + // nightly release artifact is named in lower case + var uevrAssets = latestRelease.Assets.Where(asset => String.Equals(asset.Name, "UEVR.zip", StringComparison.OrdinalIgnoreCase)); if (uevrAssets.Count() != 1) { - Logger.Error($"[dim]UEVR.zip[/] asset not found in UEVR release {latestRelease.TagName}. Download and unpack files manually."); + Logger.Error($"[dim]UEVR.zip[/] asset not found in {repoOwner}/{repoName} release {latestRelease.TagName}. Download and unpack files manually."); return false; } - var downloadSucceeded = await DownloadUEVRAsync(uevrAssets.First().BrowserDownloadUrl ?? "", latestRelease.TagName ?? ""); + var releaseName = string.Format("{0}/{1} {2}", repoOwner, repoName, (uevrBuild == UEVRBuild.Release) ? latestRelease.TagName : latestRelease.TagName?[^6..]); + + var downloadSucceeded = await DownloadUEVRAsync(uevrAssets.First().BrowserDownloadUrl ?? "", releaseName); if (downloadSucceeded) { File.WriteAllText(uevrVersionPath, latestRelease.TagName + "\n"); From a5431b706332cf64444dad0a6d6e1f639d72ac67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Lower?= Date: Tue, 6 Feb 2024 16:44:06 +0100 Subject: [PATCH 2/3] Log VR runtime used --- Helpers.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Helpers.cs b/Helpers.cs index 92d9165..739f4ca 100644 --- a/Helpers.cs +++ b/Helpers.cs @@ -607,9 +607,11 @@ public static RuntimeType DetectRuntimeType() { public static void InjectRuntime(int mainGameProcessId, RuntimeType runtime = RuntimeType.OpenXR) { switch (runtime) { case RuntimeType.OpenXR: + Logger.Info("Using [dim]OpenXR[/] runtime"); InjectDll(mainGameProcessId, "openxr_loader.dll"); break; case RuntimeType.OpenVR: + Logger.Info("Using [dim]OpenVR[/] runtime"); InjectDll(mainGameProcessId, "openvr_api.dll"); break; case RuntimeType.Auto: From f03da2270aecd36c98714e7e0e64e0188b496aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Lower?= Date: Tue, 6 Feb 2024 16:47:15 +0100 Subject: [PATCH 3/3] Update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ebc8a6..5fb91b0 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Chihuahua aims to solve this, making whole process as easy as dragging and dropp Compared to injector bundled with [UEVR](https://www.patreon.com/praydog) Chihuahua: * makes all the decisions for the user -* automatically downloads and keeps UEVR up to date +* automatically downloads and keeps UEVR up to date (now with Nightly build support) * deals with certain things that can make injection fail (choosing right executable, selecting correct VR runtime, disabling unwanted UE plugins, adding pre injection delay) * cleans up leftover processes when game exits. This allows cloud saves to upload properly * aims to provide easy to understand message about what's going on and especially what went wrong @@ -138,6 +138,7 @@ Common use case would be to add `--launch-args "-nohmd -dx11"` to help with game * `--runtime ` - override runtime autodetection, useful if process fails. `OpenVR` requires SteamVR to be installed. `OpenXR` requires OpenXR runtime to be set correctly in your HMD software (probably it is unless you have multiple HMDs/VR streaming apps). +* `--uevr-build ` - override UEVR version for auto updater. Release builds are used by default. Nightly builds may be less stable but contain the latest features. ### Game launch shortcut