namespace Kiseki.Launcher.Windows; using System.Diagnostics; using System.Net; using System.Reflection; using Kiseki.Launcher.Helpers; using Kiseki.Launcher.Models; using Microsoft.Win32; using Syroot.Windows.IO; public class Bootstrapper : Interfaces.IBootstrapper { public readonly static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; private readonly string Payload; private readonly Dictionary Arguments = new(); public event EventHandler? OnHeadingChange; public event EventHandler? OnProgressBarSet; public event EventHandler? OnProgressBarStateChange; public event EventHandler? OnError; public Bootstrapper(string payload) { Payload = payload; } public bool Initialize() { if (!Helpers.Base64.IsBase64String(Payload)) { return false; } // { mode, version, ticket, joinscript } string[] pieces = Helpers.Base64.ConvertBase64ToString(Payload).Split("|"); if (pieces.Length != 4) { return false; } Arguments["Mode"] = pieces[0]; Arguments["Version"] = pieces[1]; Arguments["Ticket"] = pieces[2]; Arguments["JoinScript"] = pieces[3]; return true; } public async void Run() { // Check for updates HeadingChange("Checking for updates..."); // Check for a new launcher release from GitHub var release = await Http.GetJson($"https://api.github.com/repos/{Constants.PROJECT_REPOSITORY}/releases/latest"); bool launcherUpToDate = true; // TODO: We can remove this check once we do our first release. if (release is not null && release.Assets is not null) { launcherUpToDate = Version == release.TagName[1..]; if (!launcherUpToDate) { // Update the launcher HeadingChange("Getting the latest launcher..."); ProgressBarStateChange(Enums.ProgressBarState.Normal); // TODO: This needs to be rewritten. It's a mess. // REF: https://stackoverflow.com/a/9459441 Thread thread = new(() => { using WebClient client = new(); client.DownloadProgressChanged += (_, e) => { double bytesIn = double.Parse(e.BytesReceived.ToString()); double totalBytes = double.Parse(e.TotalBytesToReceive.ToString()); double percentage = bytesIn / totalBytes * 100; ProgressBarSet(int.Parse(Math.Truncate(percentage).ToString())); }; client.DownloadFileCompleted += (_, _) => { HeadingChange("Installing the latest launcher..."); ProgressBarStateChange(Enums.ProgressBarState.Marquee); // Rename Kiseki.Launcher.exe.new -> Kiseki.Launcher.exe, and launch it with our payload string command = $"del /Q \"{Paths.Application}\" && move /Y \"{Paths.Application}.new\" \"{Paths.Application}\" && start \"\" \"{Paths.Application}\" {Payload}"; Process.Start(new ProcessStartInfo() { FileName = "cmd.exe", Arguments = $"/c timeout 1 && {command}", UseShellExecute = true, WindowStyle = ProcessWindowStyle.Hidden }); Environment.Exit((int)Win32.ErrorCode.ERROR_SUCCESS); }; client.DownloadFileAsync(new Uri(release.Assets[0].BrowserDownloadUrl), $"{Paths.Application}.new"); }); thread.Start(); return; } } } public void Abort() { // } #region MainWindow protected virtual void HeadingChange(string heading) { OnHeadingChange!.Invoke(this, heading); } protected virtual void ProgressBarSet(int value) { OnProgressBarSet!.Invoke(this, value); } protected virtual void ProgressBarStateChange(Enums.ProgressBarState state) { OnProgressBarStateChange!.Invoke(this, state); } protected virtual void Error(string heading, string text) { // Ugly hack for now (I don't want to derive EventHandler just for this) OnError!.Invoke(this, new string[] { heading, text }); } #endregion #region Installation public static void Install() { // Cleanup our registry entries beforehand (if they even exist) Protocol.Unregister(); Register(); // Create paths Directory.CreateDirectory(Paths.Base); Directory.CreateDirectory(Paths.Versions); Directory.CreateDirectory(Paths.Logs); // Copy ourselves if (!File.Exists(Paths.Application)) File.Copy(Application.ExecutablePath, Paths.Application, true); // Register us and our protocol handler system-wide Register(); Protocol.Register(); // Create shortcuts if (File.Exists(Path.Combine(Paths.StartMenu, $"{Constants.PROJECT_NAME}.lnk"))) File.Delete(Path.Combine(Paths.StartMenu, $"{Constants.PROJECT_NAME}.lnk")); if (File.Exists(Path.Combine(Paths.Desktop, $"{Constants.PROJECT_NAME}.lnk"))) File.Delete(Path.Combine(Paths.Desktop, $"{Constants.PROJECT_NAME}.lnk")); ShellLink.Shortcut.CreateShortcut(Paths.Application, "", Paths.Application, 0).WriteToFile(Path.Combine(Paths.StartMenu, $"{Constants.PROJECT_NAME}.lnk")); ShellLink.Shortcut.CreateShortcut(Paths.Application, "", Paths.Application, 0).WriteToFile(Path.Combine(Paths.Desktop, $"{Constants.PROJECT_NAME}.lnk")); // We're finished MessageBox.Show($"Sucessfully installed {Constants.PROJECT_NAME}!", Constants.PROJECT_NAME, MessageBoxButtons.OK, MessageBoxIcon.Information); Environment.Exit((int)Win32.ErrorCode.ERROR_SUCCESS); } public static void Uninstall(bool quiet = false) { DialogResult answer = quiet ? DialogResult.Yes : MessageBox.Show($"Are you sure you want to uninstall {Constants.PROJECT_NAME}?", Constants.PROJECT_NAME, MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (answer != DialogResult.Yes) Environment.Exit((int)Win32.ErrorCode.ERROR_CANCELLED); // Close active processes if (Process.GetProcessesByName($"{Constants.PROJECT_NAME}.Player").Any() || Process.GetProcessesByName($"{Constants.PROJECT_NAME}.Studio").Any()) { answer = quiet ? DialogResult.Yes : MessageBox.Show($"Kiseki is currently running. Would you like to close Kiseki now?", Constants.PROJECT_NAME, MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (answer != DialogResult.Yes) Environment.Exit((int)Win32.ErrorCode.ERROR_CANCELLED); try { foreach (Process process in Process.GetProcessesByName($"{Constants.PROJECT_NAME}.Player")) process.Kill(); foreach (Process process in Process.GetProcessesByName($"{Constants.PROJECT_NAME}.Studio")) process.Kill(); } catch { Environment.Exit((int)Win32.ErrorCode.ERROR_INTERNAL_ERROR); } } // Delete all files if (Directory.Exists(Paths.Logs)) Directory.Delete(Paths.Logs, true); if (Directory.Exists(Paths.Versions)) Directory.Delete(Paths.Versions, true); if (File.Exists(Paths.License)) File.Delete(Paths.License); // Delete our shortcuts if (File.Exists(Path.Combine(Paths.StartMenu, $"{Constants.PROJECT_NAME}.lnk"))) File.Delete(Path.Combine(Paths.StartMenu, $"{Constants.PROJECT_NAME}.lnk")); if (File.Exists(Path.Combine(Paths.Desktop, $"{Constants.PROJECT_NAME}.lnk"))) File.Delete(Path.Combine(Paths.Desktop, $"{Constants.PROJECT_NAME}.lnk")); // Cleanup our registry entries Unregister(); Protocol.Unregister(); answer = quiet ? DialogResult.OK : MessageBox.Show($"Sucessfully uninstalled {Constants.PROJECT_NAME}!", Constants.PROJECT_NAME, MessageBoxButtons.OK, MessageBoxIcon.Information); if (answer == DialogResult.OK || answer == DialogResult.Cancel) { string command = $"del /Q \"{Paths.Application}\""; if (Directory.GetFiles(Paths.Base, "*", SearchOption.AllDirectories).Length == 1) { // We're the only file in the directory, so we can delete the entire directory command += $" && rmdir \"{Paths.Base}\""; } Process.Start(new ProcessStartInfo() { FileName = "cmd.exe", Arguments = $"/c timeout 5 && {command}", UseShellExecute = true, WindowStyle = ProcessWindowStyle.Hidden }); Environment.Exit((int)Win32.ErrorCode.ERROR_SUCCESS); } } #endregion #region Registration public static void Register() { using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Constants.PROJECT_NAME}"); uninstallKey.SetValue("NoModify", 1); uninstallKey.SetValue("NoRepair", 1); uninstallKey.SetValue("DisplayIcon", $"{Paths.Application},0"); uninstallKey.SetValue("DisplayName", Constants.PROJECT_NAME); uninstallKey.SetValue("DisplayVersion", Version); if (uninstallKey.GetValue("InstallDate") is null) uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd")); uninstallKey.SetValue("InstallLocation", Paths.Base); uninstallKey.SetValue("Publisher", Constants.PROJECT_NAME); uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet"); uninstallKey.SetValue("UninstallString", $"\"{Paths.Application}\" -uninstall"); uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{Constants.PROJECT_REPOSITORY}"); uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{Constants.PROJECT_REPOSITORY}/releases/latest"); } public static void Unregister() { try { Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Constants.PROJECT_NAME}"); } catch { #if DEBUG throw; #endif } } #endregion #region Licensing public static bool License() { if (!File.Exists(Paths.License)) { if (!AskForLicense(Paths.License)) { // User doesn't want to license this launcher return false; } } // Load the license... while (!Web.LoadLicense(File.ReadAllText(Paths.License))) { // ...and if it's corrupt, keep asking for a new one. File.Delete(Paths.License); MessageBox.Show($"Corrupt license file! Please verify the contents of your license file (it should be named \"license.bin\".)", Constants.PROJECT_NAME, MessageBoxButtons.OK, MessageBoxIcon.Error); AskForLicense(Paths.License, false); } return true; } public static void Unlicense() { if (File.Exists(Paths.License)) { File.Delete(Paths.License); } } private static bool AskForLicense(string licensePath, bool showDialog = true) { DialogResult answer = showDialog ? MessageBox.Show($"{Constants.PROJECT_NAME} is currently undergoing maintenance and requires a license in order to access the test site. Would you like to look for the license file now?", Constants.PROJECT_NAME, MessageBoxButtons.YesNo, MessageBoxIcon.Warning) : DialogResult.Yes; if (answer == DialogResult.Yes) { using OpenFileDialog dialog = new() { Title = "Select your license file", Filter = "License files (*.bin)|*.bin", InitialDirectory = KnownFolders.Downloads.Path }; if (dialog.ShowDialog() == DialogResult.OK) { File.Copy(dialog.FileName, licensePath, true); } return true; } return false; } #endregion }