-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First draft of an alternative CliEntryPoint that serves as the refere…
…nce implementation of the Andraste.Launcher.exe, which will essentially just call into this. Game specific launchers, on the other hand, may want to build upon this entry point to do some specific things. Thus, Andraste.Host now depends on System.CommandLine. UI Applications do not need to depend on Andraste.Host anymore, though, and can just use the "CLI"
- Loading branch information
Showing
4 changed files
with
359 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
using System; | ||
using System.IO; | ||
|
||
namespace Andraste.Host | ||
{ | ||
public static class BindingRedirects | ||
{ | ||
[Obsolete("Prefer to use the variants that require specifying the path to the actual .config files")] | ||
public static void Setup(string exePath, string dllName) | ||
{ | ||
// Unfortunately, .NET FX requires us to add the config file with the bindings redirect, otherwise it fails to load assemblies. | ||
// This fails when you run the game multiple times with different .configs (or if the .config is locked by the file?), but that's a corner case. | ||
// TODO: In theory we'd need to merge files, because here, dllName.config does not containing transitive rewrites that are part in Andraste.Shared.dll.config | ||
var bindingRedirectFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dllName + ".config"); | ||
var bindingRedirectShared = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Andraste.Shared.dll.config"); | ||
if (File.Exists(bindingRedirectFile)) | ||
{ | ||
File.Copy(bindingRedirectFile, exePath + ".config", true); | ||
// For some reason, debugging has shown that sometimes, it tries to resolve the .configs in the Launcher directory. Is that dependant on the app? | ||
File.Copy(bindingRedirectFile, | ||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(exePath)) + ".config", true); | ||
//File.Copy(bindingRedirectShared, Path.Combine(Path.GetDirectoryName(exePath)!, "Andraste.Shared.dll.config"), true); | ||
} | ||
else if (File.Exists(bindingRedirectShared)) | ||
{ | ||
Console.WriteLine("Warning: Framework does not have a specific binding redirect file. Trying Andraste.Shared"); | ||
File.Copy(bindingRedirectShared, exePath + ".config", true); | ||
} | ||
else | ||
{ | ||
Console.WriteLine( | ||
$"Warning: Could not find a binding redirect file at {bindingRedirectFile}. Try to have your IDE generate one."); | ||
} | ||
} | ||
|
||
public static void CopyRedirects(string sourceFile, string frameworkPath, string applicationFile) | ||
{ | ||
File.Copy(sourceFile, Path.Combine(frameworkPath, applicationFile + ".config"), true); | ||
} | ||
|
||
// TODO: Merge XML files, but this may be non-trivial due to actual version conflicts, so rather make downstream frameworks supply the right config files. | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
using System; | ||
using System.CommandLine; | ||
using System.CommandLine.IO; | ||
using System.CommandLine.Parsing; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Andraste.Host.Logging; | ||
using Andraste.Shared.Util; | ||
|
||
#nullable enable | ||
namespace Andraste.Host.CommandLine | ||
{ | ||
public class CliEntryPoint: EntryPoint | ||
{ | ||
private readonly RootCommand _rootCommand; | ||
|
||
public CliEntryPoint() | ||
{ | ||
_rootCommand = new RootCommand("The Andraste Game Launcher Component"); | ||
BuildSubCommands(); | ||
Initialize(); | ||
} | ||
|
||
private void BuildSubCommands() | ||
{ | ||
var nonInteractiveOption = new Option<bool>("--non-interactive", () => false, | ||
"non-interactive mode: Do not redirect logging output. To be used for programmatic launches"); | ||
|
||
var fileOption = new Option<string>("--file", | ||
"The path to the application's executable") | ||
{ | ||
IsRequired = true | ||
}; | ||
|
||
var frameworkDllOption = new Option<string>("--frameworkDll", | ||
() => "Andraste.Payload.Generic.dll", | ||
"The name of the framework dll (that has to be in _this_ folder) to use") | ||
{ | ||
IsRequired = true | ||
}; | ||
|
||
var modsJsonPathOption = new Option<string>("--modsJsonPath", | ||
"The path to the mods.json file, that contains all necessary information"); | ||
var modsFolderPathOption = new Option<string>("--modsPath", | ||
"The path to the mods folder. If you can't launch by using a mods.json, this will auto-enable " + | ||
"all mods. Prefer to use --modsJsonPath where possible."); | ||
ValidateSymbolResult<CommandResult> modsValidator = result => | ||
{ | ||
if (result.Children.Count(s => s.Symbol == modsJsonPathOption || s.Symbol == modsFolderPathOption) != 1) | ||
{ | ||
result.ErrorMessage = "Either --modsJsonPath or --modsPath have to be specified"; | ||
return; | ||
} | ||
|
||
if (result.Children.Any(s => s.Symbol == modsJsonPathOption)) | ||
{ | ||
var modsJsonPath = result.GetValueForOption(modsJsonPathOption); | ||
if (!File.Exists(modsJsonPath)) | ||
{ | ||
result.ErrorMessage = $"File {modsJsonPath} does not exist!"; | ||
} | ||
} | ||
|
||
if (result.Children.Any(s => s.Symbol == modsFolderPathOption)) | ||
{ | ||
var modsFolder = result.GetValueForOption(modsFolderPathOption); | ||
if (!Directory.Exists(modsFolder)) | ||
{ | ||
result.ErrorMessage = $"Folder {modsFolder} does not exist!"; | ||
} | ||
} | ||
}; | ||
|
||
ValidateSymbolResult<CommandResult> validateFilesExist = result => | ||
{ | ||
var symbol = result.Children.FirstOrDefault(s => s.Symbol == fileOption); | ||
if (symbol != null) | ||
{ | ||
var path = result.GetValueForOption(fileOption); | ||
if (!File.Exists(path)) | ||
{ | ||
result.ErrorMessage = $"File {path} does not exist!"; | ||
} | ||
} | ||
|
||
symbol = result.Children.FirstOrDefault(s => s.Symbol == frameworkDllOption); | ||
if (symbol != null && result.GetValueForOption(frameworkDllOption) != null) | ||
{ | ||
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, | ||
result.GetValueForOption(frameworkDllOption)!); | ||
if (!File.Exists(path)) | ||
{ | ||
result.ErrorMessage = $"File {path} does not exist!"; | ||
} | ||
} | ||
}; | ||
|
||
var commandLineArgument = | ||
new Option<string>("--commandLine", "The command line to pass to the application"); | ||
var launchCommand = new Command("launch", "Launch an executable by path") | ||
{ | ||
fileOption, | ||
frameworkDllOption, | ||
modsJsonPathOption, | ||
modsFolderPathOption, | ||
commandLineArgument | ||
}; | ||
launchCommand.AddValidator(modsValidator); | ||
launchCommand.AddValidator(validateFilesExist); | ||
|
||
var monitorCommand = new Command("monitor", "Monitor an executable by path and auto-attach") | ||
{ | ||
fileOption, | ||
frameworkDllOption, | ||
modsJsonPathOption, | ||
modsFolderPathOption | ||
}; | ||
monitorCommand.AddValidator(modsValidator); | ||
monitorCommand.AddValidator(validateFilesExist); | ||
|
||
var pidOption = new Option<int>("pid", "The process id to attach to") | ||
{ | ||
IsRequired = true | ||
}; | ||
|
||
ValidateSymbolResult<CommandResult> validPidValidator = result => | ||
{ | ||
var processId = result.GetValueForOption(pidOption); | ||
try | ||
{ | ||
var proc = Process.GetProcessById(processId); | ||
} | ||
catch (Exception ex) | ||
{ | ||
result.ErrorMessage = $"Process {processId} not found! {ex}"; | ||
} | ||
}; | ||
|
||
var attachCommand = new Command("attach", "Attach to a running process") | ||
{ | ||
pidOption, | ||
frameworkDllOption, | ||
modsJsonPathOption, | ||
modsFolderPathOption | ||
}; | ||
attachCommand.AddValidator(modsValidator); | ||
attachCommand.AddValidator(validPidValidator); | ||
|
||
_rootCommand.AddCommand(launchCommand); | ||
_rootCommand.AddCommand(monitorCommand); | ||
_rootCommand.AddCommand(attachCommand); | ||
_rootCommand.AddGlobalOption(nonInteractiveOption); | ||
|
||
launchCommand.SetHandler(LaunchGame, nonInteractiveOption, fileOption, frameworkDllOption, modsJsonPathOption, modsFolderPathOption, commandLineArgument); | ||
monitorCommand.SetHandler(MonitorGame, nonInteractiveOption, fileOption, frameworkDllOption, modsJsonPathOption, modsFolderPathOption); | ||
attachCommand.SetHandler(AttachGame, nonInteractiveOption, pidOption, frameworkDllOption, modsJsonPathOption, modsFolderPathOption); | ||
} | ||
|
||
public void InvokeSync(string commandLine, IConsole? outputConsole) | ||
{ | ||
_rootCommand.Invoke(commandLine, outputConsole ?? new SystemConsole()); | ||
} | ||
|
||
public void InvokeSync(string[] commandLine, IConsole? outputConsole) { | ||
_rootCommand.Invoke(commandLine, outputConsole ?? new SystemConsole()); | ||
} | ||
|
||
// Not sure if there is a useful use case. | ||
public async Task InvokeAsync(string commandLine, IConsole? outputConsole) | ||
{ | ||
await _rootCommand.InvokeAsync(commandLine, outputConsole ?? new SystemConsole()); | ||
} | ||
|
||
protected virtual void LaunchGame(bool nonInteractive, string applicationPath, string frameworkDllName, | ||
string? modsJsonPath, string? modsFolder, string commandLine) | ||
{ | ||
var profileFolder = PreLaunch(modsJsonPath, modsFolder); | ||
// actually, we need the framework folder but with the game name? This fixes binding redirects apparently. | ||
SetupBindingRedirects(applicationPath, frameworkDllName); | ||
var process = StartApplication(applicationPath, commandLine, Path.Combine(AppDomain.CurrentDomain.BaseDirectory, frameworkDllName), profileFolder); | ||
PostLaunch(process, profileFolder, nonInteractive); | ||
} | ||
|
||
protected virtual void SetupBindingRedirects(string applicationPath, string frameworkDllName) | ||
{ | ||
var redirectFile = frameworkDllName + ".config"; | ||
if (!File.Exists(redirectFile)) | ||
{ | ||
// Fall back to the generic DLL | ||
redirectFile = "Andraste.Payload.Generic.dll.config"; | ||
} | ||
|
||
BindingRedirects.CopyRedirects(redirectFile, AppDomain.CurrentDomain.BaseDirectory, Path.GetFileName(applicationPath)); | ||
} | ||
|
||
protected virtual void MonitorGame(bool nonInteractive, string applicationPath, string frameworkDllName, | ||
string? modsJsonPath, string modsFolder) | ||
{ | ||
PreLaunch(modsJsonPath, modsFolder); | ||
SetupBindingRedirects(applicationPath, frameworkDllName); | ||
// TODO: PostLaunch | ||
//PostLaunch(); | ||
} | ||
|
||
protected virtual void AttachGame(bool nonInteractive, int pid, string frameworkDllName, | ||
string? modsJsonPath, string modsFolder) | ||
{ | ||
var profileFolder = PreLaunch(modsJsonPath, modsFolder); | ||
var process = Process.GetProcessById(pid); | ||
// TODO: Does this MainModule work? | ||
SetupBindingRedirects(process.MainModule!.FileName, frameworkDllName); | ||
PostLaunch(process, profileFolder, nonInteractive); | ||
} | ||
|
||
protected virtual string PreLaunch(string? modsJsonPath, string? modsFolder) | ||
{ | ||
if (!string.IsNullOrEmpty(modsJsonPath)) | ||
{ | ||
return Directory.GetParent(modsJsonPath)!.FullName; | ||
} | ||
|
||
if (!Directory.Exists(modsFolder)) | ||
{ | ||
Console.WriteLine("Creating \"mods\" folder"); | ||
Directory.CreateDirectory(modsFolder); | ||
} | ||
|
||
// Build the mods.json | ||
var modsJson = ModJsonBuilder.WriteModsJson(modsFolder); | ||
File.WriteAllBytes(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mods.json"), modsJson); | ||
return Directory.GetParent(modsFolder)!.FullName; | ||
|
||
} | ||
|
||
protected virtual void PostLaunch(Process? process, string modsJsonFolder, bool nonInteractive) | ||
{ | ||
if (nonInteractive) | ||
{ | ||
// return PID and terminate. We use the PID as the exit code here as well, because that's easy to read out. | ||
Console.WriteLine(process?.Id ?? -1); | ||
Environment.Exit(process?.Id ?? -1); | ||
} | ||
else | ||
{ | ||
if (process == null) | ||
{ | ||
Console.Error.WriteLine("Failed to launch the application!"); | ||
return; | ||
} | ||
|
||
Console.Title = $"Andraste Console Launcher - Attached to PID {process.Id}"; | ||
|
||
#region Logging | ||
var output = new FileLoggingHost(Path.Combine(modsJsonFolder, "output.log")); | ||
var err = new FileLoggingHost(Path.Combine(modsJsonFolder, "error.log")); | ||
output.LoggingEvent += (sender, args) => Console.WriteLine(args.Text); | ||
err.LoggingEvent += (sender, args) => Console.Error.WriteLine(args.Text); | ||
output.StartListening(); | ||
err.StartListening(); | ||
#endregion | ||
|
||
// Keep this thread (and thus the application) running | ||
process.WaitForExit(); | ||
|
||
// Dispose/Cleanup | ||
output.StopListening(); | ||
err.StopListening(); | ||
Console.WriteLine("Process exited"); | ||
} | ||
} | ||
} | ||
} | ||
#nullable restore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Andraste - The "native" C# Modding Framework | ||
|
||
> Britain/Celtic goddess of war, symbolizing invincibility | ||
The Andraste Modding Framework aims to be a solid base for those writing an | ||
in-process modding framework for native (x86, 32bit) Windows applications (Games). | ||
|
||
It is mostly the result of generalizing code that I would have written | ||
specifically for one game. Releasing it may help others to quickly re-use | ||
functionality as well as maybe contributing and reviewing decisions made here. | ||
|
||
## The Host Library | ||
The host is responsible for launching/injecting the payload into the game and | ||
communicating with it. Now due to a few circumstances, it's not actually as simple as that: | ||
- Andraste's GUI shall support loading/launching from arbitrary paths (i.e. specific | ||
versions and distributions that live in folders outside of the actual GUI), but EasyHook | ||
will always start the CLR with a `PATH` relative to the DLL that initiated the injection. | ||
This would mean, when the GUI was capable of launching, we couldn't load arbitrary distributions. | ||
- Since we want to support multiple versions and potentially even game specific frameworks, | ||
that may extend the communication between the game and the host, it'd be hard to support | ||
all from an application that has the primary goal of providing a user interface. | ||
- The idea hence is to split launching between the actual launcher component (`Andraste.Launcher.exe`), that is | ||
part of the Andraste distribution, and the actual UI (TUI/CLI, GUI). Thus, the launcher | ||
does not worry about any API stability as it can natively talk to the payload, | ||
it's just the UI<->Launcher interface that needs to be stable. | ||
- The "TUI"/CLI will most likely be that exact `Andraste.Launcher.exe`, that the UI also uses, as I don't see any | ||
point in adding yet another CLI/TUI layer with a more simple argument interface. | ||
- This, at the time of writing this ;), sounds like a good idea, especially when | ||
imagining using the generic Andraste GUI (or a CLI when launched via steam et al) | ||
while still leaving the framework distributor the choice of implementation. | ||
Some games may not like being launched directly, so the Launcher can implement | ||
specific workarounds, too. | ||
- This also leaves room for launching multiple profiles from the same UI concurrently, | ||
where every launcher monitors the relevant PID | ||
|
||
Thus, this library contains everything that is needed to build the actual Launchers, but | ||
the CLI will heavily rely on this and basically be a no-op, as the CLI Launcher will have | ||
to have the full potential and the GUI will just call the CLI Launcher that is unique to | ||
each andraste distribution. |