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 Base { get; private set; } = "";
public static string Logs { get; private set; } = ""; public static string Logs { get; private set; } = "";
public static string Versions { 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 string Application { get; private set; } = "";
public static void Initialize(string baseDirectory) public static void Initialize(string baseDirectory)
@ -16,7 +17,8 @@ namespace Kiseki.Launcher.Windows
Logs = Path.Combine(Base, "Logs"); Logs = Path.Combine(Base, "Logs");
Versions = Path.Combine(Base, "Versions"); 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 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 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() 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); 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("DisplayIcon", $"{Directories.Application},0");
uninstallKey.SetValue("DisplayName", ProjectName); uninstallKey.SetValue("DisplayName", Constants.ProjectName);
uninstallKey.SetValue("DisplayVersion", Version); uninstallKey.SetValue("DisplayVersion", Version);
if (uninstallKey.GetValue("InstallDate") is null) if (uninstallKey.GetValue("InstallDate") is null)
@ -35,17 +69,17 @@ namespace Kiseki.Launcher.Windows
uninstallKey.SetValue("InstallLocation", Directories.Base); uninstallKey.SetValue("InstallLocation", Directories.Base);
uninstallKey.SetValue("NoRepair", 1); uninstallKey.SetValue("NoRepair", 1);
uninstallKey.SetValue("Publisher", ProjectName); uninstallKey.SetValue("Publisher", Constants.ProjectName);
uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu"); uninstallKey.SetValue("ModifyPath", $"\"{Directories.Application}\" -menu");
uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet"); uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.Application}\" -uninstall -quiet");
uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall"); uninstallKey.SetValue("UninstallString", $"\"{Directories.Application}\" -uninstall");
uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{ProjectRepository}"); uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{Constants.ProjectRepository}");
uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{ProjectRepository}/releases/latest"); uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{Constants.ProjectRepository}/releases/latest");
} }
public static void Unregister() 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 TaskDialogPage Page;
private readonly Controller Controller; 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() Page = new TaskDialogPage()
{ {
Caption = Launcher.ProjectName, Caption = Constants.ProjectName,
AllowMinimize = true, AllowMinimize = true,
ProgressBar = new TaskDialogProgressBar() ProgressBar = new TaskDialogProgressBar()
@ -26,13 +32,10 @@ namespace Kiseki.Launcher.Windows
Buttons = { CloseButton } Buttons = { CloseButton }
}; };
Controller = new Controller(Launcher.BaseUrl, args); Page.Created += (s, e) =>
Controller.OnPageHeadingChange += Controller_PageHeadingChanged; {
Controller.OnProgressBarChange += Controller_ProgressBarChanged; Controller.Start();
Controller.OnProgressBarStateChange += Controller_ProgressBarStateChanged; };
Controller.OnInstall += (s, e) => Launcher.Install();
Controller.OnLaunch += (s, e) => Environment.Exit(0);
Page.Destroyed += (s, e) => Page.Destroyed += (s, e) =>
{ {
@ -45,22 +48,23 @@ namespace Kiseki.Launcher.Windows
private void CloseButton_Click(object? sender, EventArgs e) 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.Normal => TaskDialogProgressBarState.Normal,
ProgressBarState.Marquee => TaskDialogProgressBarState.Marquee, 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() private void ShowProgressDialog()
{ {
TaskDialogIcon logo = new(Resources.IconKiseki); TaskDialogIcon logo = new(Resources.IconKiseki);
Page.Icon = logo; Page.Icon = logo;
Page.Created += (s, e) =>
{
Controller.Start();
};
TaskDialog.ShowDialog(Page); TaskDialog.ShowDialog(Page);
} }
} }

View File

@ -1,3 +1,5 @@
using System.Diagnostics;
namespace Kiseki.Launcher.Windows namespace Kiseki.Launcher.Windows
{ {
internal static class Program internal static class Program
@ -6,10 +8,34 @@ namespace Kiseki.Launcher.Windows
static void Main(string[] args) static void Main(string[] args)
{ {
string parentFolder = Path.GetDirectoryName(AppContext.BaseDirectory)!; 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(); 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 public class Controller
{ {
private readonly string BaseURL;
private readonly Dictionary<string, string> Arguments = new(); private readonly Dictionary<string, string> Arguments = new();
public event EventHandler<string>? OnPageHeadingChange; public event EventHandler<string>? OnPageHeadingChange;
public event EventHandler<int>? OnProgressBarChange; public event EventHandler<int>? OnProgressBarAdd;
public event EventHandler<ProgressBarState>? OnProgressBarStateChange; public event EventHandler<ProgressBarState>? OnProgressBarStateChange;
public event EventHandler? OnInstall; public event EventHandler<string[]>? OnErrorShow;
public event EventHandler? OnLaunch; public event EventHandler? OnLaunch;
public static readonly HttpClient HttpClient = new(); public Controller(string payload)
public Controller(string baseURL, string[] args)
{ {
BaseURL = baseURL; if (!Base64.IsBase64String(payload))
if (args.Length == 0)
{ {
// We are launching for the first time. This means that we should trigger the launcher install process. ErrorShow($"Failed to launch {Constants.ProjectName}", $"Try launching {Constants.ProjectName} from the website again.");
Install(); 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 ErrorShow($"Failed to launch {Constants.ProjectName}", $"Try launching {Constants.ProjectName} from the website again.");
if (!Base64.IsBase64String(args[0])) return;
{
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];
} }
Arguments["JoinScriptURL"] = payload.Split("|")[0];
Arguments["Ticket"] = payload.Split("|")[1];
} }
public async void Start() public async void Start()
{ {
PageHeadingChange("Connecting to Kiseki..."); PageHeadingChange("Connecting to Kiseki...");
var response = await Http.GetJson<Models.Health>($"https://{BaseURL}/api/health"); // This is kind of useless, but whatever
if (response is null) 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; return;
} }
@ -71,18 +60,15 @@ namespace Kiseki.Launcher
marquee = false; marquee = false;
} }
ProgressBarChange(progressValue); ProgressBarAdd(progressValue);
} }
static async IAsyncEnumerable<int> StreamBackgroundOperationProgressAsync() static async IAsyncEnumerable<int> StreamBackgroundOperationProgressAsync()
{ {
await Task.Delay(2800); await Task.Delay(2800);
for (int i = 0; i <= 100; i += 4) yield return 4;
{ await Task.Delay(200);
yield return i;
await Task.Delay(200);
}
} }
PageHeadingChange("Installing Kiseki..."); 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.) // 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() protected virtual void Launch()
{ {
OnLaunch!.Invoke(this, EventArgs.Empty); OnLaunch!.Invoke(this, EventArgs.Empty);

View File

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

View File

@ -2,6 +2,6 @@ namespace Kiseki.Launcher.Models
{ {
public class Health 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}";
}
}
}