From bacb650ddc035d4f2b954d65a7bd559ec4338c9e Mon Sep 17 00:00:00 2001 From: pizzaboxer <41478239+pizzaboxer@users.noreply.github.com> Date: Thu, 11 Aug 2022 08:26:28 +0100 Subject: [PATCH] Features and bugfixes for v1.1.0 - Features - Add Discord Rich Presence support (the nuget package is like a year and a half out of date so submodule it is lol) - Add update checker - Add start menu folder creation - Bugfixes - Fix "Directory is not empty" error when updating Roblox - Fix uninstalling sometimes not working properly - Quality of Life - Split Bootstrapper class into partial files - Renamed TaskDialogStyle to VistaDialog for name simplification --- .gitmodules | 3 + Bloxstrap.sln | 6 + Bloxstrap/Bloxstrap.csproj | 13 +- Bloxstrap/Bootstrapper.cs | 655 ------------------ .../Bootstrapper/Bootstrapper.AppInstall.cs | 128 ++++ .../Bootstrapper/Bootstrapper.Properties.cs | 130 ++++ .../Bootstrapper.RobloxInstall.cs | 208 ++++++ .../Bootstrapper.RobloxModifications.cs | 62 ++ Bloxstrap/Bootstrapper/Bootstrapper.cs | 178 +++++ ...e.Designer.cs => LegacyDialog.Designer.cs} | 30 +- .../{LegacyDialogStyle.cs => LegacyDialog.cs} | 50 +- ...gacyDialogStyle.resx => LegacyDialog.resx} | 0 ...Designer.cs => ProgressDialog.Designer.cs} | 36 +- ...ogressDialogStyle.cs => ProgressDialog.cs} | 41 +- ...ssDialogStyle.resx => ProgressDialog.resx} | 0 .../{TaskDialogStyle.cs => VistaDialog.cs} | 10 +- Bloxstrap/Dialogs/Preferences.Designer.cs | 136 ++-- Bloxstrap/Dialogs/Preferences.cs | 92 ++- Bloxstrap/Dialogs/Preferences.resx | 3 + Bloxstrap/Enums/BootstrapperStyle.cs | 2 +- Bloxstrap/Helpers/DiscordRichPresence.cs | 73 ++ Bloxstrap/Helpers/UpdateChecker.cs | 76 ++ Bloxstrap/Helpers/Utilities.cs | 48 ++ Bloxstrap/Program.cs | 27 +- Bloxstrap/Settings.cs | 4 +- DiscordRPC | 1 + 26 files changed, 1180 insertions(+), 832 deletions(-) create mode 100644 .gitmodules delete mode 100644 Bloxstrap/Bootstrapper.cs create mode 100644 Bloxstrap/Bootstrapper/Bootstrapper.AppInstall.cs create mode 100644 Bloxstrap/Bootstrapper/Bootstrapper.Properties.cs create mode 100644 Bloxstrap/Bootstrapper/Bootstrapper.RobloxInstall.cs create mode 100644 Bloxstrap/Bootstrapper/Bootstrapper.RobloxModifications.cs create mode 100644 Bloxstrap/Bootstrapper/Bootstrapper.cs rename Bloxstrap/Dialogs/BootstrapperStyles/{LegacyDialogStyle.Designer.cs => LegacyDialog.Designer.cs} (82%) rename Bloxstrap/Dialogs/BootstrapperStyles/{LegacyDialogStyle.cs => LegacyDialog.cs} (84%) rename Bloxstrap/Dialogs/BootstrapperStyles/{LegacyDialogStyle.resx => LegacyDialog.resx} (100%) rename Bloxstrap/Dialogs/BootstrapperStyles/{ProgressDialogStyle.Designer.cs => ProgressDialog.Designer.cs} (80%) rename Bloxstrap/Dialogs/BootstrapperStyles/{ProgressDialogStyle.cs => ProgressDialog.cs} (80%) rename Bloxstrap/Dialogs/BootstrapperStyles/{ProgressDialogStyle.resx => ProgressDialog.resx} (100%) rename Bloxstrap/Dialogs/BootstrapperStyles/{TaskDialogStyle.cs => VistaDialog.cs} (91%) create mode 100644 Bloxstrap/Helpers/DiscordRichPresence.cs create mode 100644 Bloxstrap/Helpers/UpdateChecker.cs create mode 100644 Bloxstrap/Helpers/Utilities.cs create mode 160000 DiscordRPC diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..dc74b799 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "DiscordRPC"] + path = DiscordRPC + url = https://github.com/Lachee/discord-rpc-csharp.git diff --git a/Bloxstrap.sln b/Bloxstrap.sln index 62d830d7..ec42cc75 100644 --- a/Bloxstrap.sln +++ b/Bloxstrap.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bloxstrap", "Bloxstrap\Bloxstrap.csproj", "{646D1D58-C9CA-48C9-BBCD-30585A1DAAF1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordRPC", "DiscordRPC\DiscordRPC\DiscordRPC.csproj", "{BDB66971-35FA-45BD-ABD6-70B814D2E55C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {646D1D58-C9CA-48C9-BBCD-30585A1DAAF1}.Debug|Any CPU.Build.0 = Debug|Any CPU {646D1D58-C9CA-48C9-BBCD-30585A1DAAF1}.Release|Any CPU.ActiveCfg = Release|Any CPU {646D1D58-C9CA-48C9-BBCD-30585A1DAAF1}.Release|Any CPU.Build.0 = Release|Any CPU + {BDB66971-35FA-45BD-ABD6-70B814D2E55C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDB66971-35FA-45BD-ABD6-70B814D2E55C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDB66971-35FA-45BD-ABD6-70B814D2E55C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDB66971-35FA-45BD-ABD6-70B814D2E55C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index cfe3a0c3..34e8cfb3 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -9,14 +9,23 @@ AnyCPU AnyCPU;x86 Bloxstrap.ico - 1.0.0 - 1.0.0.0 + 1.1.0 + 1.1.0.0 + + + + + + + + + True diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs deleted file mode 100644 index a14dda02..00000000 --- a/Bloxstrap/Bootstrapper.cs +++ /dev/null @@ -1,655 +0,0 @@ -using System.Diagnostics; -using System.IO.Compression; -using System.Security.Cryptography; - -using Microsoft.Win32; - -using Bloxstrap.Enums; -using Bloxstrap.Dialogs.BootstrapperStyles; -using Bloxstrap.Helpers; -using Bloxstrap.Helpers.RSMM; - -namespace Bloxstrap -{ - public class Bootstrapper - { - private string? LaunchCommandLine; - - private string VersionGuid; - private PackageManifest VersionPackageManifest; - private FileManifest VersionFileManifest; - private string VersionFolder; - - private readonly string DownloadsFolder; - private readonly bool FreshInstall; - - private int ProgressIncrement; - private bool CancelFired = false; - - private static readonly HttpClient Client = new(); - - // in case a new package is added, you can find the corresponding directory - // by opening the stock bootstrapper in a hex editor - // TODO - there ideally should be a less static way to do this that's not hardcoded? - private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary() - { - { "RobloxApp.zip", @"" }, - { "shaders.zip", @"shaders\" }, - { "ssl.zip", @"ssl\" }, - - { "content-avatar.zip", @"content\avatar\" }, - { "content-configs.zip", @"content\configs\" }, - { "content-fonts.zip", @"content\fonts\" }, - { "content-sky.zip", @"content\sky\" }, - { "content-sounds.zip", @"content\sounds\" }, - { "content-textures2.zip", @"content\textures\" }, - { "content-models.zip", @"content\models\" }, - - { "content-textures3.zip", @"PlatformContent\pc\textures\" }, - { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, - { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, - - { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, - { "extracontent-translations.zip", @"ExtraContent\translations\" }, - { "extracontent-models.zip", @"ExtraContent\models\" }, - { "extracontent-textures.zip", @"ExtraContent\textures\" }, - { "extracontent-places.zip", @"ExtraContent\places\" }, - }; - - private static readonly string AppSettings = - "\n" + - "\n" + - " content\n" + - " http://www.roblox.com\n" + - "\n"; - - public event EventHandler PromptShutdownEvent; - public event ChangeEventHandler ShowSuccessEvent; - public event ChangeEventHandler MessageChanged; - public event ChangeEventHandler ProgressBarValueChanged; - public event ChangeEventHandler ProgressBarStyleChanged; - public event ChangeEventHandler CancelEnabledChanged; - - private string _message; - private int _progress = 0; - private ProgressBarStyle _progressStyle = ProgressBarStyle.Marquee; - private bool _cancelEnabled = false; - - public string Message - { - get => _message; - - private set - { - if (_message == value) - return; - - MessageChanged.Invoke(this, new ChangeEventArgs(value)); - - _message = value; - } - } - - public int Progress - { - get => _progress; - - private set - { - if (_progress == value) - return; - - ProgressBarValueChanged.Invoke(this, new ChangeEventArgs(value)); - - _progress = value; - } - } - - public ProgressBarStyle ProgressStyle - { - get => _progressStyle; - - private set - { - if (_progressStyle == value) - return; - - ProgressBarStyleChanged.Invoke(this, new ChangeEventArgs(value)); - - _progressStyle = value; - } - } - - public bool CancelEnabled - { - get => _cancelEnabled; - - private set - { - if (_cancelEnabled == value) - return; - - CancelEnabledChanged.Invoke(this, new ChangeEventArgs(value)); - - _cancelEnabled = value; - } - } - - public Bootstrapper(BootstrapperStyle bootstrapperStyle, string? launchCommandLine = null) - { - Debug.WriteLine("Initializing bootstrapper"); - - FreshInstall = String.IsNullOrEmpty(Program.Settings.VersionGuid); - LaunchCommandLine = launchCommandLine; - DownloadsFolder = Path.Combine(Program.BaseDirectory, "Downloads"); - Client.Timeout = TimeSpan.FromMinutes(10); - - switch (bootstrapperStyle) - { - case BootstrapperStyle.TaskDialog: - new TaskDialogStyle(this); - break; - - case BootstrapperStyle.LegacyDialog: - Application.Run(new LegacyDialogStyle(this)); - break; - - case BootstrapperStyle.ProgressDialog: - Application.Run(new ProgressDialogStyle(this)); - break; - } - } - - public async Task Run() - { - if (LaunchCommandLine == "-uninstall") - { - Uninstall(); - return; - } - - await CheckLatestVersion(); - - if (!Directory.Exists(VersionFolder) || Program.Settings.VersionGuid != VersionGuid) - { - Debug.WriteLineIf(!Directory.Exists(VersionFolder), $"Installing latest version (!Directory.Exists({VersionFolder}))"); - Debug.WriteLineIf(Program.Settings.VersionGuid != VersionGuid, $"Installing latest version ({Program.Settings.VersionGuid} != {VersionGuid})"); - - await InstallLatestVersion(); - } - - // yes, doing this for every start is stupid, but the death sound mod is dynamically toggleable after all - ApplyModifications(); - - if (Program.IsFirstRun) - Program.SettingsManager.ShouldSave = true; - - if (Program.IsFirstRun || FreshInstall) - Register(); - - CheckInstall(); - - await StartRoblox(); - } - - private void CheckIfRunning() - { - Process[] processes = Process.GetProcessesByName("RobloxPlayerBeta"); - - if (processes.Length > 0) - PromptShutdown(); - - try - { - // try/catch just in case process was closed before prompt was answered - - foreach (Process process in processes) - { - process.CloseMainWindow(); - process.Close(); - } - } - catch (Exception) { } - } - - private async Task StartRoblox() - { - string startEventName = Program.ProjectName.Replace(" ", "") + "StartEvent"; - - Message = "Starting Roblox..."; - - // launch time isn't really required for all launches, but it's usually just safest to do this - LaunchCommandLine += " --launchtime=" + DateTimeOffset.Now.ToUnixTimeSeconds() + " -startEvent " + startEventName; - Debug.WriteLine($"Starting game client with command line '{LaunchCommandLine}'"); - - using (SystemEvent startEvent = new(startEventName)) - { - Process.Start(Path.Combine(VersionFolder, "RobloxPlayerBeta.exe"), LaunchCommandLine); - - Debug.WriteLine($"Waiting for {startEventName} event to be fired..."); - bool startEventFired = await startEvent.WaitForEvent(); - - startEvent.Close(); - - if (startEventFired) - { - Debug.WriteLine($"{startEventName} event fired! Exiting in 5 seconds..."); - await Task.Delay(5000); - - Program.Exit(); - } - } - } - - // Bootstrapper Installing - - public static void Register() - { - RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{Program.ProjectName}"); - - // new install location selected, delete old one - string? oldInstallLocation = (string?)applicationKey.GetValue("OldInstallLocation"); - if (!String.IsNullOrEmpty(oldInstallLocation) && oldInstallLocation != Program.BaseDirectory) - { - try - { - if (Directory.Exists(oldInstallLocation)) - Directory.Delete(oldInstallLocation, true); - } - catch (Exception) { } - - applicationKey.DeleteValue("OldInstallLocation"); - } - - applicationKey.SetValue("InstallLocation", Program.BaseDirectory); - applicationKey.Close(); - - // set uninstall key - RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}"); - uninstallKey.SetValue("DisplayIcon", $"{Program.FilePath},0"); - uninstallKey.SetValue("DisplayName", Program.ProjectName); - uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd")); - uninstallKey.SetValue("InstallLocation", Program.BaseDirectory); - // uninstallKey.SetValue("NoModify", 1); - uninstallKey.SetValue("NoRepair", 1); - uninstallKey.SetValue("Publisher", Program.ProjectName); - uninstallKey.SetValue("ModifyPath", $"\"{Program.FilePath}\" -preferences"); - uninstallKey.SetValue("UninstallString", $"\"{Program.FilePath}\" -uninstall"); - uninstallKey.Close(); - } - - public static void CheckInstall() - { - // check if launch uri is set to our bootstrapper - // this doesn't go under register, so we check every launch - // just in case the stock bootstrapper changes it back - - Protocol.Register("roblox", "Roblox", Program.FilePath); - Protocol.Register("roblox-player", "Roblox", Program.FilePath); - - // in case the user is reinstalling - if (File.Exists(Program.FilePath) && Program.IsFirstRun) - File.Delete(Program.FilePath); - - // check to make sure bootstrapper is in the install folder - if (!File.Exists(Program.FilePath) && Environment.ProcessPath is not null) - File.Copy(Environment.ProcessPath, Program.FilePath); - } - - private void Uninstall() - { - CheckIfRunning(); - - // lots of try/catches here... lol - - Message = $"Uninstalling {Program.ProjectName}..."; - - Program.SettingsManager.ShouldSave = false; - - // check if stock bootstrapper is still installed - RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); - if (bootstrapperKey is null) - { - Protocol.Unregister("roblox"); - Protocol.Unregister("roblox-player"); - } - else - { - // revert launch uri handler to stock bootstrapper - - string bootstrapperLocation = (string?)bootstrapperKey.GetValue("InstallLocation") + "RobloxPlayerLauncher.exe"; - - Protocol.Register("roblox", "Roblox", bootstrapperLocation); - Protocol.Register("roblox-player", "Roblox", bootstrapperLocation); - } - - try - { - // delete application key - Registry.CurrentUser.DeleteSubKey($@"Software\{Program.ProjectName}"); - } - catch (Exception) { } - - try - { - // delete installation folder - // (should delete everything except bloxstrap itself) - Directory.Delete(Program.BaseDirectory, true); - } - catch (Exception) { } - - try - { - // delete uninstall key - Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}"); - } - catch (Exception) { } - - ShowSuccess($"{Program.ProjectName} has been uninstalled"); - Program.Exit(); - } - - // Roblox Installing - - private async Task CheckLatestVersion() - { - Message = "Connecting to Roblox..."; - - Debug.WriteLine($"Checking latest version..."); - VersionGuid = await Client.GetStringAsync($"{Program.BaseUrlSetup}/version"); - VersionFolder = Path.Combine(Program.BaseDirectory, "Versions", VersionGuid); - Debug.WriteLine($"Latest version is {VersionGuid}"); - - Debug.WriteLine("Getting package manifest..."); - VersionPackageManifest = await PackageManifest.Get(VersionGuid); - - Debug.WriteLine("Getting file manifest..."); - VersionFileManifest = await FileManifest.Get(VersionGuid); - } - - private async Task InstallLatestVersion() - { - CheckIfRunning(); - - if (FreshInstall) - Message = "Installing Roblox..."; - else - Message = "Upgrading Roblox..."; - - Directory.CreateDirectory(Program.BaseDirectory); - - CancelEnabled = true; - - // i believe the original bootstrapper bases the progress bar off zip - // extraction progress, but here i'm doing package download progress - - ProgressStyle = ProgressBarStyle.Continuous; - - ProgressIncrement = (int)Math.Floor((decimal) 1 / VersionPackageManifest.Count * 100); - Debug.WriteLine($"Progress Increment is {ProgressIncrement}"); - - Directory.CreateDirectory(Path.Combine(Program.BaseDirectory, "Downloads")); - - foreach (Package package in VersionPackageManifest) - { - // no await, download all the packages at once - DownloadPackage(package); - } - - do - { - // wait for download to finish (and also round off the progress bar if needed) - - if (Progress == ProgressIncrement * VersionPackageManifest.Count) - Progress = 100; - - await Task.Delay(1000); - } - while (Progress != 100); - - ProgressStyle = ProgressBarStyle.Marquee; - - Debug.WriteLine("Finished downloading"); - - Directory.CreateDirectory(Path.Combine(Program.BaseDirectory, "Versions")); - - foreach (Package package in VersionPackageManifest) - { - // extract all the packages at once (shouldn't be too heavy on cpu?) - ExtractPackage(package); - } - - Debug.WriteLine("Finished extracting packages"); - - Message = "Configuring Roblox..."; - - string appSettingsLocation = Path.Combine(VersionFolder, "AppSettings.xml"); - await File.WriteAllTextAsync(appSettingsLocation, AppSettings); - - if (!FreshInstall) - { - // let's take this opportunity to delete any packages we don't need anymore - foreach (string filename in Directory.GetFiles(DownloadsFolder)) - { - if (!VersionPackageManifest.Exists(package => filename.Contains(package.Signature))) - File.Delete(filename); - } - - // and also to delete our old version folder - Directory.Delete(Path.Combine(Program.BaseDirectory, "Versions", Program.Settings.VersionGuid)); - } - - CancelEnabled = false; - - Program.Settings.VersionGuid = VersionGuid; - } - - private async void ApplyModifications() - { - // i guess we can just assume that if the hash does not match the manifest, then it's a mod - // probably not the best way to do this? don't think file corruption is that much of a worry here - - // TODO - i'm thinking i could have a manifest on my website like rbxManifest.txt - // for integrity checking and to quickly fix/alter stuff (like ouch.ogg being renamed) - // but that probably wouldn't be great to check on every run in case my webserver ever goes down - // interesting idea nonetheless, might add it sometime - - // TODO - i'm hoping i can take this idea of content mods much further - // for stuff like easily installing (community-created?) texture/shader/audio mods - // but for now, let's just keep it at this - - string fileContentName = "ouch.ogg"; - string fileContentLocation = "content\\sounds\\ouch.ogg"; - string fileLocation = Path.Combine(VersionFolder, fileContentLocation); - - string officialDeathSoundHash = VersionFileManifest[fileContentLocation]; - string currentDeathSoundHash = CalculateMD5(fileLocation); - - if (Program.Settings.UseOldDeathSound && currentDeathSoundHash == officialDeathSoundHash) - { - // let's get the old one! - - Debug.WriteLine($"Fetching old death sound..."); - - var response = await Client.GetAsync($"{Program.BaseUrlApplication}/mods/{fileContentLocation}"); - - if (File.Exists(fileLocation)) - File.Delete(fileLocation); - - using (var fileStream = new FileStream(fileLocation, FileMode.CreateNew)) - { - await response.Content.CopyToAsync(fileStream); - } - } - else if (!Program.Settings.UseOldDeathSound && currentDeathSoundHash != officialDeathSoundHash) - { - // who's lame enough to ever do this? - // well, we need to re-extract the one that's in the content-sounds.zip package - - Debug.WriteLine("Fetching current death sound..."); - - var package = VersionPackageManifest.Find(x => x.Name == "content-sounds.zip"); - - if (package is null) - { - Debug.WriteLine("Failed to find content-sounds.zip package! Aborting..."); - return; - } - - DownloadPackage(package); - - string packageLocation = Path.Combine(DownloadsFolder, package.Signature); - string packageFolder = Path.Combine(VersionFolder, PackageDirectories[package.Name]); - - using (ZipArchive archive = ZipFile.OpenRead(packageLocation)) - { - ZipArchiveEntry? entry = archive.Entries.Where(x => x.FullName == fileContentName).FirstOrDefault(); - - if (entry is null) - { - Debug.WriteLine("Failed to find file entry in content-sounds.zip! Aborting..."); - return; - } - - if (File.Exists(fileLocation)) - File.Delete(fileLocation); - - entry.ExtractToFile(fileLocation); - } - } - } - - private async void DownloadPackage(Package package) - { - string packageUrl = $"{Program.BaseUrlSetup}/{VersionGuid}-{package.Name}"; - string packageLocation = Path.Combine(DownloadsFolder, package.Signature); - string robloxPackageLocation = Path.Combine(Program.LocalAppData, "Roblox", "Downloads", package.Signature); - - if (File.Exists(packageLocation)) - { - FileInfo file = new(packageLocation); - - string calculatedMD5 = CalculateMD5(packageLocation); - if (calculatedMD5 != package.Signature) - { - Debug.WriteLine($"{package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); - file.Delete(); - } - else - { - Debug.WriteLine($"{package.Name} is already downloaded, skipping..."); - Progress += ProgressIncrement; - return; - } - } - else if (File.Exists(robloxPackageLocation)) - { - // let's cheat! if the stock bootstrapper already previously downloaded the file, - // then we can just copy the one from there - - Debug.WriteLine($"Found existing version of {package.Name} ({robloxPackageLocation})! Copying to Downloads folder..."); - File.Copy(robloxPackageLocation, packageLocation); - Progress += ProgressIncrement; - return; - } - - if (!File.Exists(packageLocation)) - { - Debug.WriteLine($"Downloading {package.Name}..."); - - var response = await Client.GetAsync(packageUrl); - - if (CancelFired) - return; - - using (var fileStream = new FileStream(packageLocation, FileMode.CreateNew)) - { - await response.Content.CopyToAsync(fileStream); - } - - Debug.WriteLine($"Finished downloading {package.Name}!"); - Progress += ProgressIncrement; - } - } - - private void ExtractPackage(Package package) - { - if (CancelFired) - return; - - string packageLocation = Path.Combine(DownloadsFolder, package.Signature); - string packageFolder = Path.Combine(VersionFolder, PackageDirectories[package.Name]); - string extractPath; - - Debug.WriteLine($"Extracting {package.Name} to {packageFolder}..."); - - using (ZipArchive archive = ZipFile.OpenRead(packageLocation)) - { - foreach (ZipArchiveEntry entry in archive.Entries) - { - if (CancelFired) - return; - - if (entry.FullName.EndsWith(@"\")) - continue; - - extractPath = Path.Combine(packageFolder, entry.FullName); - - Debug.WriteLine($"[{package.Name}] Writing {extractPath}..."); - - Directory.CreateDirectory(Path.GetDirectoryName(extractPath)); - - if (File.Exists(extractPath)) - File.Delete(extractPath); - - entry.ExtractToFile(extractPath); - } - } - } - - // Dialog Events - - public void CancelButtonClicked() - { - CancelFired = true; - - try - { - if (Program.IsFirstRun) - Directory.Delete(Program.BaseDirectory, true); - else if (Directory.Exists(VersionFolder)) - Directory.Delete(VersionFolder, true); - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to cleanup install!\n\n{ex}"); - } - - Program.Exit(); - } - - private void ShowSuccess(string message) - { - ShowSuccessEvent.Invoke(this, new ChangeEventArgs(message)); - } - - private void PromptShutdown() - { - PromptShutdownEvent.Invoke(this, new EventArgs()); - } - - // Utilities - - private static string CalculateMD5(string filename) - { - using (MD5 md5 = MD5.Create()) - { - using (FileStream stream = File.OpenRead(filename)) - { - byte[] hash = md5.ComputeHash(stream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } - } - } -} diff --git a/Bloxstrap/Bootstrapper/Bootstrapper.AppInstall.cs b/Bloxstrap/Bootstrapper/Bootstrapper.AppInstall.cs new file mode 100644 index 00000000..f45aece2 --- /dev/null +++ b/Bloxstrap/Bootstrapper/Bootstrapper.AppInstall.cs @@ -0,0 +1,128 @@ +using Microsoft.Win32; +using Bloxstrap.Helpers; + +namespace Bloxstrap +{ + partial class Bootstrapper + { + public static void Register() + { + if (Program.BaseDirectory is null) + return; + + RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{Program.ProjectName}"); + + // new install location selected, delete old one + string? oldInstallLocation = (string?)applicationKey.GetValue("OldInstallLocation"); + if (!String.IsNullOrEmpty(oldInstallLocation) && oldInstallLocation != Program.BaseDirectory) + { + try + { + if (Directory.Exists(oldInstallLocation)) + Directory.Delete(oldInstallLocation, true); + } + catch (Exception) { } + + applicationKey.DeleteValue("OldInstallLocation"); + } + + applicationKey.SetValue("InstallLocation", Program.BaseDirectory); + applicationKey.Close(); + + // set uninstall key + RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}"); + uninstallKey.SetValue("DisplayIcon", $"{Program.FilePath},0"); + uninstallKey.SetValue("DisplayName", Program.ProjectName); + uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd")); + uninstallKey.SetValue("InstallLocation", Program.BaseDirectory); + uninstallKey.SetValue("NoRepair", 1); + uninstallKey.SetValue("Publisher", Program.ProjectName); + uninstallKey.SetValue("ModifyPath", $"\"{Program.FilePath}\" -preferences"); + uninstallKey.SetValue("UninstallString", $"\"{Program.FilePath}\" -uninstall"); + uninstallKey.Close(); + } + + public static void CheckInstall() + { + // check if launch uri is set to our bootstrapper + // this doesn't go under register, so we check every launch + // just in case the stock bootstrapper changes it back + + Protocol.Register("roblox", "Roblox", Program.FilePath); + Protocol.Register("roblox-player", "Roblox", Program.FilePath); + + // in case the user is reinstalling + if (File.Exists(Program.FilePath) && Program.IsFirstRun) + File.Delete(Program.FilePath); + + // check to make sure bootstrapper is in the install folder + if (!File.Exists(Program.FilePath) && Environment.ProcessPath is not null) + File.Copy(Environment.ProcessPath, Program.FilePath); + + // this SHOULD go under Register(), + // but then people who have Bloxstrap v1.0.0 installed won't have this without a reinstall + // maybe in a later version? + if (!Directory.Exists(Program.StartMenuDirectory)) + { + Directory.CreateDirectory(Program.StartMenuDirectory); + + ShellLink.Shortcut.CreateShortcut(Program.FilePath, "", Program.FilePath, 0) + .WriteToFile(Path.Combine(Program.StartMenuDirectory, "Play Roblox.lnk")); + + ShellLink.Shortcut.CreateShortcut(Program.FilePath, "-preferences", Program.FilePath, 0) + .WriteToFile(Path.Combine(Program.StartMenuDirectory, "Configure Bloxstrap.lnk")); + } + } + + private void Uninstall() + { + if (Program.BaseDirectory is null) + return; + + CheckIfRunning(); + + // lots of try/catches here... lol + + Message = $"Uninstalling {Program.ProjectName}..."; + + Program.SettingsManager.ShouldSave = false; + + // check if stock bootstrapper is still installed + RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); + if (bootstrapperKey is null) + { + Protocol.Unregister("roblox"); + Protocol.Unregister("roblox-player"); + } + else + { + // revert launch uri handler to stock bootstrapper + + string bootstrapperLocation = (string?)bootstrapperKey.GetValue("InstallLocation") + "RobloxPlayerLauncher.exe"; + + Protocol.Register("roblox", "Roblox", bootstrapperLocation); + Protocol.Register("roblox-player", "Roblox", bootstrapperLocation); + } + + try + { + // delete application key + Registry.CurrentUser.DeleteSubKey($@"Software\{Program.ProjectName}"); + + // delete start menu folder + Directory.Delete(Program.StartMenuDirectory, true); + + // delete uninstall key + Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}"); + + // delete installation folder + // (should delete everything except bloxstrap itself) + Directory.Delete(Program.BaseDirectory, true); + } + catch (Exception) { } + + ShowSuccess($"{Program.ProjectName} has been uninstalled"); + Program.Exit(); + } + } +} diff --git a/Bloxstrap/Bootstrapper/Bootstrapper.Properties.cs b/Bloxstrap/Bootstrapper/Bootstrapper.Properties.cs new file mode 100644 index 00000000..bd36f056 --- /dev/null +++ b/Bloxstrap/Bootstrapper/Bootstrapper.Properties.cs @@ -0,0 +1,130 @@ +using Bloxstrap.Helpers.RSMM; + +namespace Bloxstrap +{ + partial class Bootstrapper + { + private string? LaunchCommandLine; + + private string VersionGuid; + private PackageManifest VersionPackageManifest; + private FileManifest VersionFileManifest; + private string VersionFolder; + + private readonly string DownloadsFolder; + private readonly bool FreshInstall; + + private int ProgressIncrement; + private bool CancelFired = false; + + private static readonly HttpClient Client = new(); + + // in case a new package is added, you can find the corresponding directory + // by opening the stock bootstrapper in a hex editor + // TODO - there ideally should be a less static way to do this that's not hardcoded? + private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary() + { + { "RobloxApp.zip", @"" }, + { "shaders.zip", @"shaders\" }, + { "ssl.zip", @"ssl\" }, + + { "content-avatar.zip", @"content\avatar\" }, + { "content-configs.zip", @"content\configs\" }, + { "content-fonts.zip", @"content\fonts\" }, + { "content-sky.zip", @"content\sky\" }, + { "content-sounds.zip", @"content\sounds\" }, + { "content-textures2.zip", @"content\textures\" }, + { "content-models.zip", @"content\models\" }, + + { "content-textures3.zip", @"PlatformContent\pc\textures\" }, + { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, + { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, + + { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, + { "extracontent-translations.zip", @"ExtraContent\translations\" }, + { "extracontent-models.zip", @"ExtraContent\models\" }, + { "extracontent-textures.zip", @"ExtraContent\textures\" }, + { "extracontent-places.zip", @"ExtraContent\places\" }, + }; + + private static readonly string AppSettings = + "\n" + + "\n" + + " content\n" + + " http://www.roblox.com\n" + + "\n"; + + public event EventHandler CloseDialogEvent; + public event EventHandler PromptShutdownEvent; + public event ChangeEventHandler ShowSuccessEvent; + public event ChangeEventHandler MessageChanged; + public event ChangeEventHandler ProgressBarValueChanged; + public event ChangeEventHandler ProgressBarStyleChanged; + public event ChangeEventHandler CancelEnabledChanged; + + private string _message; + private int _progress = 0; + private ProgressBarStyle _progressStyle = ProgressBarStyle.Marquee; + private bool _cancelEnabled = false; + + public string Message + { + get => _message; + + private set + { + if (_message == value) + return; + + MessageChanged.Invoke(this, new ChangeEventArgs(value)); + + _message = value; + } + } + + public int Progress + { + get => _progress; + + private set + { + if (_progress == value) + return; + + ProgressBarValueChanged.Invoke(this, new ChangeEventArgs(value)); + + _progress = value; + } + } + + public ProgressBarStyle ProgressStyle + { + get => _progressStyle; + + private set + { + if (_progressStyle == value) + return; + + ProgressBarStyleChanged.Invoke(this, new ChangeEventArgs(value)); + + _progressStyle = value; + } + } + + public bool CancelEnabled + { + get => _cancelEnabled; + + private set + { + if (_cancelEnabled == value) + return; + + CancelEnabledChanged.Invoke(this, new ChangeEventArgs(value)); + + _cancelEnabled = value; + } + } + } +} diff --git a/Bloxstrap/Bootstrapper/Bootstrapper.RobloxInstall.cs b/Bloxstrap/Bootstrapper/Bootstrapper.RobloxInstall.cs new file mode 100644 index 00000000..915272cb --- /dev/null +++ b/Bloxstrap/Bootstrapper/Bootstrapper.RobloxInstall.cs @@ -0,0 +1,208 @@ +using System.Diagnostics; +using System.IO.Compression; + +using Bloxstrap.Helpers; +using Bloxstrap.Helpers.RSMM; + +namespace Bloxstrap +{ + partial class Bootstrapper + { + private async Task CheckLatestVersion() + { + if (Program.BaseDirectory is null) + return; + + Message = "Connecting to Roblox..."; + + VersionGuid = await Client.GetStringAsync($"{Program.BaseUrlSetup}/version"); + VersionFolder = Path.Combine(Program.BaseDirectory, "Versions", VersionGuid); + VersionPackageManifest = await PackageManifest.Get(VersionGuid); + VersionFileManifest = await FileManifest.Get(VersionGuid); + } + + private async Task InstallLatestVersion() + { + if (Program.BaseDirectory is null) + return; + + CheckIfRunning(); + + if (FreshInstall) + Message = "Installing Roblox..."; + else + Message = "Upgrading Roblox..."; + + Directory.CreateDirectory(Program.BaseDirectory); + + CancelEnabled = true; + + // i believe the original bootstrapper bases the progress bar off zip + // extraction progress, but here i'm doing package download progress + + ProgressStyle = ProgressBarStyle.Continuous; + + ProgressIncrement = (int)Math.Floor((decimal)1 / VersionPackageManifest.Count * 100); + + Directory.CreateDirectory(Path.Combine(Program.BaseDirectory, "Downloads")); + + foreach (Package package in VersionPackageManifest) + { + // no await, download all the packages at once + DownloadPackage(package); + } + + do + { + // wait for download to finish (and also round off the progress bar if needed) + + if (Progress == ProgressIncrement * VersionPackageManifest.Count) + Progress = 100; + + await Task.Delay(1000); + } + while (Progress != 100); + + ProgressStyle = ProgressBarStyle.Marquee; + + Debug.WriteLine("Finished downloading"); + + Directory.CreateDirectory(Path.Combine(Program.BaseDirectory, "Versions")); + + foreach (Package package in VersionPackageManifest) + { + // extract all the packages at once (shouldn't be too heavy on cpu?) + ExtractPackage(package); + } + + Debug.WriteLine("Finished extracting packages"); + + Message = "Configuring Roblox..."; + + string appSettingsLocation = Path.Combine(VersionFolder, "AppSettings.xml"); + await File.WriteAllTextAsync(appSettingsLocation, AppSettings); + + if (!FreshInstall) + { + // let's take this opportunity to delete any packages we don't need anymore + foreach (string filename in Directory.GetFiles(DownloadsFolder)) + { + if (!VersionPackageManifest.Exists(package => filename.Contains(package.Signature))) + File.Delete(filename); + } + + // and also to delete our old version folder + Directory.Delete(Path.Combine(Program.BaseDirectory, "Versions", Program.Settings.VersionGuid), true); + } + + CancelEnabled = false; + + Program.Settings.VersionGuid = VersionGuid; + } + + private async void ApplyModifications() + { + // i guess we can just assume that if the hash does not match the manifest, then it's a mod + // probably not the best way to do this? don't think file corruption is that much of a worry here + + // TODO - i'm thinking i could have a manifest on my website like rbxManifest.txt + // for integrity checking and to quickly fix/alter stuff (like ouch.ogg being renamed) + // but that probably wouldn't be great to check on every run in case my webserver ever goes down + // interesting idea nonetheless, might add it sometime + + // TODO - i'm hoping i can take this idea of content mods much further + // for stuff like easily installing (community-created?) texture/shader/audio mods + // but for now, let's just keep it at this + + await ModifyDeathSound(); + } + + private async void DownloadPackage(Package package) + { + string packageUrl = $"{Program.BaseUrlSetup}/{VersionGuid}-{package.Name}"; + string packageLocation = Path.Combine(DownloadsFolder, package.Signature); + string robloxPackageLocation = Path.Combine(Program.LocalAppData, "Roblox", "Downloads", package.Signature); + + if (File.Exists(packageLocation)) + { + FileInfo file = new(packageLocation); + + string calculatedMD5 = Utilities.CalculateMD5(packageLocation); + if (calculatedMD5 != package.Signature) + { + Debug.WriteLine($"{package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading..."); + file.Delete(); + } + else + { + Debug.WriteLine($"{package.Name} is already downloaded, skipping..."); + Progress += ProgressIncrement; + return; + } + } + else if (File.Exists(robloxPackageLocation)) + { + // let's cheat! if the stock bootstrapper already previously downloaded the file, + // then we can just copy the one from there + + Debug.WriteLine($"Found existing version of {package.Name} ({robloxPackageLocation})! Copying to Downloads folder..."); + File.Copy(robloxPackageLocation, packageLocation); + Progress += ProgressIncrement; + return; + } + + if (!File.Exists(packageLocation)) + { + Debug.WriteLine($"Downloading {package.Name}..."); + + var response = await Client.GetAsync(packageUrl); + + if (CancelFired) + return; + + using (var fileStream = new FileStream(packageLocation, FileMode.CreateNew)) + { + await response.Content.CopyToAsync(fileStream); + } + + Debug.WriteLine($"Finished downloading {package.Name}!"); + Progress += ProgressIncrement; + } + } + + private void ExtractPackage(Package package) + { + if (CancelFired) + return; + + string packageLocation = Path.Combine(DownloadsFolder, package.Signature); + string packageFolder = Path.Combine(VersionFolder, PackageDirectories[package.Name]); + string extractPath; + + Debug.WriteLine($"Extracting {package.Name} to {packageFolder}..."); + + using (ZipArchive archive = ZipFile.OpenRead(packageLocation)) + { + foreach (ZipArchiveEntry entry in archive.Entries) + { + if (CancelFired) + return; + + if (entry.FullName.EndsWith(@"\")) + continue; + + extractPath = Path.Combine(packageFolder, entry.FullName); + + Debug.WriteLine($"[{package.Name}] Writing {extractPath}..."); + + Directory.CreateDirectory(Path.GetDirectoryName(extractPath)); + + if (File.Exists(extractPath)) + File.Delete(extractPath); + + entry.ExtractToFile(extractPath); + } + } + } + } +} diff --git a/Bloxstrap/Bootstrapper/Bootstrapper.RobloxModifications.cs b/Bloxstrap/Bootstrapper/Bootstrapper.RobloxModifications.cs new file mode 100644 index 00000000..955f82e7 --- /dev/null +++ b/Bloxstrap/Bootstrapper/Bootstrapper.RobloxModifications.cs @@ -0,0 +1,62 @@ +using System.IO.Compression; + +using Bloxstrap.Helpers; + +namespace Bloxstrap +{ + partial class Bootstrapper + { + private async Task ModifyDeathSound() + { + string fileContentName = "ouch.ogg"; + string fileContentLocation = "content\\sounds\\ouch.ogg"; + string fileLocation = Path.Combine(VersionFolder, fileContentLocation); + + string officialDeathSoundHash = VersionFileManifest[fileContentLocation]; + string currentDeathSoundHash = Utilities.CalculateMD5(fileLocation); + + if (Program.Settings.UseOldDeathSound && currentDeathSoundHash == officialDeathSoundHash) + { + // let's get the old one! + + var response = await Client.GetAsync($"{Program.BaseUrlApplication}/mods/{fileContentLocation}"); + + if (File.Exists(fileLocation)) + File.Delete(fileLocation); + + using (var fileStream = new FileStream(fileLocation, FileMode.CreateNew)) + { + await response.Content.CopyToAsync(fileStream); + } + } + else if (!Program.Settings.UseOldDeathSound && currentDeathSoundHash != officialDeathSoundHash) + { + // who's lame enough to ever do this? + // well, we need to re-extract the one that's in the content-sounds.zip package + + var package = VersionPackageManifest.Find(x => x.Name == "content-sounds.zip"); + + if (package is null) + return; + + DownloadPackage(package); + + string packageLocation = Path.Combine(DownloadsFolder, package.Signature); + string packageFolder = Path.Combine(VersionFolder, PackageDirectories[package.Name]); + + using (ZipArchive archive = ZipFile.OpenRead(packageLocation)) + { + ZipArchiveEntry? entry = archive.Entries.Where(x => x.FullName == fileContentName).FirstOrDefault(); + + if (entry is null) + return; + + if (File.Exists(fileLocation)) + File.Delete(fileLocation); + + entry.ExtractToFile(fileLocation); + } + } + } + } +} diff --git a/Bloxstrap/Bootstrapper/Bootstrapper.cs b/Bloxstrap/Bootstrapper/Bootstrapper.cs new file mode 100644 index 00000000..30727dcd --- /dev/null +++ b/Bloxstrap/Bootstrapper/Bootstrapper.cs @@ -0,0 +1,178 @@ +using System.Diagnostics; + +using Bloxstrap.Enums; +using Bloxstrap.Dialogs.BootstrapperStyles; +using Bloxstrap.Helpers; +using Bloxstrap.Helpers.RSMM; + +namespace Bloxstrap +{ + public partial class Bootstrapper + { + public Bootstrapper() + { + if (Program.BaseDirectory is null) + return; + + FreshInstall = String.IsNullOrEmpty(Program.Settings.VersionGuid); + DownloadsFolder = Path.Combine(Program.BaseDirectory, "Downloads"); + Client.Timeout = TimeSpan.FromMinutes(10); + } + + public void Initialize(BootstrapperStyle bootstrapperStyle, string? launchCommandLine = null) + { + LaunchCommandLine = launchCommandLine; + + switch (bootstrapperStyle) + { + case BootstrapperStyle.VistaDialog: + new VistaDialog(this); + break; + + case BootstrapperStyle.LegacyDialog: + Application.Run(new LegacyDialog(this)); + break; + + case BootstrapperStyle.ProgressDialog: + Application.Run(new ProgressDialog(this)); + break; + } + } + + public async Task Run() + { + if (LaunchCommandLine == "-uninstall") + { + Uninstall(); + return; + } + + await CheckLatestVersion(); + + if (!Directory.Exists(VersionFolder) || Program.Settings.VersionGuid != VersionGuid) + { + Debug.WriteLineIf(!Directory.Exists(VersionFolder), $"Installing latest version (!Directory.Exists({VersionFolder}))"); + Debug.WriteLineIf(Program.Settings.VersionGuid != VersionGuid, $"Installing latest version ({Program.Settings.VersionGuid} != {VersionGuid})"); + + await InstallLatestVersion(); + } + + // yes, doing this for every start is stupid, but the death sound mod is dynamically toggleable after all + ApplyModifications(); + + if (Program.IsFirstRun) + Program.SettingsManager.ShouldSave = true; + + if (Program.IsFirstRun || FreshInstall) + Register(); + + CheckInstall(); + + await StartRoblox(); + + Program.Exit(); + } + + private void CheckIfRunning() + { + Process[] processes = Process.GetProcessesByName("RobloxPlayerBeta"); + + if (processes.Length > 0) + PromptShutdown(); + + try + { + // try/catch just in case process was closed before prompt was answered + + foreach (Process process in processes) + { + process.CloseMainWindow(); + process.Close(); + } + } + catch (Exception) { } + } + + private async Task StartRoblox() + { + string startEventName = Program.ProjectName.Replace(" ", "") + "StartEvent"; + + Message = "Starting Roblox..."; + + // launch time isn't really required for all launches, but it's usually just safest to do this + LaunchCommandLine += " --launchtime=" + DateTimeOffset.Now.ToUnixTimeSeconds() + " -startEvent " + startEventName; + + using (SystemEvent startEvent = new(startEventName)) + { + Process gameClient = Process.Start(Path.Combine(VersionFolder, "RobloxPlayerBeta.exe"), LaunchCommandLine); + + bool startEventFired = await startEvent.WaitForEvent(); + + startEvent.Close(); + + if (!startEventFired) + return; + + // event fired, wait for 6 seconds then close + await Task.Delay(6000); + + // now we move onto handling rich presence + // except beta app launch since we have to rely strictly on website launch + if (!Program.Settings.UseDiscordRichPresence || LaunchCommandLine.Contains("--app")) + return; + + // probably not the most ideal way to do this + string? placeId = Utilities.GetKeyValue(LaunchCommandLine, "placeId=", '&'); + + if (placeId is null) + return; + + // keep bloxstrap open to handle rich presence + using (DiscordRichPresence richPresence = new()) + { + bool presenceSet = await richPresence.SetPresence(placeId); + + if (!presenceSet) + return; + + CloseDialog(); + await gameClient.WaitForExitAsync(); + } + } + } + + public void CancelButtonClicked() + { + if (Program.BaseDirectory is null) + return; + + CancelFired = true; + + try + { + if (Program.IsFirstRun) + Directory.Delete(Program.BaseDirectory, true); + else if (Directory.Exists(VersionFolder)) + Directory.Delete(VersionFolder, true); + } + catch (Exception) { } + + Program.Exit(); + } + + private void ShowSuccess(string message) + { + ShowSuccessEvent.Invoke(this, new ChangeEventArgs(message)); + } + + private void PromptShutdown() + { + PromptShutdownEvent.Invoke(this, new EventArgs()); + } + + private void CloseDialog() + { + CloseDialogEvent.Invoke(this, new EventArgs()); + } + } +} diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.Designer.cs b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialog.Designer.cs similarity index 82% rename from Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.Designer.cs rename to Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialog.Designer.cs index 2da9d576..270c4998 100644 --- a/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.Designer.cs +++ b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialog.Designer.cs @@ -1,6 +1,6 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles { - partial class LegacyDialogStyle + partial class LegacyDialog { /// /// Required designer variable. @@ -31,7 +31,7 @@ private void InitializeComponent() this.Message = new System.Windows.Forms.Label(); this.ProgressBar = new System.Windows.Forms.ProgressBar(); this.IconBox = new System.Windows.Forms.PictureBox(); - this.CancelButton = new System.Windows.Forms.Button(); + this.ButtonCancel = new System.Windows.Forms.Button(); ((System.ComponentModel.ISupportInitialize)(this.IconBox)).BeginInit(); this.SuspendLayout(); // @@ -62,25 +62,25 @@ private void InitializeComponent() this.IconBox.TabIndex = 2; this.IconBox.TabStop = false; // - // CancelButton + // ButtonCancel // - this.CancelButton.Enabled = false; - this.CancelButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.CancelButton.Location = new System.Drawing.Point(271, 83); - this.CancelButton.Name = "CancelButton"; - this.CancelButton.Size = new System.Drawing.Size(75, 23); - this.CancelButton.TabIndex = 3; - this.CancelButton.Text = "Cancel"; - this.CancelButton.UseVisualStyleBackColor = true; - this.CancelButton.Visible = false; - this.CancelButton.Click += new System.EventHandler(this.CancelButton_Click); + this.ButtonCancel.Enabled = false; + this.ButtonCancel.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.ButtonCancel.Location = new System.Drawing.Point(271, 83); + this.ButtonCancel.Name = "ButtonCancel"; + this.ButtonCancel.Size = new System.Drawing.Size(75, 23); + this.ButtonCancel.TabIndex = 3; + this.ButtonCancel.Text = "Cancel"; + this.ButtonCancel.UseVisualStyleBackColor = true; + this.ButtonCancel.Visible = false; + this.ButtonCancel.Click += new System.EventHandler(this.ButtonCancel_Click); // // LegacyDialogStyle // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(362, 131); - this.Controls.Add(this.CancelButton); + this.Controls.Add(this.ButtonCancel); this.Controls.Add(this.IconBox); this.Controls.Add(this.ProgressBar); this.Controls.Add(this.Message); @@ -103,6 +103,6 @@ private void InitializeComponent() private Label Message; private ProgressBar ProgressBar; private PictureBox IconBox; - private Button CancelButton; + private Button ButtonCancel; } } \ No newline at end of file diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.cs b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialog.cs similarity index 84% rename from Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.cs rename to Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialog.cs index 0a44dea3..da6c3b65 100644 --- a/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.cs +++ b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialog.cs @@ -11,27 +11,17 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles // but once winforms code is cleaned up we could also do the 2009 version too // example: https://youtu.be/VpduiruysuM?t=18 - public partial class LegacyDialogStyle : Form + public partial class LegacyDialog : Form { - private Bootstrapper? Bootstrapper; + private readonly Bootstrapper? Bootstrapper; - public LegacyDialogStyle(Bootstrapper? bootstrapper = null) + public LegacyDialog(Bootstrapper? bootstrapper = null) { InitializeComponent(); - if (bootstrapper is not null) - { - Bootstrapper = bootstrapper; - Bootstrapper.PromptShutdownEvent += new EventHandler(PromptShutdown); - Bootstrapper.ShowSuccessEvent += new ChangeEventHandler(ShowSuccess); - Bootstrapper.MessageChanged += new ChangeEventHandler(MessageChanged); - Bootstrapper.ProgressBarValueChanged += new ChangeEventHandler(ProgressBarValueChanged); - Bootstrapper.ProgressBarStyleChanged += new ChangeEventHandler(ProgressBarStyleChanged); - Bootstrapper.CancelEnabledChanged += new ChangeEventHandler(CancelEnabledChanged); - } - + Bootstrapper = bootstrapper; + Icon icon = IconManager.GetIconResource(); - this.Text = Program.ProjectName; this.Icon = icon; this.IconBox.Image = icon.ToBitmap(); @@ -39,17 +29,28 @@ public LegacyDialogStyle(Bootstrapper? bootstrapper = null) if (Bootstrapper is null) { this.Message.Text = "Click the Cancel button to return to preferences"; - this.CancelButton.Enabled = true; - this.CancelButton.Visible = true; + this.ButtonCancel.Enabled = true; + this.ButtonCancel.Visible = true; } else { + Bootstrapper.CloseDialogEvent += new EventHandler(CloseDialog); + Bootstrapper.PromptShutdownEvent += new EventHandler(PromptShutdown); + Bootstrapper.ShowSuccessEvent += new ChangeEventHandler(ShowSuccess); + Bootstrapper.MessageChanged += new ChangeEventHandler(MessageChanged); + Bootstrapper.ProgressBarValueChanged += new ChangeEventHandler(ProgressBarValueChanged); + Bootstrapper.ProgressBarStyleChanged += new ChangeEventHandler(ProgressBarStyleChanged); + Bootstrapper.CancelEnabledChanged += new ChangeEventHandler(CancelEnabledChanged); + Task.Run(() => RunBootstrapper()); } } public async void RunBootstrapper() { + if (Bootstrapper is null) + return; + try { await Bootstrapper.Run(); @@ -84,6 +85,11 @@ private void ShowSuccess(object sender, ChangeEventArgs e) ); } + private void CloseDialog(object? sender, EventArgs e) + { + this.Close(); + } + private void PromptShutdown(object? sender, EventArgs e) { DialogResult result = MessageBox.Show( @@ -138,19 +144,19 @@ private void ProgressBarStyleChanged(object sender, ChangeEventArgs e) { - if (this.CancelButton.InvokeRequired) + if (this.ButtonCancel.InvokeRequired) { ChangeEventHandler handler = new(CancelEnabledChanged); - this.CancelButton.Invoke(handler, sender, e); + this.ButtonCancel.Invoke(handler, sender, e); } else { - this.CancelButton.Enabled = e.Value; - this.CancelButton.Visible = e.Value; + this.ButtonCancel.Enabled = e.Value; + this.ButtonCancel.Visible = e.Value; } } - private void CancelButton_Click(object sender, EventArgs e) + private void ButtonCancel_Click(object sender, EventArgs e) { if (Bootstrapper is null) this.Close(); diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.resx b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialog.resx similarity index 100% rename from Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.resx rename to Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialog.resx diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.Designer.cs b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialog.Designer.cs similarity index 80% rename from Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.Designer.cs rename to Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialog.Designer.cs index 6f8de52a..91bd4533 100644 --- a/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.Designer.cs +++ b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialog.Designer.cs @@ -1,6 +1,6 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles { - partial class ProgressDialogStyle + partial class ProgressDialog { /// /// Required designer variable. @@ -31,10 +31,10 @@ private void InitializeComponent() this.ProgressBar = new System.Windows.Forms.ProgressBar(); this.Message = new System.Windows.Forms.Label(); this.IconBox = new System.Windows.Forms.PictureBox(); - this.CancelButton = new System.Windows.Forms.PictureBox(); + this.ButtonCancel = new System.Windows.Forms.PictureBox(); this.panel1 = new System.Windows.Forms.Panel(); ((System.ComponentModel.ISupportInitialize)(this.IconBox)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.CancelButton)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.ButtonCancel)).BeginInit(); this.panel1.SuspendLayout(); this.SuspendLayout(); // @@ -69,26 +69,26 @@ private void InitializeComponent() this.IconBox.TabIndex = 2; this.IconBox.TabStop = false; // - // CancelButton + // ButtonCancel // - this.CancelButton.Enabled = false; - this.CancelButton.Image = global::Bloxstrap.Properties.Resources.CancelButton; - this.CancelButton.Location = new System.Drawing.Point(194, 264); - this.CancelButton.Name = "CancelButton"; - this.CancelButton.Size = new System.Drawing.Size(130, 44); - this.CancelButton.TabIndex = 3; - this.CancelButton.TabStop = false; - this.CancelButton.Visible = false; - this.CancelButton.Click += new System.EventHandler(this.CancelButton_Click); - this.CancelButton.MouseEnter += new System.EventHandler(this.CancelButton_MouseEnter); - this.CancelButton.MouseLeave += new System.EventHandler(this.CancelButton_MouseLeave); + this.ButtonCancel.Enabled = false; + this.ButtonCancel.Image = global::Bloxstrap.Properties.Resources.CancelButton; + this.ButtonCancel.Location = new System.Drawing.Point(194, 264); + this.ButtonCancel.Name = "ButtonCancel"; + this.ButtonCancel.Size = new System.Drawing.Size(130, 44); + this.ButtonCancel.TabIndex = 3; + this.ButtonCancel.TabStop = false; + this.ButtonCancel.Visible = false; + this.ButtonCancel.Click += new System.EventHandler(this.ButtonCancel_Click); + this.ButtonCancel.MouseEnter += new System.EventHandler(this.ButtonCancel_MouseEnter); + this.ButtonCancel.MouseLeave += new System.EventHandler(this.ButtonCancel_MouseLeave); // // panel1 // this.panel1.BackColor = System.Drawing.SystemColors.Window; this.panel1.Controls.Add(this.Message); this.panel1.Controls.Add(this.IconBox); - this.panel1.Controls.Add(this.CancelButton); + this.panel1.Controls.Add(this.ButtonCancel); this.panel1.Controls.Add(this.ProgressBar); this.panel1.Location = new System.Drawing.Point(1, 1); this.panel1.Name = "panel1"; @@ -109,7 +109,7 @@ private void InitializeComponent() this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; this.Text = "ProgressDialogStyle"; ((System.ComponentModel.ISupportInitialize)(this.IconBox)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.CancelButton)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.ButtonCancel)).EndInit(); this.panel1.ResumeLayout(false); this.ResumeLayout(false); @@ -120,7 +120,7 @@ private void InitializeComponent() private ProgressBar ProgressBar; private Label Message; private PictureBox IconBox; - private PictureBox CancelButton; + private PictureBox ButtonCancel; private Panel panel1; } } \ No newline at end of file diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.cs b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialog.cs similarity index 80% rename from Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.cs rename to Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialog.cs index 739b26c8..70f21a7e 100644 --- a/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.cs +++ b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialog.cs @@ -1,15 +1,17 @@ -using Bloxstrap.Helpers; +using System.Diagnostics; + +using Bloxstrap.Helpers; using Bloxstrap.Helpers.RSMM; namespace Bloxstrap.Dialogs.BootstrapperStyles { // TODO - universal implementation for winforms-based styles? (to reduce duplicate code) - public partial class ProgressDialogStyle : Form + public partial class ProgressDialog : Form { - private Bootstrapper? Bootstrapper; + private readonly Bootstrapper? Bootstrapper; - public ProgressDialogStyle(Bootstrapper? bootstrapper = null) + public ProgressDialog(Bootstrapper? bootstrapper = null) { InitializeComponent(); @@ -22,11 +24,12 @@ public ProgressDialogStyle(Bootstrapper? bootstrapper = null) if (Bootstrapper is null) { this.Message.Text = "Click the Cancel button to return to preferences"; - this.CancelButton.Enabled = true; - this.CancelButton.Visible = true; + this.ButtonCancel.Enabled = true; + this.ButtonCancel.Visible = true; } else { + Bootstrapper.CloseDialogEvent += new EventHandler(CloseDialog); Bootstrapper.PromptShutdownEvent += new EventHandler(PromptShutdown); Bootstrapper.ShowSuccessEvent += new ChangeEventHandler(ShowSuccess); Bootstrapper.MessageChanged += new ChangeEventHandler(MessageChanged); @@ -40,6 +43,9 @@ public ProgressDialogStyle(Bootstrapper? bootstrapper = null) public async void RunBootstrapper() { + if (Bootstrapper is null) + return; + try { await Bootstrapper.Run(); @@ -74,6 +80,11 @@ private void ShowSuccess(object sender, ChangeEventArgs e) ); } + private void CloseDialog(object? sender, EventArgs e) + { + this.Hide(); + } + private void PromptShutdown(object? sender, EventArgs e) { DialogResult result = MessageBox.Show( @@ -128,19 +139,19 @@ private void ProgressBarStyleChanged(object sender, ChangeEventArgs e) { - if (this.CancelButton.InvokeRequired) + if (this.ButtonCancel.InvokeRequired) { ChangeEventHandler handler = new(CancelEnabledChanged); - this.CancelButton.Invoke(handler, sender, e); + this.ButtonCancel.Invoke(handler, sender, e); } else { - this.CancelButton.Enabled = e.Value; - this.CancelButton.Visible = e.Value; + this.ButtonCancel.Enabled = e.Value; + this.ButtonCancel.Visible = e.Value; } } - private void CancelButton_Click(object sender, EventArgs e) + private void ButtonCancel_Click(object sender, EventArgs e) { if (Bootstrapper is null) this.Close(); @@ -148,14 +159,14 @@ private void CancelButton_Click(object sender, EventArgs e) Task.Run(() => Bootstrapper.CancelButtonClicked()); } - private void CancelButton_MouseEnter(object sender, EventArgs e) + private void ButtonCancel_MouseEnter(object sender, EventArgs e) { - this.CancelButton.Image = Properties.Resources.CancelButtonHover; + this.ButtonCancel.Image = Properties.Resources.CancelButtonHover; } - private void CancelButton_MouseLeave(object sender, EventArgs e) + private void ButtonCancel_MouseLeave(object sender, EventArgs e) { - this.CancelButton.Image = Properties.Resources.CancelButton; + this.ButtonCancel.Image = Properties.Resources.CancelButton; } } } diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.resx b/Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialog.resx similarity index 100% rename from Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialogStyle.resx rename to Bloxstrap/Dialogs/BootstrapperStyles/ProgressDialog.resx diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/TaskDialogStyle.cs b/Bloxstrap/Dialogs/BootstrapperStyles/VistaDialog.cs similarity index 91% rename from Bloxstrap/Dialogs/BootstrapperStyles/TaskDialogStyle.cs rename to Bloxstrap/Dialogs/BootstrapperStyles/VistaDialog.cs index 019325f7..e65a3fe6 100644 --- a/Bloxstrap/Dialogs/BootstrapperStyles/TaskDialogStyle.cs +++ b/Bloxstrap/Dialogs/BootstrapperStyles/VistaDialog.cs @@ -5,10 +5,6 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles { // example: https://youtu.be/h0_AL95Sc3o?t=48 - // i suppose a better name for this here would be "VistaDialog" rather than "TaskDialog"? - // having this named as BootstrapperStyles.TaskDialog would conflict with Forms.TaskDialog - // so naming it VistaDialog would let us drop the ~Style suffix on every style name - // this currently doesn't work because c# is stupid // technically, task dialogs are treated as winforms controls, but they don't classify as winforms controls at all // all winforms controls have the ability to be invoked from another thread, but task dialogs don't @@ -17,12 +13,12 @@ namespace Bloxstrap.Dialogs.BootstrapperStyles // for now, just stick to legacydialog and progressdialog - public class TaskDialogStyle + public class VistaDialog { - private Bootstrapper Bootstrapper; + private readonly Bootstrapper Bootstrapper; private TaskDialogPage Dialog; - public TaskDialogStyle(Bootstrapper bootstrapper) + public VistaDialog(Bootstrapper bootstrapper) { Bootstrapper = bootstrapper; Bootstrapper.ShowSuccessEvent += new ChangeEventHandler(ShowSuccess); diff --git a/Bloxstrap/Dialogs/Preferences.Designer.cs b/Bloxstrap/Dialogs/Preferences.Designer.cs index c7f09023..fe111a66 100644 --- a/Bloxstrap/Dialogs/Preferences.Designer.cs +++ b/Bloxstrap/Dialogs/Preferences.Designer.cs @@ -28,9 +28,12 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { + this.components = new System.ComponentModel.Container(); this.label1 = new System.Windows.Forms.Label(); this.Tabs = new System.Windows.Forms.TabControl(); this.DialogTab = new System.Windows.Forms.TabPage(); + this.groupBox5 = new System.Windows.Forms.GroupBox(); + this.ToggleDiscordRichPresence = new System.Windows.Forms.CheckBox(); this.groupBox3 = new System.Windows.Forms.GroupBox(); this.IconPreview = new System.Windows.Forms.PictureBox(); this.IconSelection = new System.Windows.Forms.ListBox(); @@ -38,23 +41,24 @@ private void InitializeComponent() this.StyleSelection = new System.Windows.Forms.ListBox(); this.InstallationTab = new System.Windows.Forms.TabPage(); this.groupBox4 = new System.Windows.Forms.GroupBox(); - this.ModifyDeathSoundToggle = new System.Windows.Forms.CheckBox(); - this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.ToggleDeathSound = new System.Windows.Forms.CheckBox(); + this.GroupBoxInstallLocation = new System.Windows.Forms.GroupBox(); this.InstallLocationBrowseButton = new System.Windows.Forms.Button(); this.InstallLocation = new System.Windows.Forms.TextBox(); this.SaveButton = new System.Windows.Forms.Button(); this.panel1 = new System.Windows.Forms.Panel(); - this.label2 = new System.Windows.Forms.Label(); this.PreviewButton = new System.Windows.Forms.Button(); this.InstallLocationBrowseDialog = new System.Windows.Forms.FolderBrowserDialog(); + this.InfoTooltip = new System.Windows.Forms.ToolTip(this.components); this.Tabs.SuspendLayout(); this.DialogTab.SuspendLayout(); + this.groupBox5.SuspendLayout(); this.groupBox3.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.IconPreview)).BeginInit(); this.groupBox2.SuspendLayout(); this.InstallationTab.SuspendLayout(); this.groupBox4.SuspendLayout(); - this.groupBox1.SuspendLayout(); + this.GroupBoxInstallLocation.SuspendLayout(); this.panel1.SuspendLayout(); this.SuspendLayout(); // @@ -66,7 +70,7 @@ private void InitializeComponent() this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(237, 23); this.label1.TabIndex = 1; - this.label1.Text = "Configure Preferences"; + this.label1.Text = "Configure Bloxstrap"; // // Tabs // @@ -75,21 +79,45 @@ private void InitializeComponent() this.Tabs.Location = new System.Drawing.Point(12, 40); this.Tabs.Name = "Tabs"; this.Tabs.SelectedIndex = 0; - this.Tabs.Size = new System.Drawing.Size(442, 176); + this.Tabs.Size = new System.Drawing.Size(442, 226); this.Tabs.TabIndex = 2; // // DialogTab // + this.DialogTab.Controls.Add(this.groupBox5); this.DialogTab.Controls.Add(this.groupBox3); this.DialogTab.Controls.Add(this.groupBox2); this.DialogTab.Location = new System.Drawing.Point(4, 24); this.DialogTab.Name = "DialogTab"; this.DialogTab.Padding = new System.Windows.Forms.Padding(3); - this.DialogTab.Size = new System.Drawing.Size(434, 148); + this.DialogTab.Size = new System.Drawing.Size(434, 198); this.DialogTab.TabIndex = 0; this.DialogTab.Text = "Bootstrapper"; this.DialogTab.UseVisualStyleBackColor = true; // + // groupBox5 + // + this.groupBox5.Controls.Add(this.ToggleDiscordRichPresence); + this.groupBox5.Location = new System.Drawing.Point(5, 146); + this.groupBox5.Name = "groupBox5"; + this.groupBox5.Size = new System.Drawing.Size(422, 46); + this.groupBox5.TabIndex = 7; + this.groupBox5.TabStop = false; + this.groupBox5.Text = "Launch"; + // + // ToggleDiscordRichPresence + // + this.ToggleDiscordRichPresence.AutoSize = true; + this.ToggleDiscordRichPresence.Checked = true; + this.ToggleDiscordRichPresence.CheckState = System.Windows.Forms.CheckState.Checked; + this.ToggleDiscordRichPresence.Location = new System.Drawing.Point(9, 19); + this.ToggleDiscordRichPresence.Name = "ToggleDiscordRichPresence"; + this.ToggleDiscordRichPresence.Size = new System.Drawing.Size(274, 19); + this.ToggleDiscordRichPresence.TabIndex = 0; + this.ToggleDiscordRichPresence.Text = "Show game activity with Discord Rich Presence"; + this.ToggleDiscordRichPresence.UseVisualStyleBackColor = true; + this.ToggleDiscordRichPresence.CheckedChanged += new System.EventHandler(this.ToggleDiscordRichPresence_CheckedChanged); + // // groupBox3 // this.groupBox3.Controls.Add(this.IconPreview); @@ -144,49 +172,49 @@ private void InitializeComponent() // InstallationTab // this.InstallationTab.Controls.Add(this.groupBox4); - this.InstallationTab.Controls.Add(this.groupBox1); + this.InstallationTab.Controls.Add(this.GroupBoxInstallLocation); this.InstallationTab.Location = new System.Drawing.Point(4, 24); this.InstallationTab.Name = "InstallationTab"; this.InstallationTab.Padding = new System.Windows.Forms.Padding(3); - this.InstallationTab.Size = new System.Drawing.Size(434, 148); + this.InstallationTab.Size = new System.Drawing.Size(434, 198); this.InstallationTab.TabIndex = 2; this.InstallationTab.Text = "Installation"; this.InstallationTab.UseVisualStyleBackColor = true; // // groupBox4 // - this.groupBox4.Controls.Add(this.ModifyDeathSoundToggle); - this.groupBox4.Location = new System.Drawing.Point(5, 59); + this.groupBox4.Controls.Add(this.ToggleDeathSound); + this.groupBox4.Location = new System.Drawing.Point(5, 60); this.groupBox4.Name = "groupBox4"; - this.groupBox4.Size = new System.Drawing.Size(422, 84); + this.groupBox4.Size = new System.Drawing.Size(422, 46); this.groupBox4.TabIndex = 2; this.groupBox4.TabStop = false; this.groupBox4.Text = "Modifications"; // - // ModifyDeathSoundToggle - // - this.ModifyDeathSoundToggle.AutoSize = true; - this.ModifyDeathSoundToggle.Checked = true; - this.ModifyDeathSoundToggle.CheckState = System.Windows.Forms.CheckState.Checked; - this.ModifyDeathSoundToggle.Location = new System.Drawing.Point(9, 21); - this.ModifyDeathSoundToggle.Margin = new System.Windows.Forms.Padding(2); - this.ModifyDeathSoundToggle.Name = "ModifyDeathSoundToggle"; - this.ModifyDeathSoundToggle.Size = new System.Drawing.Size(138, 19); - this.ModifyDeathSoundToggle.TabIndex = 1; - this.ModifyDeathSoundToggle.Text = "Use Old Death Sound"; - this.ModifyDeathSoundToggle.UseVisualStyleBackColor = true; - this.ModifyDeathSoundToggle.CheckedChanged += new System.EventHandler(this.ModifyDeathSoundToggle_CheckedChanged); - // - // groupBox1 - // - this.groupBox1.Controls.Add(this.InstallLocationBrowseButton); - this.groupBox1.Controls.Add(this.InstallLocation); - this.groupBox1.Location = new System.Drawing.Point(5, 3); - this.groupBox1.Name = "groupBox1"; - this.groupBox1.Size = new System.Drawing.Size(422, 53); - this.groupBox1.TabIndex = 0; - this.groupBox1.TabStop = false; - this.groupBox1.Text = "Install Location"; + // ToggleDeathSound + // + this.ToggleDeathSound.AutoSize = true; + this.ToggleDeathSound.Checked = true; + this.ToggleDeathSound.CheckState = System.Windows.Forms.CheckState.Checked; + this.ToggleDeathSound.Location = new System.Drawing.Point(9, 19); + this.ToggleDeathSound.Margin = new System.Windows.Forms.Padding(2); + this.ToggleDeathSound.Name = "ToggleDeathSound"; + this.ToggleDeathSound.Size = new System.Drawing.Size(134, 19); + this.ToggleDeathSound.TabIndex = 1; + this.ToggleDeathSound.Text = "Use old death sound"; + this.ToggleDeathSound.UseVisualStyleBackColor = true; + this.ToggleDeathSound.CheckedChanged += new System.EventHandler(this.ToggleDeathSound_CheckedChanged); + // + // GroupBoxInstallLocation + // + this.GroupBoxInstallLocation.Controls.Add(this.InstallLocationBrowseButton); + this.GroupBoxInstallLocation.Controls.Add(this.InstallLocation); + this.GroupBoxInstallLocation.Location = new System.Drawing.Point(5, 3); + this.GroupBoxInstallLocation.Name = "GroupBoxInstallLocation"; + this.GroupBoxInstallLocation.Size = new System.Drawing.Size(422, 54); + this.GroupBoxInstallLocation.TabIndex = 0; + this.GroupBoxInstallLocation.TabStop = false; + this.GroupBoxInstallLocation.Text = "Install Location"; // // InstallLocationBrowseButton // @@ -225,24 +253,13 @@ private void InitializeComponent() | System.Windows.Forms.AnchorStyles.Right))); this.panel1.BackColor = System.Drawing.SystemColors.Control; this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; - this.panel1.Controls.Add(this.label2); this.panel1.Controls.Add(this.PreviewButton); this.panel1.Controls.Add(this.SaveButton); - this.panel1.Location = new System.Drawing.Point(-1, 227); + this.panel1.Location = new System.Drawing.Point(-1, 277); this.panel1.Name = "panel1"; this.panel1.Size = new System.Drawing.Size(466, 42); this.panel1.TabIndex = 6; // - // label2 - // - this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(12, 13); - this.label2.Margin = new System.Windows.Forms.Padding(0); - this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(221, 15); - this.label2.TabIndex = 6; - this.label2.Text = "made by pizzaboxer - i think this works..."; - // // PreviewButton // this.PreviewButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); @@ -254,12 +271,18 @@ private void InitializeComponent() this.PreviewButton.UseVisualStyleBackColor = true; this.PreviewButton.Click += new System.EventHandler(this.PreviewButton_Click); // + // InfoTooltip + // + this.InfoTooltip.ShowAlways = true; + this.InfoTooltip.ToolTipIcon = System.Windows.Forms.ToolTipIcon.Info; + this.InfoTooltip.ToolTipTitle = "Information"; + // // Preferences // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.SystemColors.Window; - this.ClientSize = new System.Drawing.Size(464, 268); + this.ClientSize = new System.Drawing.Size(464, 318); this.Controls.Add(this.panel1); this.Controls.Add(this.Tabs); this.Controls.Add(this.label1); @@ -271,16 +294,17 @@ private void InitializeComponent() this.Text = "Preferences"; this.Tabs.ResumeLayout(false); this.DialogTab.ResumeLayout(false); + this.groupBox5.ResumeLayout(false); + this.groupBox5.PerformLayout(); this.groupBox3.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this.IconPreview)).EndInit(); this.groupBox2.ResumeLayout(false); this.InstallationTab.ResumeLayout(false); this.groupBox4.ResumeLayout(false); this.groupBox4.PerformLayout(); - this.groupBox1.ResumeLayout(false); - this.groupBox1.PerformLayout(); + this.GroupBoxInstallLocation.ResumeLayout(false); + this.GroupBoxInstallLocation.PerformLayout(); this.panel1.ResumeLayout(false); - this.panel1.PerformLayout(); this.ResumeLayout(false); } @@ -294,7 +318,7 @@ private void InitializeComponent() private Button SaveButton; private Panel panel1; private ListBox StyleSelection; - private GroupBox groupBox1; + private GroupBox GroupBoxInstallLocation; private Button InstallLocationBrowseButton; private TextBox InstallLocation; private FolderBrowserDialog InstallLocationBrowseDialog; @@ -303,8 +327,10 @@ private void InitializeComponent() private PictureBox IconPreview; private ListBox IconSelection; private Button PreviewButton; - private Label label2; - private CheckBox ModifyDeathSoundToggle; + private CheckBox ToggleDeathSound; private GroupBox groupBox4; + private GroupBox groupBox5; + private CheckBox ToggleDiscordRichPresence; + private ToolTip InfoTooltip; } } \ No newline at end of file diff --git a/Bloxstrap/Dialogs/Preferences.cs b/Bloxstrap/Dialogs/Preferences.cs index 5756dffd..5c35e2cd 100644 --- a/Bloxstrap/Dialogs/Preferences.cs +++ b/Bloxstrap/Dialogs/Preferences.cs @@ -25,24 +25,10 @@ public partial class Preferences : Form { "2019", BootstrapperIcon.Icon2019 }, }; - private bool _useOldDeathSound = true; private BootstrapperStyle? _selectedStyle; private BootstrapperIcon? _selectedIcon; - - private bool UseOldDeathSound - { - get => _useOldDeathSound; - - set - { - if (_useOldDeathSound == value) - return; - - _useOldDeathSound = value; - - this.ModifyDeathSoundToggle.Checked = value; - } - } + private bool _useDiscordRichPresence = true; + private bool _useOldDeathSound = true; private BootstrapperStyle SelectedStyle { @@ -77,16 +63,48 @@ private BootstrapperIcon SelectedIcon } } + private bool UseDiscordRichPresence + { + get => _useDiscordRichPresence; + + set + { + if (_useDiscordRichPresence == value) + return; + + _useDiscordRichPresence = value; + + this.ToggleDiscordRichPresence.Checked = value; + } + } + + private bool UseOldDeathSound + { + get => _useOldDeathSound; + + set + { + if (_useOldDeathSound == value) + return; + + _useOldDeathSound = value; + + this.ToggleDeathSound.Checked = value; + } + } + public Preferences() { InitializeComponent(); + Program.SettingsManager.ShouldSave = false; + this.Icon = Properties.Resources.IconBloxstrap_ico; this.Text = Program.ProjectName; if (Program.IsFirstRun) { - this.SaveButton.Text = "Continue"; + this.SaveButton.Text = "Install"; this.InstallLocation.Text = Path.Combine(Program.LocalAppData, Program.ProjectName); } else @@ -104,14 +122,15 @@ public Preferences() this.IconSelection.Items.Add(icon.Key); } - UseOldDeathSound = Program.Settings.UseOldDeathSound; + this.InfoTooltip.SetToolTip(this.StyleSelection, "Choose how the bootstrapper dialog should look."); + this.InfoTooltip.SetToolTip(this.IconSelection, "Choose what icon the bootstrapper should use."); + this.InfoTooltip.SetToolTip(this.GroupBoxInstallLocation, "Choose where Bloxstrap should install to.\nThis is useful if you typically install all your games to a separate storage drive."); + this.InfoTooltip.SetToolTip(this.ToggleDiscordRichPresence, "Choose whether to show what game you're playing on your Discord profile.\nThis will ONLY work when you launch a game from the website, and is not supported in the Beta App."); + SelectedStyle = Program.Settings.BootstrapperStyle; SelectedIcon = Program.Settings.BootstrapperIcon; - } - - private void ShowDialog(MessageBoxIcon icon, string message) - { - MessageBox.Show(message, Program.ProjectName, MessageBoxButtons.OK, icon); + UseDiscordRichPresence = Program.Settings.UseDiscordRichPresence; + UseOldDeathSound = Program.Settings.UseOldDeathSound; } private void InstallLocationBrowseButton_Click(object sender, EventArgs e) @@ -140,7 +159,7 @@ private void SaveButton_Click(object sender, EventArgs e) if (String.IsNullOrEmpty(installLocation)) { - ShowDialog(MessageBoxIcon.Error, "You must set an install location"); + Program.ShowMessageBox(MessageBoxIcon.Error, "You must set an install location"); return; } @@ -163,12 +182,12 @@ private void SaveButton_Click(object sender, EventArgs e) } catch (UnauthorizedAccessException) { - ShowDialog(MessageBoxIcon.Error, $"{Program.ProjectName} does not have write access to the install location you selected. Please choose another install location."); + Program.ShowMessageBox(MessageBoxIcon.Error, $"{Program.ProjectName} does not have write access to the install location you selected. Please choose another install location."); return; } catch (Exception ex) { - ShowDialog(MessageBoxIcon.Error, ex.Message); + Program.ShowMessageBox(MessageBoxIcon.Error, ex.Message); return; } @@ -179,7 +198,7 @@ private void SaveButton_Click(object sender, EventArgs e) } else if (Program.BaseDirectory != installLocation) { - ShowDialog(MessageBoxIcon.Information, $"{Program.ProjectName} will install to the new location you've set the next time it runs."); + Program.ShowMessageBox(MessageBoxIcon.Information, $"{Program.ProjectName} will install to the new location you've set the next time it runs."); Program.Settings.VersionGuid = ""; @@ -196,9 +215,13 @@ private void SaveButton_Click(object sender, EventArgs e) File.Copy(Path.Combine(Program.BaseDirectory, "Settings.json"), Path.Combine(installLocation, "Settings.json")); } - Program.Settings.UseOldDeathSound = UseOldDeathSound; + if (!Program.IsFirstRun) + Program.SettingsManager.ShouldSave = true; + Program.Settings.BootstrapperStyle = SelectedStyle; Program.Settings.BootstrapperIcon = SelectedIcon; + Program.Settings.UseDiscordRichPresence = UseDiscordRichPresence; + Program.Settings.UseOldDeathSound = UseOldDeathSound; this.Close(); } @@ -214,11 +237,11 @@ private void PreviewButton_Click(object sender, EventArgs e) switch (SelectedStyle) { case BootstrapperStyle.LegacyDialog: - new LegacyDialogStyle().ShowDialog(); + new LegacyDialog().ShowDialog(); break; case BootstrapperStyle.ProgressDialog: - new ProgressDialogStyle().ShowDialog(); + new ProgressDialog().ShowDialog(); break; } @@ -227,9 +250,14 @@ private void PreviewButton_Click(object sender, EventArgs e) this.Visible = true; } - private void ModifyDeathSoundToggle_CheckedChanged(object sender, EventArgs e) + private void ToggleDiscordRichPresence_CheckedChanged(object sender, EventArgs e) + { + UseDiscordRichPresence = this.ToggleDiscordRichPresence.Checked; + } + + private void ToggleDeathSound_CheckedChanged(object sender, EventArgs e) { - UseOldDeathSound = this.ModifyDeathSoundToggle.Checked; + UseOldDeathSound = this.ToggleDeathSound.Checked; } } } diff --git a/Bloxstrap/Dialogs/Preferences.resx b/Bloxstrap/Dialogs/Preferences.resx index 8e732d63..453d499c 100644 --- a/Bloxstrap/Dialogs/Preferences.resx +++ b/Bloxstrap/Dialogs/Preferences.resx @@ -60,4 +60,7 @@ 17, 17 + + 222, 17 + \ No newline at end of file diff --git a/Bloxstrap/Enums/BootstrapperStyle.cs b/Bloxstrap/Enums/BootstrapperStyle.cs index fa3e00a7..fd0a9fd8 100644 --- a/Bloxstrap/Enums/BootstrapperStyle.cs +++ b/Bloxstrap/Enums/BootstrapperStyle.cs @@ -2,7 +2,7 @@ { public enum BootstrapperStyle { - TaskDialog, + VistaDialog, LegacyDialog, ProgressDialog } diff --git a/Bloxstrap/Helpers/DiscordRichPresence.cs b/Bloxstrap/Helpers/DiscordRichPresence.cs new file mode 100644 index 00000000..8df615dc --- /dev/null +++ b/Bloxstrap/Helpers/DiscordRichPresence.cs @@ -0,0 +1,73 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using DiscordRPC; + +namespace Bloxstrap.Helpers +{ + internal class DiscordRichPresence : IDisposable + { + readonly DiscordRpcClient RichPresence = new("1005469189907173486"); + + public async Task SetPresence(string placeId) + { + string placeName; + string placeThumbnail; + string creatorName; + + // null checking could probably be a lot more concrete here + using (HttpClient client = new()) + { + JObject placeInfo = await Utilities.GetJson($"https://economy.roblox.com/v2/assets/{placeId}/details"); + + placeName = placeInfo["Name"].Value(); + creatorName = placeInfo["Creator"]["Name"].Value(); + + JObject thumbnailInfo = await Utilities.GetJson($"https://thumbnails.roblox.com/v1/places/gameicons?placeIds={placeId}&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false"); + + if (thumbnailInfo["data"] is null) + return false; + + placeThumbnail = thumbnailInfo["data"][0]["imageUrl"].Value(); + } + + RichPresence.Initialize(); + + RichPresence.SetPresence(new RichPresence() + { + Details = placeName, + State = $"by {creatorName}", + Timestamps = new Timestamps() { Start = DateTime.UtcNow }, + + Assets = new Assets() + { + LargeImageKey = placeThumbnail, + LargeImageText = placeName, + SmallImageKey = "bloxstrap", + SmallImageText = "Rich Presence provided by Bloxstrap" + }, + + Buttons = new DiscordRPC.Button[] + { + new DiscordRPC.Button() + { + Label = "Play", + Url = $"https://www.roblox.com/games/start?placeId={placeId}&launchData=%7B%7D" + }, + + new DiscordRPC.Button() + { + Label = "View Details", + Url = $"https://www.roblox.com/games/{placeId}" + } + } + }); + + return true; + } + + public void Dispose() + { + RichPresence.Dispose(); + } + } +} diff --git a/Bloxstrap/Helpers/UpdateChecker.cs b/Bloxstrap/Helpers/UpdateChecker.cs new file mode 100644 index 00000000..b335ec88 --- /dev/null +++ b/Bloxstrap/Helpers/UpdateChecker.cs @@ -0,0 +1,76 @@ +using System.Diagnostics; +using Newtonsoft.Json.Linq; + +namespace Bloxstrap.Helpers +{ + public class UpdateChecker + { + public static void CheckInstalledVersion() + { + if (Environment.ProcessPath is null || !File.Exists(Program.FilePath)) + return; + + // if downloaded version doesn't match, replace installed version with downloaded version + FileVersionInfo currentVersionInfo = FileVersionInfo.GetVersionInfo(Environment.ProcessPath); + FileVersionInfo installedVersionInfo = FileVersionInfo.GetVersionInfo(Program.FilePath); + + if (installedVersionInfo != currentVersionInfo) + { + DialogResult result = MessageBox.Show( + $"The version of {Program.ProjectName} you've launched is newer than the version you currently have installed.\nWould you like to update your installed version of {Program.ProjectName}?", + Program.ProjectName, + MessageBoxButtons.YesNo, + MessageBoxIcon.Question + ); + + if (result == DialogResult.Yes) + { + File.Delete(Program.FilePath); + File.Copy(Environment.ProcessPath, Program.FilePath); + } + } + } + + public static async Task Check() + { + if (Environment.ProcessPath is null) + return; + + FileVersionInfo currentVersionInfo = FileVersionInfo.GetVersionInfo(Environment.ProcessPath); + string currentVersion = $"Bloxstrap v{currentVersionInfo.ProductVersion}"; + string latestVersion; + string releaseNotes; + + // get the latest version according to the latest github release info + // it should contain the latest product version, which we can check against + try + { + JObject releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{Program.ProjectRepository}/releases/latest"); + + latestVersion = releaseInfo["name"].Value(); + releaseNotes = releaseInfo["body"].Value(); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to fetch latest version info! ({ex.Message})"); + return; + } + + if (currentVersion != latestVersion) + { + DialogResult result = MessageBox.Show( + $"A new version of {Program.ProjectName} is available\n\nRelease notes:\n{releaseNotes}\n\nDo you want to download {latestVersion}?", + Program.ProjectName, + MessageBoxButtons.YesNo, + MessageBoxIcon.Question + ); + + if (result == DialogResult.Yes) + { + Process.Start(new ProcessStartInfo { FileName = $"https://github.com/{Program.ProjectRepository}/releases/latest", UseShellExecute = true }); + Program.Exit(); + } + } + } + } +} \ No newline at end of file diff --git a/Bloxstrap/Helpers/Utilities.cs b/Bloxstrap/Helpers/Utilities.cs new file mode 100644 index 00000000..894cc713 --- /dev/null +++ b/Bloxstrap/Helpers/Utilities.cs @@ -0,0 +1,48 @@ +using System.Security.Cryptography; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Bloxstrap.Helpers +{ + public class Utilities + { + public static async Task GetJson(string url) + { + using (HttpClient client = new()) + { + client.DefaultRequestHeaders.Add("User-Agent", Program.ProjectRepository); + + string jsonString = await client.GetStringAsync(url); + return (JObject)JsonConvert.DeserializeObject(jsonString); + } + } + + public static string CalculateMD5(string filename) + { + using (MD5 md5 = MD5.Create()) + { + using (FileStream stream = File.OpenRead(filename)) + { + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + } + } + + // quick and hacky way of getting a value from any key/value pair formatted list + // (command line args, uri params, etc) + public static string? GetKeyValue(string subject, string key, char delimiter) + { + if (subject.LastIndexOf(key) == -1) + return null; + + string substr = subject.Substring(subject.LastIndexOf(key) + key.Length); + + if (substr.IndexOf(delimiter) == -1) + return null; + + return substr.Split(delimiter)[0]; + } + } +} diff --git a/Bloxstrap/Program.cs b/Bloxstrap/Program.cs index b293e03c..1ea0dca4 100644 --- a/Bloxstrap/Program.cs +++ b/Bloxstrap/Program.cs @@ -1,5 +1,5 @@ -using Microsoft.Win32; using System.Diagnostics; +using Microsoft.Win32; using Bloxstrap.Helpers; namespace Bloxstrap @@ -19,11 +19,17 @@ internal static class Program public static string? BaseDirectory; public static string LocalAppData { get; private set; } public static string FilePath { get; private set; } + public static string StartMenuDirectory { get; private set; } public static bool IsFirstRun { get; private set; } = false; public static SettingsFormat Settings; public static SettingsManager SettingsManager = new(); + public static void ShowMessageBox(MessageBoxIcon icon, string message) + { + MessageBox.Show(message, Program.ProjectName, MessageBoxButtons.OK, icon); + } + public static void Exit() { SettingsManager.Save(); @@ -40,16 +46,17 @@ static void Main(string[] args) // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); - // ensure only one is running - Process[] processes = Process.GetProcessesByName(ProjectName); - if (processes.Length > 1) + if (Process.GetProcessesByName(ProjectName).Length > 1) + { + ShowMessageBox(MessageBoxIcon.Error, $"{ProjectName} is already running. Please close any currently open {ProjectName} window.\nIf you have Discord Rich Presence enabled, then close Roblox if it's running."); return; + } - // Task.Run(() => Updater.CheckForUpdates()).Wait(); - // return; + UpdateChecker.Check().Wait(); - LocalAppData = Environment.GetEnvironmentVariable("localappdata"); + LocalAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + // check if installed RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}"); if (registryKey is null) @@ -64,18 +71,20 @@ static void Main(string[] args) registryKey.Close(); } - // selection dialog was closed + // preferences dialog was closed, and so base directory was never set // (this doesnt account for the registry value not existing but thats basically never gonna happen) if (BaseDirectory is null) return; SettingsManager.SaveLocation = Path.Combine(BaseDirectory, "Settings.json"); FilePath = Path.Combine(BaseDirectory, $"{ProjectName}.exe"); + StartMenuDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", ProjectName); // we shouldn't save settings on the first run until the first installation is finished, // just in case the user decides to cancel the install if (!IsFirstRun) { + UpdateChecker.CheckInstalledVersion(); Settings = SettingsManager.Settings; SettingsManager.ShouldSave = true; } @@ -107,7 +116,7 @@ static void Main(string[] args) } if (!String.IsNullOrEmpty(commandLine)) - new Bootstrapper(Settings.BootstrapperStyle, commandLine); + new Bootstrapper().Initialize(Settings.BootstrapperStyle, commandLine); SettingsManager.Save(); } diff --git a/Bloxstrap/Settings.cs b/Bloxstrap/Settings.cs index ede8d381..83064fca 100644 --- a/Bloxstrap/Settings.cs +++ b/Bloxstrap/Settings.cs @@ -7,9 +7,11 @@ namespace Bloxstrap public class SettingsFormat { public string VersionGuid { get; set; } - public bool UseOldDeathSound { get; set; } = true; + public BootstrapperStyle BootstrapperStyle { get; set; } = BootstrapperStyle.ProgressDialog; public BootstrapperIcon BootstrapperIcon { get; set; } = BootstrapperIcon.IconBloxstrap; + public bool UseDiscordRichPresence { get; set; } = true; + public bool UseOldDeathSound { get; set; } = true; } public class SettingsManager diff --git a/DiscordRPC b/DiscordRPC new file mode 160000 index 00000000..a9fcc8d1 --- /dev/null +++ b/DiscordRPC @@ -0,0 +1 @@ +Subproject commit a9fcc8d1e85738bc6493474a62a961842fa8dbc3