Skip to content

Commit

Permalink
Merge branch 'dev/UEVR_nightly_support'
Browse files Browse the repository at this point in the history
  • Loading branch information
keton committed Feb 6, 2024
2 parents c6db3b1 + f03da22 commit d502d46
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 50 deletions.
83 changes: 46 additions & 37 deletions Chihuahua.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,17 @@ private static int ParseArgs(string[] args) {
var gameExeArgument = new Argument<FileInfo>(name: "full path to game.exe", description: "Unreal Engine executable to spawn and inject");
var verboseOption = new Option<bool>(aliases: ["--verbose", "-v"], description: "enable debug output");
var runtimeOption = new Option<RuntimeType>(name: "--runtime", getDefaultValue: () => RuntimeType.Auto, description: "VR runtime to use");
var uevrBuildOption = new Option<UEVRBuild>(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()
Expand Down Expand Up @@ -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);
Expand All @@ -136,42 +146,41 @@ 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.");
}

if (!Helpers.CheckDLLsPresent(ownExePath ?? "")) {
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.");
}
}
Expand All @@ -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);
Expand All @@ -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);
}
});
}
Expand Down
50 changes: 38 additions & 12 deletions Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -103,7 +117,7 @@ public static string FileSizeToHumanReadable(long bytes) {
return (readable / 1024).ToString("0.## ", CultureInfo.InvariantCulture) + suffix;
}

public static async Task<bool> DownloadUEVRAsync(string downloadURL, string tagName = "") {
public static async Task<bool> DownloadUEVRAsync(string downloadURL, string uevrRelease = "") {
try {
Logger.Debug($"Downloading UEVR release from: [dim white]{downloadURL}[/]");

Expand All @@ -121,7 +135,7 @@ public static async Task<bool> 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;
Expand All @@ -143,7 +157,7 @@ public static async Task<bool> 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));
Expand All @@ -167,7 +181,7 @@ public static async Task<bool> DownloadUEVRAsync(string downloadURL, string tagN
}
}

public static async Task<bool> UpdateUEVRAsync(bool forceDownload = false) {
public static async Task<bool> UpdateUEVRAsync(bool forceDownload = false, UEVRBuild uevrBuild = UEVRBuild.Release) {
try {
var uevrVersionPath = Path.Join(Path.GetDirectoryName(Environment.ProcessPath), "uevr.version");
var currentVersion = "";
Expand All @@ -176,15 +190,22 @@ public static async Task<bool> 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;
}

Expand All @@ -198,20 +219,23 @@ public static async Task<bool> 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");
Expand Down Expand Up @@ -583,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:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -138,6 +138,7 @@ Common use case would be to add `--launch-args "-nohmd -dx11"` to help with game
* `--runtime <OpenVR|OpenXR>` - 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 <Nightly|Release>` - 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

Expand Down

0 comments on commit d502d46

Please sign in to comment.