diff --git a/Bloxstrap/Bloxstrap/App.xaml.cs b/Bloxstrap/Bloxstrap/App.xaml.cs index c27cc75..84ab14a 100644 --- a/Bloxstrap/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/Bloxstrap/App.xaml.cs @@ -1,10 +1,21 @@ -using System; +using Bloxstrap.Models; +using System; using System.Collections.Generic; using System.Configuration; using System.Data; +using System.Globalization; using System.Linq; +using System.Net.Http; +using System.Net; +using System.Reflection; using System.Threading.Tasks; using System.Windows; +using System.Diagnostics; +using Bloxstrap.Dialogs.Menu; +using Bloxstrap.Enums; +using Bloxstrap.Helpers; +using Microsoft.Win32; +using System.IO; namespace Bloxstrap { @@ -13,5 +24,154 @@ namespace Bloxstrap /// public partial class App : Application { + public const StringComparison StringFormat = StringComparison.InvariantCulture; + public static readonly CultureInfo CultureFormat = CultureInfo.InvariantCulture; + + public const string ProjectName = "Bloxstrap"; + public const string ProjectRepository = "pizzaboxer/bloxstrap"; + + public static string BaseDirectory = null!; + public static bool IsFirstRun { get; private set; } = false; + public static bool IsQuiet { get; private set; } = false; + public static bool IsUninstall { get; private set; } = false; + public static bool IsNoLaunch { get; private set; } = false; + public static bool IsUpgrade { get; private set; } = false; + public static string[] LaunchArgs { get; private set; } = null!; + + + public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; + + public static SettingsManager SettingsManager = new(); + public static SettingsFormat Settings = SettingsManager.Settings; + public static readonly HttpClient HttpClient = new(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }); + + // shorthand + public static MessageBoxResult ShowMessageBox(string message, MessageBoxImage icon = MessageBoxImage.None, MessageBoxButton buttons = MessageBoxButton.OK) + { + if (IsQuiet) + return MessageBoxResult.None; + + return MessageBox.Show(message, ProjectName, buttons, icon); + } + + public static void Terminate(int code = Bootstrapper.ERROR_SUCCESS) + { + SettingsManager.Save(); + Environment.Exit(code); + } + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + + LaunchArgs = e.Args; + + HttpClient.Timeout = TimeSpan.FromMinutes(5); + HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository); + + if (LaunchArgs.Length > 0) + { + if (Array.IndexOf(LaunchArgs, "-quiet") != -1) + IsQuiet = true; + + if (Array.IndexOf(LaunchArgs, "-uninstall") != -1) + IsUninstall = true; + + if (Array.IndexOf(LaunchArgs, "-nolaunch") != -1) + IsNoLaunch = true; + + if (Array.IndexOf(LaunchArgs, "-upgrade") != -1) + IsUpgrade = true; + } + + // check if installed + RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey($@"Software\{ProjectName}"); + + if (registryKey is null) + { + IsFirstRun = true; + Settings = SettingsManager.Settings; + + if (IsQuiet) + BaseDirectory = Path.Combine(Directories.LocalAppData, ProjectName); + else + new Preferences().ShowDialog(); + } + else + { + BaseDirectory = (string)registryKey.GetValue("InstallLocation")!; + registryKey.Close(); + } + + // 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 (String.IsNullOrEmpty(BaseDirectory)) + return; + + Directories.Initialize(BaseDirectory); + + SettingsManager.SaveLocation = Path.Combine(Directories.Base, "Settings.json"); + + // 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) + { + Settings = SettingsManager.Settings; + SettingsManager.ShouldSave = true; + } + +#if !DEBUG + if (!IsUninstall && !IsFirstRun) + Updater.CheckInstalledVersion(); +#endif + + string commandLine = ""; + +#if false//DEBUG + new Preferences().ShowDialog(); +#else + if (LaunchArgs.Length > 0) + { + if (LaunchArgs[0] == "-preferences") + { + if (Process.GetProcessesByName(ProjectName).Length > 1) + { + ShowMessageBox($"{ProjectName} is already running. Please close any currently open Bloxstrap or Roblox window before opening the configuration menu.", MessageBoxImage.Error); + return; + } + + new Preferences().ShowDialog(); + } + else if (LaunchArgs[0].StartsWith("roblox-player:")) + { + commandLine = Protocol.ParseUri(LaunchArgs[0]); + } + else if (LaunchArgs[0].StartsWith("roblox:")) + { + commandLine = $"--app --deeplink {LaunchArgs[0]}"; + } + else + { + commandLine = "--app"; + } + } + else + { + commandLine = "--app"; + } +#endif + + if (!String.IsNullOrEmpty(commandLine)) + { + DeployManager.Channel = Settings.Channel; + Settings.BootstrapperStyle.Show(new Bootstrapper(commandLine)); + } + + SettingsManager.Save(); + } } } diff --git a/Bloxstrap/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap/Bloxstrap.csproj index 4106cb0..7ea46c8 100644 --- a/Bloxstrap/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap/Bloxstrap.csproj @@ -5,6 +5,26 @@ net6.0-windows enable true + True + Bloxstrap.ico + 2.0.0 + 2.0.0.0 + + + + + + + + + + + + + + + + diff --git a/Bloxstrap/Bloxstrap/Bloxstrap.ico b/Bloxstrap/Bloxstrap/Bloxstrap.ico new file mode 100644 index 0000000..93904fb Binary files /dev/null and b/Bloxstrap/Bloxstrap/Bloxstrap.ico differ diff --git a/Bloxstrap/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bloxstrap/Bootstrapper.cs new file mode 100644 index 0000000..1d37f9e --- /dev/null +++ b/Bloxstrap/Bloxstrap/Bootstrapper.cs @@ -0,0 +1,800 @@ +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Net.Http; + +using Microsoft.Win32; + +using Bloxstrap.Enums; +using Bloxstrap.Dialogs.BootstrapperDialogs; +using Bloxstrap.Helpers; +using Bloxstrap.Helpers.Integrations; +using Bloxstrap.Helpers.RSMM; +using Bloxstrap.Models; +using System.Net; +using Bloxstrap.Properties; +using System.Threading.Tasks; +using System.Linq; +using System.Collections.Generic; +using System; +using System.Windows.Forms; +using System.Windows; + +namespace Bloxstrap +{ + public partial class Bootstrapper + { + #region Properties + + // https://learn.microsoft.com/en-us/windows/win32/msi/error-codes + public const int ERROR_SUCCESS = 0; + public const int ERROR_INSTALL_USEREXIT = 1602; + public const int ERROR_INSTALL_FAILURE = 1603; + public const int ERROR_PRODUCT_UNINSTALLED = 1614; + + // 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"; + + private string? LaunchCommandLine; + + private string VersionGuid = null!; + private PackageManifest VersionPackageManifest = null!; + private string VersionFolder = null!; + + private readonly bool FreshInstall; + + private double ProgressIncrement; + private long TotalBytes = 0; + private long TotalDownloadedBytes = 0; + private int PackagesExtracted = 0; + private bool CancelFired = false; + + public IBootstrapperDialog Dialog = null!; + #endregion + + #region Core + public Bootstrapper(string? launchCommandLine = null) + { + LaunchCommandLine = launchCommandLine; + FreshInstall = String.IsNullOrEmpty(App.Settings.VersionGuid); + } + + // this is called from BootstrapperStyleForm.SetupDialog() + public async Task Run() + { + if (App.IsQuiet) + Dialog.CloseDialog(); + + if (App.IsUninstall) + { + Uninstall(); + return; + } + +#if !DEBUG + if (!App.IsFirstRun && App.Settings.CheckForUpdates) + await CheckForUpdates(); +#endif + + await CheckLatestVersion(); + + // if bloxstrap is installing for the first time but is running, prompt to close roblox + // if roblox needs updating but is running, ignore update for now + if (!Directory.Exists(VersionFolder) && CheckIfRunning(true) || App.Settings.VersionGuid != VersionGuid && !CheckIfRunning(false)) + await InstallLatestVersion(); + + await ApplyModifications(); + + if (App.IsFirstRun) + App.SettingsManager.ShouldSave = true; + + if (App.IsFirstRun || FreshInstall) + Register(); + + CheckInstall(); + + await RbxFpsUnlocker.CheckInstall(); + + App.SettingsManager.Save(); + + if (App.IsFirstRun && App.IsNoLaunch) + Dialog.ShowSuccess($"{App.ProjectName} has successfully installed"); + else if (!App.IsNoLaunch) + await StartRoblox(); + } + + private async Task CheckForUpdates() + { + string currentVersion = $"Bloxstrap v{App.Version}"; + + var releaseInfo = await Utilities.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); + + if (releaseInfo is null || releaseInfo.Name is null || releaseInfo.Assets is null || currentVersion == releaseInfo.Name) + return; + + Dialog.Message = "Getting the latest Bloxstrap..."; + + // 64-bit is always the first option + GithubReleaseAsset asset = releaseInfo.Assets[Environment.Is64BitOperatingSystem ? 0 : 1]; + string downloadLocation = Path.Combine(Directories.Updates, asset.Name); + + Directory.CreateDirectory(Directories.Updates); + + Debug.WriteLine($"Downloading {releaseInfo.Name}..."); + + if (!File.Exists(downloadLocation)) + { + var response = await App.HttpClient.GetAsync(asset.BrowserDownloadUrl); + + using (var fileStream = new FileStream(Path.Combine(Directories.Updates, asset.Name), FileMode.CreateNew)) + { + await response.Content.CopyToAsync(fileStream); + } + } + + Debug.WriteLine($"Starting {releaseInfo.Name}..."); + + ProcessStartInfo startInfo = new() + { + FileName = downloadLocation, + }; + + foreach (string arg in App.LaunchArgs) + startInfo.ArgumentList.Add(arg); + + App.SettingsManager.Save(); + + Process.Start(startInfo); + + Environment.Exit(0); + } + + private async Task CheckLatestVersion() + { + Dialog.Message = "Connecting to Roblox..."; + + ClientVersion clientVersion = await DeployManager.GetLastDeploy(App.Settings.Channel); + VersionGuid = clientVersion.VersionGuid; + VersionFolder = Path.Combine(Directories.Versions, VersionGuid); + VersionPackageManifest = await PackageManifest.Get(VersionGuid); + } + + private bool CheckIfRunning(bool shutdown) + { + Process[] processes = Process.GetProcessesByName("RobloxPlayerBeta"); + + if (processes.Length == 0) + return false; + + if (shutdown) + { + Dialog.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) { } + } + + return true; + } + + private async Task StartRoblox() + { + string startEventName = App.ProjectName.Replace(" ", "") + "StartEvent"; + + Dialog.Message = "Starting Roblox..."; + + if (LaunchCommandLine == "--app" && App.Settings.UseDisableAppPatch) + { + Utilities.OpenWebsite("https://www.roblox.com/games"); + return; + } + + // launch time isn't really required for all launches, but it's usually just safest to do this + LaunchCommandLine += " --launchtime=" + DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + if (App.Settings.Channel.ToLower() != DeployManager.DefaultChannel.ToLower()) + LaunchCommandLine += " -channel " + App.Settings.Channel.ToLower(); + + LaunchCommandLine += " -startEvent " + startEventName; + + using (SystemEvent startEvent = new(startEventName)) + { + bool shouldWait = false; + + Process gameClient = Process.Start(Path.Combine(VersionFolder, "RobloxPlayerBeta.exe"), LaunchCommandLine); + Process? rbxFpsUnlocker = null; + DiscordRichPresence? richPresence = null; + + bool startEventFired = await startEvent.WaitForEvent(); + + startEvent.Close(); + + if (!startEventFired) + return; + + if (App.Settings.RFUEnabled && Process.GetProcessesByName("rbxfpsunlocker").Length == 0) + { + ProcessStartInfo startInfo = new() + { + FileName = Path.Combine(Directories.Integrations, @"rbxfpsunlocker\rbxfpsunlocker.exe"), + WorkingDirectory = Path.Combine(Directories.Integrations, "rbxfpsunlocker") + }; + + rbxFpsUnlocker = Process.Start(startInfo); + + if (App.Settings.RFUAutoclose) + shouldWait = true; + } + + // event fired, wait for 3 seconds then close + await Task.Delay(3000); + + // now we move onto handling rich presence + if (App.Settings.UseDiscordRichPresence) + { + richPresence = new DiscordRichPresence(); + richPresence.MonitorGameActivity(); + + shouldWait = true; + } + + if (!shouldWait) + return; + + // keep bloxstrap open in the background + Dialog.CloseDialog(); + await gameClient.WaitForExitAsync(); + + if (richPresence is not null) + richPresence.Dispose(); + + if (App.Settings.RFUAutoclose && rbxFpsUnlocker is not null) + rbxFpsUnlocker.Kill(); + } + } + + public void CancelButtonClicked() + { + if (!Dialog.CancelEnabled) + { + App.Terminate(ERROR_INSTALL_USEREXIT); + return; + } + + CancelFired = true; + + try + { + if (App.IsFirstRun) + Directory.Delete(Directories.Base, true); + else if (Directory.Exists(VersionFolder)) + Directory.Delete(VersionFolder, true); + } + catch (Exception) { } + + App.Terminate(ERROR_INSTALL_USEREXIT); + } +#endregion + +#region App Install + public static void Register() + { + RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}"); + + // new install location selected, delete old one + string? oldInstallLocation = (string?)applicationKey.GetValue("OldInstallLocation"); + if (!String.IsNullOrEmpty(oldInstallLocation) && oldInstallLocation != Directories.Base) + { + try + { + if (Directory.Exists(oldInstallLocation)) + Directory.Delete(oldInstallLocation, true); + } + catch (Exception) { } + + applicationKey.DeleteValue("OldInstallLocation"); + } + + applicationKey.SetValue("InstallLocation", Directories.Base); + applicationKey.Close(); + + // set uninstall key + RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}"); + uninstallKey.SetValue("DisplayIcon", $"{Directories.App},0"); + uninstallKey.SetValue("DisplayName", App.ProjectName); + uninstallKey.SetValue("DisplayVersion", App.Version); + + if (uninstallKey.GetValue("InstallDate") is null) + uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd")); + + uninstallKey.SetValue("InstallLocation", Directories.Base); + uninstallKey.SetValue("NoRepair", 1); + uninstallKey.SetValue("Publisher", "pizzaboxer"); + uninstallKey.SetValue("ModifyPath", $"\"{Directories.App}\" -preferences"); + uninstallKey.SetValue("QuietUninstallString", $"\"{Directories.App}\" -uninstall -quiet"); + uninstallKey.SetValue("UninstallString", $"\"{Directories.App}\" -uninstall"); + uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); + uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); + 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", Directories.App); + Protocol.Register("roblox-player", "Roblox", Directories.App); + + // in case the user is reinstalling + if (File.Exists(Directories.App) && App.IsFirstRun) + File.Delete(Directories.App); + + // check to make sure bootstrapper is in the install folder + if (!File.Exists(Directories.App) && Environment.ProcessPath is not null) + File.Copy(Environment.ProcessPath, Directories.App); + + // 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(Directories.StartMenu)) + { + Directory.CreateDirectory(Directories.StartMenu); + + ShellLink.Shortcut.CreateShortcut(Directories.App, "", Directories.App, 0) + .WriteToFile(Path.Combine(Directories.StartMenu, "Play Roblox.lnk")); + + ShellLink.Shortcut.CreateShortcut(Directories.App, "-preferences", Directories.App, 0) + .WriteToFile(Path.Combine(Directories.StartMenu, $"Configure {App.ProjectName}.lnk")); + } + + if (App.Settings.CreateDesktopIcon && !File.Exists(Path.Combine(Directories.Desktop, "Play Roblox.lnk"))) + { + ShellLink.Shortcut.CreateShortcut(Directories.App, "", Directories.App, 0) + .WriteToFile(Path.Combine(Directories.Desktop, "Play Roblox.lnk")); + } + } + + private void Uninstall() + { + CheckIfRunning(true); + + Dialog.Message = $"Uninstalling {App.ProjectName}..."; + + App.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\{App.ProjectName}"); + + // delete start menu folder + Directory.Delete(Directories.StartMenu, true); + + // delete desktop shortcut + File.Delete(Path.Combine(Directories.Desktop, "Play Roblox.lnk")); + + // delete uninstall key + Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}"); + + // delete installation folder + // (should delete everything except bloxstrap itself) + Directory.Delete(Directories.Base, true); + } + catch (Exception e) + { + Debug.WriteLine($"Could not fully uninstall! ({e})"); + } + + Dialog.ShowSuccess($"{App.ProjectName} has succesfully uninstalled"); + + App.Terminate(); + } +#endregion + +#region Roblox Install + private void UpdateProgressbar() + { + int newProgress = (int)Math.Floor(ProgressIncrement * TotalDownloadedBytes); + Dialog.ProgressValue = newProgress; + } + + private async Task InstallLatestVersion() + { + if (FreshInstall) + Dialog.Message = "Installing Roblox..."; + else + Dialog.Message = "Upgrading Roblox..."; + + // check if we have at least 300 megabytes of free disk space + if (Utilities.GetFreeDiskSpace(Directories.Base) < 1024*1024*300) + { + App.ShowMessageBox($"{App.ProjectName} requires at least 300 MB of disk space to install Roblox. Please free up some disk space and try again.", MessageBoxImage.Error); + App.Terminate(ERROR_INSTALL_FAILURE); + return; + } + + Directory.CreateDirectory(Directories.Base); + + Dialog.CancelEnabled = true; + Dialog.ProgressStyle = ProgressBarStyle.Continuous; + + // compute total bytes to download + + foreach (Package package in VersionPackageManifest) + TotalBytes += package.PackedSize; + + ProgressIncrement = (double)1 / TotalBytes * 100; + + Directory.CreateDirectory(Directories.Downloads); + Directory.CreateDirectory(Directories.Versions); + + foreach (Package package in VersionPackageManifest) + { + // download all the packages synchronously + await DownloadPackage(package); + + // extract the package immediately after download + ExtractPackage(package); + } + + // allow progress bar to 100% before continuing (purely ux reasons lol) + await Task.Delay(1000); + + Dialog.ProgressStyle = ProgressBarStyle.Marquee; + + Dialog.Message = "Configuring Roblox..."; + + // wait for all packages to finish extracting + while (PackagesExtracted < VersionPackageManifest.Count) + { + await Task.Delay(100); + } + + string appSettingsLocation = Path.Combine(VersionFolder, "AppSettings.xml"); + await File.WriteAllTextAsync(appSettingsLocation, AppSettings); + + if (!FreshInstall) + { + ReShade.SynchronizeConfigFile(); + + // let's take this opportunity to delete any packages we don't need anymore + foreach (string filename in Directory.GetFiles(Directories.Downloads)) + { + if (!VersionPackageManifest.Exists(package => filename.Contains(package.Signature))) + File.Delete(filename); + } + + string oldVersionFolder = Path.Combine(Directories.Versions, App.Settings.VersionGuid); + + if (VersionGuid != App.Settings.VersionGuid && Directory.Exists(oldVersionFolder)) + { + // and also to delete our old version folder + Directory.Delete(oldVersionFolder, true); + } + } + + Dialog.CancelEnabled = false; + + App.Settings.VersionGuid = VersionGuid; + } + + private async Task ApplyModifications() + { + Dialog.Message = "Applying Roblox modifications..."; + + string modFolder = Path.Combine(Directories.Modifications); + string manifestFile = Path.Combine(Directories.Base, "ModManifest.txt"); + + List manifestFiles = new(); + List modFolderFiles = new(); + + if (!Directory.Exists(modFolder)) + Directory.CreateDirectory(modFolder); + + await CheckModPreset(App.Settings.UseOldDeathSound, @"content\sounds\ouch.ogg", "OldDeath.ogg"); + await CheckModPreset(App.Settings.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowCursor.png", "OldCursor.png"); + await CheckModPreset(App.Settings.UseOldMouseCursor, @"content\textures\Cursors\KeyboardMouse\ArrowFarCursor.png", "OldFarCursor.png"); + await CheckModPreset(App.Settings.UseDisableAppPatch, @"ExtraContent\places\Mobile.rbxl", ""); + + await ReShade.CheckModifications(); + + foreach (string file in Directory.GetFiles(modFolder, "*.*", SearchOption.AllDirectories)) + { + // get relative directory path + string relativeFile = file.Substring(modFolder.Length + 1); + + // v1.7.0 - README has been moved to the preferences menu now + if (relativeFile == "README.txt") + { + File.Delete(file); + continue; + } + + modFolderFiles.Add(relativeFile); + } + + // the manifest is primarily here to keep track of what files have been + // deleted from the modifications folder, so that we know when to restore the + // original files from the downloaded packages + + if (File.Exists(manifestFile)) + manifestFiles = (await File.ReadAllLinesAsync(manifestFile)).ToList(); + else + manifestFiles = modFolderFiles; + + // copy and overwrite + foreach (string file in modFolderFiles) + { + string fileModFolder = Path.Combine(modFolder, file); + string fileVersionFolder = Path.Combine(VersionFolder, file); + + if (File.Exists(fileVersionFolder)) + { + if (Utilities.MD5File(fileModFolder) == Utilities.MD5File(fileVersionFolder)) + continue; + } + + string? directory = Path.GetDirectoryName(fileVersionFolder); + + if (directory is null) + continue; + + Directory.CreateDirectory(directory); + + File.Copy(fileModFolder, fileVersionFolder, true); + File.SetAttributes(fileVersionFolder, File.GetAttributes(fileModFolder) & ~FileAttributes.ReadOnly); + } + + // now check for files that have been deleted from the mod folder according to the manifest + foreach (string fileLocation in manifestFiles) + { + if (modFolderFiles.Contains(fileLocation)) + continue; + + KeyValuePair packageDirectory; + + try + { + packageDirectory = PackageDirectories.First(x => x.Key != "RobloxApp.zip" && fileLocation.StartsWith(x.Value)); + } + catch (InvalidOperationException) + { + // package doesn't exist, likely mistakenly placed file + string versionFileLocation = Path.Combine(VersionFolder, fileLocation); + + File.Delete(versionFileLocation); + + continue; + } + + // restore original file + string fileName = fileLocation.Substring(packageDirectory.Value.Length); + ExtractFileFromPackage(packageDirectory.Key, fileName); + } + + File.WriteAllLines(manifestFile, modFolderFiles); + } + + private static async Task CheckModPreset(bool condition, string location, string name) + { + string modFolderLocation = Path.Combine(Directories.Modifications, location); + byte[] binaryData = string.IsNullOrEmpty(name) ? Array.Empty() : await ResourceHelper.Get(name); + + if (condition) + { + if (!File.Exists(modFolderLocation)) + { + string? directory = Path.GetDirectoryName(modFolderLocation); + + if (directory is null) + return; + + Directory.CreateDirectory(directory); + + await File.WriteAllBytesAsync(modFolderLocation, binaryData); + } + } + else if (File.Exists(modFolderLocation) && Utilities.MD5File(modFolderLocation) == Utilities.MD5Data(binaryData)) + { + File.Delete(modFolderLocation); + } + } + + private async Task DownloadPackage(Package package) + { + string packageUrl = $"{DeployManager.BaseUrl}/{VersionGuid}-{package.Name}"; + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string robloxPackageLocation = Path.Combine(Directories.LocalAppData, "Roblox", "Downloads", package.Signature); + + if (File.Exists(packageLocation)) + { + FileInfo file = new(packageLocation); + + string calculatedMD5 = Utilities.MD5File(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..."); + TotalDownloadedBytes += package.PackedSize; + UpdateProgressbar(); + 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); + TotalDownloadedBytes += package.PackedSize; + UpdateProgressbar(); + return; + } + + if (!File.Exists(packageLocation)) + { + Debug.WriteLine($"Downloading {package.Name}..."); + + if (CancelFired) + return; + + var response = await App.HttpClient.GetAsync(packageUrl, HttpCompletionOption.ResponseHeadersRead); + + var buffer = new byte[8192]; + + using (var stream = await response.Content.ReadAsStreamAsync()) + using (var fileStream = new FileStream(packageLocation, FileMode.CreateNew)) + { + while (true) + { + var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + if (bytesRead == 0) + break; // we're done + + await fileStream.WriteAsync(buffer, 0, bytesRead); + + TotalDownloadedBytes += bytesRead; + UpdateProgressbar(); + } + } + + Debug.WriteLine($"Finished downloading {package.Name}!"); + } + } + + private async void ExtractPackage(Package package) + { + if (CancelFired) + return; + + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string packageFolder = Path.Combine(VersionFolder, PackageDirectories[package.Name]); + string extractPath; + string? directory; + + Debug.WriteLine($"Extracting {package.Name} to {packageFolder}..."); + + using (ZipArchive archive = await Task.Run(() => 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 = Path.GetDirectoryName(extractPath); + + if (directory is null) + continue; + + Directory.CreateDirectory(directory); + + await Task.Run(() => entry.ExtractToFile(extractPath, true)); + } + } + + Debug.WriteLine($"Finished extracting {package.Name}"); + + PackagesExtracted += 1; + } + + private void ExtractFileFromPackage(string packageName, string fileName) + { + Package? package = VersionPackageManifest.Find(x => x.Name == packageName); + + if (package is null) + return; + + DownloadPackage(package).GetAwaiter().GetResult(); + + string packageLocation = Path.Combine(Directories.Downloads, package.Signature); + string packageFolder = Path.Combine(VersionFolder, PackageDirectories[package.Name]); + + using (ZipArchive archive = ZipFile.OpenRead(packageLocation)) + { + ZipArchiveEntry? entry = archive.Entries.Where(x => x.FullName == fileName).FirstOrDefault(); + + if (entry is null) + return; + + string fileLocation = Path.Combine(packageFolder, entry.FullName); + + File.Delete(fileLocation); + + entry.ExtractToFile(fileLocation); + } + } +#endregion + } +} diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/BootstrapperDialogForm.cs b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/BootstrapperDialogForm.cs new file mode 100644 index 0000000..94bcc2e --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/BootstrapperDialogForm.cs @@ -0,0 +1,163 @@ +using Bloxstrap.Enums; +using Bloxstrap.Helpers; +using System.Windows.Forms; +using System.Threading.Tasks; +using System.Windows; +using System; + +namespace Bloxstrap.Dialogs.BootstrapperDialogs +{ + public class BootstrapperDialogForm : Form, IBootstrapperDialog + { + public Bootstrapper? Bootstrapper { get; set; } + + protected virtual string _message { get; set; } = "Please wait..."; + protected virtual ProgressBarStyle _progressStyle { get; set; } + protected virtual int _progressValue { get; set; } + protected virtual bool _cancelEnabled { get; set; } + + public string Message + { + get => _message; + set + { + if (this.InvokeRequired) + this.Invoke(() => _message = value); + else + _message = value; + } + } + + public ProgressBarStyle ProgressStyle + { + get => _progressStyle; + set + { + if (this.InvokeRequired) + this.Invoke(() => _progressStyle = value); + else + _progressStyle = value; + } + } + + public int ProgressValue + { + get => _progressValue; + set + { + if (this.InvokeRequired) + this.Invoke(() => _progressValue = value); + else + _progressValue = value; + } + } + + public bool CancelEnabled + { + get => _cancelEnabled; + set + { + if (this.InvokeRequired) + this.Invoke(() => _cancelEnabled = value); + else + _cancelEnabled = value; + } + } + + public void ScaleWindow() + { + this.Size = this.MinimumSize = this.MaximumSize = WindowScaling.GetScaledSize(this.Size); + + foreach (Control control in this.Controls) + { + control.Size = WindowScaling.GetScaledSize(control.Size); + control.Location = WindowScaling.GetScaledPoint(control.Location); + control.Padding = WindowScaling.GetScaledPadding(control.Padding); + } + } + + public void SetupDialog() + { + if (App.IsQuiet) + this.Hide(); + + this.Text = App.ProjectName; + this.Icon = App.Settings.BootstrapperIcon.GetIcon(); + + if (Bootstrapper is null) + { + Message = "Style Preview - Click Cancel to return"; + CancelEnabled = true; + } + else + { + Bootstrapper.Dialog = this; + Task.Run(() => RunBootstrapper()); + } + } + + + public async void RunBootstrapper() + { + if (Bootstrapper is null) + return; + +#if DEBUG + await Bootstrapper.Run(); +#else + try + { + await Bootstrapper.Run(); + } + catch (Exception ex) + { + // string message = String.Format("{0}: {1}", ex.GetType(), ex.Message); + string message = ex.ToString(); + ShowError(message); + } +#endif + + App.Terminate(); + } + + public virtual void ShowSuccess(string message) + { + App.ShowMessageBox(message, MessageBoxImage.Information); + App.Terminate(); + } + + public virtual void ShowError(string message) + { + App.ShowMessageBox($"An error occurred while starting Roblox\n\nDetails: {message}", MessageBoxImage.Error); + App.Terminate(Bootstrapper.ERROR_INSTALL_FAILURE); + } + + public virtual void CloseDialog() + { + if (this.InvokeRequired) + this.Invoke(CloseDialog); + else + this.Hide(); + } + + public void PromptShutdown() + { + MessageBoxResult result = App.ShowMessageBox( + "Roblox is currently running, but needs to close. Would you like close Roblox now?", + MessageBoxImage.Information, + MessageBoxButton.OKCancel + ); + + if (result != MessageBoxResult.OK) + Environment.Exit(Bootstrapper.ERROR_INSTALL_USEREXIT); + } + + public void ButtonCancel_Click(object? sender, EventArgs e) + { + if (Bootstrapper is null) + this.Close(); + else + Task.Run(() => Bootstrapper.CancelButtonClicked()); + } + } +} diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/IBootstrapperDialog.cs b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/IBootstrapperDialog.cs new file mode 100644 index 0000000..772ecee --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/IBootstrapperDialog.cs @@ -0,0 +1,20 @@ +using System.Windows.Forms; + +namespace Bloxstrap.Dialogs.BootstrapperDialogs +{ + public interface IBootstrapperDialog + { + Bootstrapper? Bootstrapper { get; set; } + + string Message { get; set; } + ProgressBarStyle ProgressStyle { get; set; } + int ProgressValue { get; set; } + bool CancelEnabled { get; set; } + + void RunBootstrapper(); + void ShowSuccess(string message); + void ShowError(string message); + void CloseDialog(); + void PromptShutdown(); + } +} diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.Designer.cs b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.Designer.cs new file mode 100644 index 0000000..af757c5 --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.Designer.cs @@ -0,0 +1,96 @@ +using System.Windows.Forms; + +namespace Bloxstrap.Dialogs.BootstrapperDialogs +{ + partial class LegacyDialog2009 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.labelMessage = new System.Windows.Forms.Label(); + this.ProgressBar = new System.Windows.Forms.ProgressBar(); + this.buttonCancel = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // labelMessage + // + this.labelMessage.Font = new System.Drawing.Font("Tahoma", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.labelMessage.Location = new System.Drawing.Point(12, 16); + this.labelMessage.Name = "labelMessage"; + this.labelMessage.Size = new System.Drawing.Size(287, 17); + this.labelMessage.TabIndex = 0; + this.labelMessage.Text = "Please wait..."; + // + // ProgressBar + // + this.ProgressBar.Location = new System.Drawing.Point(15, 47); + this.ProgressBar.MarqueeAnimationSpeed = 33; + this.ProgressBar.Name = "ProgressBar"; + this.ProgressBar.Size = new System.Drawing.Size(281, 20); + this.ProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.ProgressBar.TabIndex = 1; + // + // buttonCancel + // + this.buttonCancel.Enabled = false; + this.buttonCancel.Font = new System.Drawing.Font("Tahoma", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.buttonCancel.Location = new System.Drawing.Point(221, 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.Click += new System.EventHandler(this.ButtonCancel_Click); + // + // LegacyDialog2009 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(311, 122); + this.Controls.Add(this.buttonCancel); + this.Controls.Add(this.ProgressBar); + this.Controls.Add(this.labelMessage); + this.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.MaximumSize = new System.Drawing.Size(327, 161); + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(327, 161); + this.Name = "LegacyDialog2009"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "LegacyDialog2009"; + this.Load += new System.EventHandler(this.LegacyDialog2009_Load); + this.ResumeLayout(false); + + } + + #endregion + + private Label labelMessage; + private ProgressBar ProgressBar; + private Button buttonCancel; + } +} \ No newline at end of file diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.cs b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.cs new file mode 100644 index 0000000..0592603 --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.cs @@ -0,0 +1,50 @@ +using System; +using System.Windows.Forms; + +namespace Bloxstrap.Dialogs.BootstrapperDialogs +{ + // windows: https://youtu.be/VpduiruysuM?t=18 + // mac: https://youtu.be/ncHhbcVDRgQ?t=63 + + public partial class LegacyDialog2009 : BootstrapperDialogForm + { + protected override string _message + { + get => labelMessage.Text; + set => labelMessage.Text = value; + } + + protected override ProgressBarStyle _progressStyle + { + get => ProgressBar.Style; + set => ProgressBar.Style = value; + } + + protected override int _progressValue + { + get => ProgressBar.Value; + set => ProgressBar.Value = value; + } + + protected override bool _cancelEnabled + { + get => this.buttonCancel.Enabled; + set => this.buttonCancel.Enabled = value; + } + + public LegacyDialog2009(Bootstrapper? bootstrapper = null) + { + InitializeComponent(); + + Bootstrapper = bootstrapper; + + ScaleWindow(); + SetupDialog(); + } + + private void LegacyDialog2009_Load(object sender, EventArgs e) + { + this.Activate(); + } + } +} diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.resx b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.Designer.cs b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.Designer.cs new file mode 100644 index 0000000..2c490ce --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.Designer.cs @@ -0,0 +1,111 @@ +using System.Windows.Forms; + +namespace Bloxstrap.Dialogs.BootstrapperDialogs +{ + partial class LegacyDialog2011 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.labelMessage = new System.Windows.Forms.Label(); + this.ProgressBar = new System.Windows.Forms.ProgressBar(); + this.IconBox = new System.Windows.Forms.PictureBox(); + this.buttonCancel = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.IconBox)).BeginInit(); + this.SuspendLayout(); + // + // labelMessage + // + this.labelMessage.Location = new System.Drawing.Point(55, 23); + this.labelMessage.Name = "labelMessage"; + this.labelMessage.Size = new System.Drawing.Size(287, 17); + this.labelMessage.TabIndex = 0; + this.labelMessage.Text = "Please wait..."; + // + // ProgressBar + // + this.ProgressBar.Location = new System.Drawing.Point(58, 51); + this.ProgressBar.MarqueeAnimationSpeed = 33; + this.ProgressBar.Name = "ProgressBar"; + this.ProgressBar.Size = new System.Drawing.Size(287, 26); + this.ProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.ProgressBar.TabIndex = 1; + // + // IconBox + // + this.IconBox.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom; + this.IconBox.ImageLocation = ""; + this.IconBox.Location = new System.Drawing.Point(19, 16); + this.IconBox.Name = "IconBox"; + this.IconBox.Size = new System.Drawing.Size(32, 32); + this.IconBox.TabIndex = 2; + this.IconBox.TabStop = false; + // + // buttonCancel + // + 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); + // + // LegacyDialog2011 + // + 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.buttonCancel); + this.Controls.Add(this.IconBox); + this.Controls.Add(this.ProgressBar); + this.Controls.Add(this.labelMessage); + this.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.MaximumSize = new System.Drawing.Size(378, 170); + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(378, 170); + this.Name = "LegacyDialog2011"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "LegacyDialog2011"; + this.Load += new System.EventHandler(this.LegacyDialog2011_Load); + ((System.ComponentModel.ISupportInitialize)(this.IconBox)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private Label labelMessage; + private ProgressBar ProgressBar; + private PictureBox IconBox; + private Button buttonCancel; + } +} \ No newline at end of file diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.cs b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.cs new file mode 100644 index 0000000..03f34db --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.cs @@ -0,0 +1,53 @@ +using Bloxstrap.Enums; +using System; +using System.Windows.Forms; + +namespace Bloxstrap.Dialogs.BootstrapperDialogs +{ + // https://youtu.be/3K9oCEMHj2s?t=35 + + public partial class LegacyDialog2011 : BootstrapperDialogForm + { + protected override string _message + { + get => labelMessage.Text; + set => labelMessage.Text = value; + } + + protected override ProgressBarStyle _progressStyle + { + get => ProgressBar.Style; + set => ProgressBar.Style = value; + } + + protected override int _progressValue + { + get => ProgressBar.Value; + set => ProgressBar.Value = value; + } + + protected override bool _cancelEnabled + { + get => this.buttonCancel.Enabled; + set => this.buttonCancel.Enabled = this.buttonCancel.Visible = value; + } + + public LegacyDialog2011(Bootstrapper? bootstrapper = null) + { + InitializeComponent(); + + Bootstrapper = bootstrapper; + + // have to convert icon -> bitmap since winforms scaling is poop + this.IconBox.BackgroundImage = App.Settings.BootstrapperIcon.GetIcon().ToBitmap(); + + ScaleWindow(); + SetupDialog(); + } + + private void LegacyDialog2011_Load(object sender, EventArgs e) + { + this.Activate(); + } + } +} diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.resx b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.Designer.cs b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.Designer.cs new file mode 100644 index 0000000..3beb221 --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.Designer.cs @@ -0,0 +1,130 @@ +using System.Windows.Forms; + +namespace Bloxstrap.Dialogs.BootstrapperDialogs +{ + partial class ProgressDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.ProgressBar = new System.Windows.Forms.ProgressBar(); + this.labelMessage = new System.Windows.Forms.Label(); + this.IconBox = 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.buttonCancel)).BeginInit(); + this.panel1.SuspendLayout(); + this.SuspendLayout(); + // + // ProgressBar + // + this.ProgressBar.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.ProgressBar.Location = new System.Drawing.Point(29, 241); + this.ProgressBar.MarqueeAnimationSpeed = 20; + this.ProgressBar.Name = "ProgressBar"; + this.ProgressBar.Size = new System.Drawing.Size(460, 20); + this.ProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.ProgressBar.TabIndex = 0; + // + // labelMessage + // + this.labelMessage.Font = new System.Drawing.Font("Tahoma", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.labelMessage.Location = new System.Drawing.Point(29, 199); + this.labelMessage.Name = "labelMessage"; + this.labelMessage.Size = new System.Drawing.Size(460, 18); + this.labelMessage.TabIndex = 1; + this.labelMessage.Text = "Please wait..."; + this.labelMessage.TextAlign = System.Drawing.ContentAlignment.TopCenter; + this.labelMessage.UseMnemonic = false; + // + // IconBox + // + this.IconBox.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom; + this.IconBox.ImageLocation = ""; + this.IconBox.Location = new System.Drawing.Point(212, 66); + this.IconBox.Name = "IconBox"; + this.IconBox.Size = new System.Drawing.Size(92, 92); + this.IconBox.TabIndex = 2; + this.IconBox.TabStop = false; + // + // buttonCancel + // + 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.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + 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.labelMessage); + this.panel1.Controls.Add(this.IconBox); + 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"; + this.panel1.Size = new System.Drawing.Size(518, 318); + this.panel1.TabIndex = 4; + // + // ProgressDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.SystemColors.ActiveBorder; + this.ClientSize = new System.Drawing.Size(520, 320); + this.Controls.Add(this.panel1); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + this.MaximumSize = new System.Drawing.Size(520, 320); + this.MinimumSize = new System.Drawing.Size(520, 320); + this.Name = "ProgressDialog"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "ProgressDialog"; + this.Load += new System.EventHandler(this.ProgressDialog_Load); + ((System.ComponentModel.ISupportInitialize)(this.IconBox)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.buttonCancel)).EndInit(); + this.panel1.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private ProgressBar ProgressBar; + private Label labelMessage; + private PictureBox IconBox; + private PictureBox buttonCancel; + private Panel panel1; + } +} \ No newline at end of file diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.cs b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.cs new file mode 100644 index 0000000..74ba41d --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.cs @@ -0,0 +1,84 @@ +using Bloxstrap.Enums; +using System; +using System.Windows.Forms; +using System.Drawing; + +namespace Bloxstrap.Dialogs.BootstrapperDialogs +{ + // basically just the modern dialog + + public partial class ProgressDialog : BootstrapperDialogForm + { + protected override string _message + { + get => labelMessage.Text; + set => labelMessage.Text = value; + } + + protected override ProgressBarStyle _progressStyle + { + get => ProgressBar.Style; + set => ProgressBar.Style = value; + } + + protected override int _progressValue + { + get => ProgressBar.Value; + set => ProgressBar.Value = value; + } + + protected override bool _cancelEnabled + { + get => this.buttonCancel.Enabled; + set => this.buttonCancel.Enabled = this.buttonCancel.Visible = value; + } + + public ProgressDialog(Bootstrapper? bootstrapper = null) + { + InitializeComponent(); + + if (App.Settings.Theme.GetFinal() == Theme.Dark) + { + this.labelMessage.ForeColor = SystemColors.Window; + this.buttonCancel.Image = Properties.Resources.DarkCancelButton; + this.panel1.BackColor = Color.FromArgb(35, 37, 39); + this.BackColor = Color.FromArgb(25, 27, 29); + } + + Bootstrapper = bootstrapper; + + this.IconBox.BackgroundImage = App.Settings.BootstrapperIcon.GetBitmap(); + + SetupDialog(); + } + + private void ButtonCancel_MouseEnter(object sender, EventArgs e) + { + if (App.Settings.Theme.GetFinal() == Theme.Dark) + { + this.buttonCancel.Image = Properties.Resources.DarkCancelButtonHover; + } + else + { + this.buttonCancel.Image = Properties.Resources.CancelButtonHover; + } + } + + private void ButtonCancel_MouseLeave(object sender, EventArgs e) + { + if (App.Settings.Theme.GetFinal() == Theme.Dark) + { + this.buttonCancel.Image = Properties.Resources.DarkCancelButton; + } + else + { + this.buttonCancel.Image = Properties.Resources.CancelButton; + } + } + + private void ProgressDialog_Load(object sender, EventArgs e) + { + this.Activate(); + } + } +} diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.resx b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.Designer.cs b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.Designer.cs new file mode 100644 index 0000000..e2d27d5 --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.Designer.cs @@ -0,0 +1,51 @@ +namespace Bloxstrap.Dialogs.BootstrapperDialogs +{ + partial class VistaDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.SuspendLayout(); + // + // VistaDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(0, 0); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + this.Name = "VistaDialog"; + this.Opacity = 0D; + this.ShowInTaskbar = false; + this.Text = "VistaDialog"; + this.WindowState = System.Windows.Forms.FormWindowState.Minimized; + this.Load += new System.EventHandler(this.VistaDialog_Load); + this.ResumeLayout(false); + + } + + #endregion + } +} \ No newline at end of file diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.cs b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.cs new file mode 100644 index 0000000..35ee378 --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.cs @@ -0,0 +1,167 @@ +using Bloxstrap.Enums; +using System.Windows.Forms; +using System; + +namespace Bloxstrap.Dialogs.BootstrapperDialogs +{ + // https://youtu.be/h0_AL95Sc3o?t=48 + + // a bit hacky, but this is actually a hidden form + // since taskdialog is part of winforms, it can't really be properly used without a form + // for example, cross-threaded calls to ui controls can't really be done outside of a form + + public partial class VistaDialog : BootstrapperDialogForm + { + private TaskDialogPage Dialog; + + protected override string _message + { + get => Dialog.Heading ?? ""; + set => Dialog.Heading = value; + } + + protected override ProgressBarStyle _progressStyle + { + set + { + if (Dialog.ProgressBar is null) + return; + + switch (value) + { + case ProgressBarStyle.Continuous: + case ProgressBarStyle.Blocks: + Dialog.ProgressBar.State = TaskDialogProgressBarState.Normal; + break; + + case ProgressBarStyle.Marquee: + Dialog.ProgressBar.State = TaskDialogProgressBarState.Marquee; + break; + } + } + } + + protected override int _progressValue + { + get => Dialog.ProgressBar is null ? 0 : Dialog.ProgressBar.Value; + set + { + if (Dialog.ProgressBar is null) + return; + + Dialog.ProgressBar.Value = value; + } + } + + protected override bool _cancelEnabled + { + get => Dialog.Buttons[0].Enabled; + set => Dialog.Buttons[0].Enabled = value; + } + + public VistaDialog(Bootstrapper? bootstrapper = null) + { + InitializeComponent(); + + Bootstrapper = bootstrapper; + + Dialog = new TaskDialogPage() + { + Icon = new TaskDialogIcon(App.Settings.BootstrapperIcon.GetIcon()), + Caption = App.ProjectName, + + Buttons = { TaskDialogButton.Cancel }, + ProgressBar = new TaskDialogProgressBar() + { + State = TaskDialogProgressBarState.Marquee + } + }; + + _message = "Please wait..."; + _cancelEnabled = false; + + Dialog.Buttons[0].Click += (sender, e) => ButtonCancel_Click(sender, e); + + SetupDialog(); + } + + public override void ShowSuccess(string message) + { + if (this.InvokeRequired) + { + this.Invoke(ShowSuccess, message); + } + else + { + TaskDialogPage successDialog = new() + { + Icon = TaskDialogIcon.ShieldSuccessGreenBar, + Caption = App.ProjectName, + Heading = message, + Buttons = { TaskDialogButton.OK } + }; + + successDialog.Buttons[0].Click += (sender, e) => App.Terminate(); + + if (!App.IsQuiet) + Dialog.Navigate(successDialog); + + Dialog = successDialog; + } + } + + public override void ShowError(string message) + { + if (this.InvokeRequired) + { + this.Invoke(ShowError, message); + } + else + { + TaskDialogPage errorDialog = new() + { + Icon = TaskDialogIcon.Error, + Caption = App.ProjectName, + Heading = "An error occurred while starting Roblox", + Buttons = { TaskDialogButton.Close }, + Expander = new TaskDialogExpander() + { + Text = message, + CollapsedButtonText = "See details", + ExpandedButtonText = "Hide details", + Position = TaskDialogExpanderPosition.AfterText + } + }; + + errorDialog.Buttons[0].Click += (sender, e) => App.Terminate(Bootstrapper.ERROR_INSTALL_FAILURE); + + if (!App.IsQuiet) + Dialog.Navigate(errorDialog); + + Dialog = errorDialog; + } + } + + public override void CloseDialog() + { + if (this.InvokeRequired) + { + this.Invoke(CloseDialog); + } + else + { + if (Dialog.BoundDialog is null) + return; + + Dialog.BoundDialog.Close(); + } + } + + + private void VistaDialog_Load(object sender, EventArgs e) + { + if (!App.IsQuiet) + TaskDialog.ShowDialog(Dialog); + } + } +} diff --git a/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.resx b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Bloxstrap/Bloxstrap/Dialogs/Menu/ModHelp.xaml b/Bloxstrap/Bloxstrap/Dialogs/Menu/ModHelp.xaml new file mode 100644 index 0000000..4775db9 --- /dev/null +++ b/Bloxstrap/Bloxstrap/Dialogs/Menu/ModHelp.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + +