diff --git a/Andraste.Host.csproj b/Andraste.Host.csproj
index 9d3c2a7..d60bbc3 100644
--- a/Andraste.Host.csproj
+++ b/Andraste.Host.csproj
@@ -15,5 +15,7 @@
+
+
diff --git a/BindingRedirects.cs b/BindingRedirects.cs
new file mode 100644
index 0000000..c35eb41
--- /dev/null
+++ b/BindingRedirects.cs
@@ -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.
+ }
+}
\ No newline at end of file
diff --git a/CommandLine/CliEntryPoint.cs b/CommandLine/CliEntryPoint.cs
new file mode 100644
index 0000000..5a7671c
--- /dev/null
+++ b/CommandLine/CliEntryPoint.cs
@@ -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("--non-interactive", () => false,
+ "non-interactive mode: Do not redirect logging output. To be used for programmatic launches");
+
+ var fileOption = new Option("--file",
+ "The path to the application's executable")
+ {
+ IsRequired = true
+ };
+
+ var frameworkDllOption = new Option("--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("--modsJsonPath",
+ "The path to the mods.json file, that contains all necessary information");
+ var modsFolderPathOption = new Option("--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 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 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("--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("pid", "The process id to attach to")
+ {
+ IsRequired = true
+ };
+
+ ValidateSymbolResult 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
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..98cb3f6
--- /dev/null
+++ b/README.md
@@ -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.
\ No newline at end of file