feat: refactor project, installation, licensing, a bunch of features

This commit is contained in:
rjindael 2023-07-31 02:32:06 -07:00
parent f9f7d653e3
commit 2eea14d000
No known key found for this signature in database
GPG Key ID: D069369C906CCF31
9 changed files with 183 additions and 86 deletions

View File

@ -7,6 +7,7 @@ namespace Kiseki.Launcher.Windows
public static string Base { get; private set; } = "";
public static string Logs { get; private set; } = "";
public static string Versions { get; private set; } = "";
public static string License { get; private set; } = "";
public static string Application { get; private set; } = "";
public static void Initialize(string baseDirectory)
@ -15,8 +16,9 @@ namespace Kiseki.Launcher.Windows
Logs = Path.Combine(Base, "Logs");
Versions = Path.Combine(Base, "Versions");
Application = Path.Combine(Base, $"{Launcher.ProjectName}.exe");
License = Path.Combine(Base, "license.bin");
Application = Path.Combine(Base, $"{Constants.ProjectName}.Launcher.exe");
}
}
}

View File

@ -6,28 +6,62 @@ namespace Kiseki.Launcher.Windows
{
public class Launcher : ILauncher
{
public const string ProjectName = "Kiseki";
public const string ProjectRepository = "kiseki-lol/launcher";
public readonly static string BaseUrl = "test.kiseki.lol"; // TODO: This should be set dynamically somehow
public readonly static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2];
public static void Install()
public async static void Install()
{
Directory.CreateDirectory(Directories.Base);
int response = await Web.CheckHealth();
if (response != Web.RESPONSE_SUCCESS || response != Web.RESPONSE_MAINTENANCE)
{
if (response != Web.RESPONSE_MAINTENANCE)
{
// The Kiseki website is either down or we can't connect to the internet.
// TODO: This is a strange scenario where we need to display an error outside of the controller. Can we do this within the page instead of a message box?
MessageBox.Show($"Failed to connect to the {Constants.ProjectName} website. Please check your internet connection.", Constants.ProjectName, MessageBoxButtons.OK, MessageBoxIcon.Error);
Environment.Exit(0);
}
else
{
// We are in maintenance mode, so let's ask for a license.
AskForLicense(Directories.License);
Web.LoadLicense(File.ReadAllText(Directories.License));
// ... try this again;
Install();
}
}
// okay, now download the launcher from the Kiseki website...
}
private static void AskForLicense(string licensePath)
{
using OpenFileDialog dialog = new()
{
Title = "Select your license file",
Filter = "License files (*.bin)|*.bin",
InitialDirectory = Directories.Base
};
if (dialog.ShowDialog() == DialogResult.OK)
{
File.Copy(dialog.FileName, licensePath, true);
}
}
public static void Register()
{
using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{ProjectName}"))
using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{Constants.ProjectName}"))
{
applicationKey.SetValue("InstallLocation", Directories.Base);
}
using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{ProjectName}");
using RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Constants.ProjectName}");
uninstallKey.SetValue("DisplayIcon", $"{Directories.Application},0");
uninstallKey.SetValue("DisplayName", ProjectName);
uninstallKey.SetValue("DisplayName", Constants.ProjectName);
uninstallKey.SetValue("DisplayVersion", Version);
if (uninstallKey.GetValue("InstallDate") is null)
@ -35,17 +69,17 @@ namespace Kiseki.Launcher.Windows
uninstallKey.SetValue("InstallLocation", Directories.Base);
uninstallKey.SetValue("NoRepair", 1);
uninstallKey.SetValue("Publisher", ProjectName);
uninstallKey.SetValue("Publisher", Constants.ProjectName);
uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu");
uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet");
uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall");
uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{ProjectRepository}");
uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{ProjectRepository}/releases/latest");
uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{Constants.ProjectRepository}");
uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{Constants.ProjectRepository}/releases/latest");
}
public static void Unregister()
{
Registry.CurrentUser.DeleteSubKey($@"Software\{ProjectName}");
Registry.CurrentUser.DeleteSubKey($@"Software\{Constants.ProjectName}");
}
}
}

View File

@ -9,13 +9,19 @@ namespace Kiseki.Launcher.Windows
private readonly TaskDialogPage Page;
private readonly Controller Controller;
public MainWindow(string[] args)
public MainWindow(string payload)
{
CloseButton = TaskDialogButton.Close;
Controller = new Controller(payload);
Controller.OnPageHeadingChange += Controller_PageHeadingChanged;
Controller.OnProgressBarAdd += Controller_ProgressBarAdded;
Controller.OnProgressBarStateChange += Controller_ProgressBarStateChanged;
Controller.OnErrorShow += Controller_ErrorShown;
Controller.OnLaunch += (s, e) => Environment.Exit(0);
CloseButton = TaskDialogButton.Close;
Page = new TaskDialogPage()
{
Caption = Launcher.ProjectName,
Caption = Constants.ProjectName,
AllowMinimize = true,
ProgressBar = new TaskDialogProgressBar()
@ -26,13 +32,10 @@ namespace Kiseki.Launcher.Windows
Buttons = { CloseButton }
};
Controller = new Controller(Launcher.BaseUrl, args);
Controller.OnPageHeadingChange += Controller_PageHeadingChanged;
Controller.OnProgressBarChange += Controller_ProgressBarChanged;
Controller.OnProgressBarStateChange += Controller_ProgressBarStateChanged;
Controller.OnInstall += (s, e) => Launcher.Install();
Controller.OnLaunch += (s, e) => Environment.Exit(0);
Page.Created += (s, e) =>
{
Controller.Start();
};
Page.Destroyed += (s, e) =>
{
@ -45,22 +48,23 @@ namespace Kiseki.Launcher.Windows
private void CloseButton_Click(object? sender, EventArgs e)
{
throw new NotImplementedException();
Controller.Dispose();
Environment.Exit(0);
}
private void Controller_PageHeadingChanged(object? sender, string Heading)
private void Controller_PageHeadingChanged(object? sender, string heading)
{
Page.Heading = Heading;
Page.Heading = heading;
}
private void Controller_ProgressBarChanged(object? sender, int Value)
private void Controller_ProgressBarAdded(object? sender, int value)
{
Page.ProgressBar!.Value = Value;
Page.ProgressBar!.Value += value;
}
private void Controller_ProgressBarStateChanged(object? sender, ProgressBarState State)
private void Controller_ProgressBarStateChanged(object? sender, ProgressBarState state)
{
Page.ProgressBar!.State = State switch
Page.ProgressBar!.State = state switch
{
ProgressBarState.Normal => TaskDialogProgressBarState.Normal,
ProgressBarState.Marquee => TaskDialogProgressBarState.Marquee,
@ -68,16 +72,20 @@ namespace Kiseki.Launcher.Windows
};
}
private void Controller_ErrorShown(object? sender, string[] texts)
{
Page.Icon = TaskDialogIcon.Error;
Page.Heading = texts[0];
Page.Text = texts[1];
Controller.Dispose();
}
private void ShowProgressDialog()
{
TaskDialogIcon logo = new(Resources.IconKiseki);
Page.Icon = logo;
Page.Created += (s, e) =>
{
Controller.Start();
};
TaskDialog.ShowDialog(Page);
}
}

View File

@ -1,3 +1,5 @@
using System.Diagnostics;
namespace Kiseki.Launcher.Windows
{
internal static class Program
@ -6,10 +8,34 @@ namespace Kiseki.Launcher.Windows
static void Main(string[] args)
{
string parentFolder = Path.GetDirectoryName(AppContext.BaseDirectory)!;
Directories.Initialize(parentFolder.ToLower().Contains(Launcher.ProjectName) ? AppContext.BaseDirectory : Path.Combine(Directories.LocalAppData, Launcher.ProjectName));
if (parentFolder.ToLower().Contains(Constants.ProjectName))
{
// Set to the current directory (either user-installed or default; it has "Kiseki" in the path, so that's good enough for us)
Directories.Initialize(AppContext.BaseDirectory);
}
else
{
// Set to the default directory (user likely hasn't installed the launcher yet)
Directories.Initialize(Path.Combine(Directories.LocalAppData, Constants.ProjectName));
}
Web.Initialize();
if (!File.Exists(Directories.Application))
{
// The launcher is not installed, so let's install it.
Launcher.Install();
}
if (args.Length == 0)
{
// Nothing for us to do :P
Process.Start(Web.Url("/games"));
Environment.Exit(0);
}
ApplicationConfiguration.Initialize();
Application.Run(new MainWindow(args));
Application.Run(new MainWindow(args[0]));
}
}
}

View File

@ -0,0 +1,8 @@
namespace Kiseki.Launcher
{
public static class Constants
{
public const string ProjectName = "Kiseki";
public const string ProjectRepository = "kiseki-lol/launcher";
}
}

View File

@ -10,54 +10,43 @@ namespace Kiseki.Launcher
public class Controller
{
private readonly string BaseURL;
private readonly Dictionary<string, string> Arguments = new();
public event EventHandler<string>? OnPageHeadingChange;
public event EventHandler<int>? OnProgressBarChange;
public event EventHandler<int>? OnProgressBarAdd;
public event EventHandler<ProgressBarState>? OnProgressBarStateChange;
public event EventHandler? OnInstall;
public event EventHandler<string[]>? OnErrorShow;
public event EventHandler? OnLaunch;
public static readonly HttpClient HttpClient = new();
public Controller(string baseURL, string[] args)
public Controller(string payload)
{
BaseURL = baseURL;
if (args.Length == 0)
if (!Base64.IsBase64String(payload))
{
// We are launching for the first time. This means that we should trigger the launcher install process.
Install();
ErrorShow($"Failed to launch {Constants.ProjectName}", $"Try launching {Constants.ProjectName} from the website again.");
return;
}
else
// TODO: The payload will soon include more members; update this accordingly
payload = Base64.ConvertBase64ToString(payload);
if (payload.Split("|").Length != 2)
{
// TODO: handle these more gracefully
if (!Base64.IsBase64String(args[0]))
{
Environment.Exit(0);
}
// TODO: the payload will soon include more members, such as whether to open the IDE or not (as well as values required for our weird loopback authentication thing)
string payload = Base64.ConvertBase64ToString(args[0]);
if (payload.Split("|").Length != 2)
{
Environment.Exit(0);
}
Arguments["JoinScriptURL"] = payload.Split("|")[0];
Arguments["Ticket"] = payload.Split("|")[1];
ErrorShow($"Failed to launch {Constants.ProjectName}", $"Try launching {Constants.ProjectName} from the website again.");
return;
}
Arguments["JoinScriptURL"] = payload.Split("|")[0];
Arguments["Ticket"] = payload.Split("|")[1];
}
public async void Start()
{
PageHeadingChange("Connecting to Kiseki...");
var response = await Http.GetJson<Models.Health>($"https://{BaseURL}/api/health");
if (response is null)
// This is kind of useless, but whatever
int response = await Web.CheckHealth();
if (response != Web.RESPONSE_SUCCESS)
{
PageHeadingChange("Failed to connect");
ErrorShow($"Failed to connect to {Constants.ProjectName}.", "Please check your internet connection.");
return;
}
@ -71,18 +60,15 @@ namespace Kiseki.Launcher
marquee = false;
}
ProgressBarChange(progressValue);
ProgressBarAdd(progressValue);
}
static async IAsyncEnumerable<int> StreamBackgroundOperationProgressAsync()
{
await Task.Delay(2800);
for (int i = 0; i <= 100; i += 4)
{
yield return i;
await Task.Delay(200);
}
yield return 4;
await Task.Delay(200);
}
PageHeadingChange("Installing Kiseki...");
@ -103,27 +89,27 @@ namespace Kiseki.Launcher
// TODO: This will only be called when the user closes the window OR we're done (i.e. the Launched event is called.)
}
protected virtual void PageHeadingChange(string Heading)
protected virtual void PageHeadingChange(string heading)
{
OnPageHeadingChange!.Invoke(this, Heading);
OnPageHeadingChange!.Invoke(this, heading);
}
protected virtual void ProgressBarChange(int Value)
protected virtual void ProgressBarAdd(int value)
{
OnProgressBarChange!.Invoke(this, Value);
OnProgressBarAdd!.Invoke(this, value);
}
protected virtual void ProgressBarStateChange(ProgressBarState State)
protected virtual void ProgressBarStateChange(ProgressBarState state)
{
OnProgressBarStateChange!.Invoke(this, State);
OnProgressBarStateChange!.Invoke(this, state);
}
protected virtual void Install()
protected virtual void ErrorShow(string heading, string text)
{
OnInstall!.Invoke(this, EventArgs.Empty);
// ugly hack for now (I don't want to derive EventHandler just for this)
OnErrorShow!.Invoke(this, new string[] { heading, text });
}
protected virtual void Launch()
{
OnLaunch!.Invoke(this, EventArgs.Empty);

View File

@ -6,7 +6,7 @@ namespace Kiseki.Launcher.Helpers
{
public static async Task<T?> GetJson<T>(string url)
{
string json = await Controller.HttpClient.GetStringAsync(url);
string json = await Web.HttpClient.GetStringAsync(url);
try
{

View File

@ -2,6 +2,6 @@ namespace Kiseki.Launcher.Models
{
public class Health
{
public string Response { get; set; } = "";
public int Response { get; set; } = -1;
}
}

33
Kiseki.Launcher/Web.cs Normal file
View File

@ -0,0 +1,33 @@
namespace Kiseki.Launcher
{
public static class Web
{
public const int RESPONSE_SUCCESS = 0;
public const int RESPONSE_FAILURE = -1;
public const int RESPONSE_MAINTENANCE = 1;
public const string BaseUrl = "kiseki.lol";
public const string MaintenanceDomain = "test";
public static readonly HttpClient HttpClient = new();
public static string CurrentUrl { get; private set; } = "";
public static void Initialize() => CurrentUrl = BaseUrl;
public static string Url(string path) => $"https://{CurrentUrl}/{path}";
public static async Task<int> CheckHealth()
{
var response = await Helpers.Http.GetJson<Models.Health>(Url("/api/health"));
return response?.Response ?? RESPONSE_FAILURE;
}
public static void LoadLicense(string license)
{
CurrentUrl = $"{MaintenanceDomain}.{CurrentUrl}";
}
}
}