From cd63ee3cc7750ede59ca3cdc927559600e05e99f Mon Sep 17 00:00:00 2001 From: pizzaboxer <41478239+pizzaboxer@users.noreply.github.com> Date: Fri, 27 Jan 2023 15:25:13 +0000 Subject: [PATCH] Port files to WPF application --- Bloxstrap/Bloxstrap/App.xaml.cs | 162 +- Bloxstrap/Bloxstrap/Bloxstrap.csproj | 20 + Bloxstrap/Bloxstrap/Bloxstrap.ico | Bin 0 -> 123254 bytes Bloxstrap/Bloxstrap/Bootstrapper.cs | 800 +++ .../BootstrapperDialogForm.cs | 163 + .../IBootstrapperDialog.cs | 20 + .../LegacyDialog2009.Designer.cs | 96 + .../BootstrapperDialogs/LegacyDialog2009.cs | 50 + .../BootstrapperDialogs/LegacyDialog2009.resx | 60 + .../LegacyDialog2011.Designer.cs | 111 + .../BootstrapperDialogs/LegacyDialog2011.cs | 53 + .../BootstrapperDialogs/LegacyDialog2011.resx | 60 + .../ProgressDialog.Designer.cs | 130 + .../BootstrapperDialogs/ProgressDialog.cs | 84 + .../BootstrapperDialogs/ProgressDialog.resx | 60 + .../VistaDialog.Designer.cs | 51 + .../BootstrapperDialogs/VistaDialog.cs | 167 + .../BootstrapperDialogs/VistaDialog.resx | 60 + Bloxstrap/Bloxstrap/Dialogs/Menu/ModHelp.xaml | 39 + .../Bloxstrap/Dialogs/Menu/ModHelp.xaml.cs | 34 + .../Bloxstrap/Dialogs/Menu/Preferences.xaml | 177 + .../Dialogs/Menu/Preferences.xaml.cs | 427 ++ .../Bloxstrap/Dialogs/Menu/ReShadeHelp.xaml | 54 + .../Dialogs/Menu/ReShadeHelp.xaml.cs | 34 + .../Menu/Themes/ColourfulDarkTheme.xaml | 4505 ++++++++++++++++ .../Menu/Themes/ColourfulDarkTheme.xaml.cs | 43 + .../Menu/Themes/ColourfulLightTheme.xaml | 4555 ++++++++++++++++ .../Menu/Themes/ColourfulLightTheme.xaml.cs | 43 + .../Dialogs/Menu/Themes/DarkTheme.xaml | 4644 +++++++++++++++++ .../Dialogs/Menu/Themes/DarkTheme.xaml.cs | 43 + .../Dialogs/Menu/Themes/LightTheme.xaml | 4461 ++++++++++++++++ .../Dialogs/Menu/Themes/LightTheme.xaml.cs | 43 + Bloxstrap/Bloxstrap/Enums/BootstrapperIcon.cs | 81 + .../Bloxstrap/Enums/BootstrapperStyle.cs | 50 + Bloxstrap/Bloxstrap/Enums/Theme.cs | 32 + Bloxstrap/Bloxstrap/Helpers/DeployManager.cs | 93 + Bloxstrap/Bloxstrap/Helpers/Directories.cs | 39 + .../Integrations/DiscordRichPresence.cs | 210 + .../Helpers/Integrations/RbxFpsUnlocker.cs | 111 + .../Bloxstrap/Helpers/Integrations/ReShade.cs | 402 ++ Bloxstrap/Bloxstrap/Helpers/Protocol.cs | 112 + Bloxstrap/Bloxstrap/Helpers/RSMM/Package.cs | 17 + .../Bloxstrap/Helpers/RSMM/PackageManifest.cs | 60 + .../Bloxstrap/Helpers/RSMM/SystemEvent.cs | 43 + Bloxstrap/Bloxstrap/Helpers/ResourceHelper.cs | 27 + Bloxstrap/Bloxstrap/Helpers/Updater.cs | 65 + Bloxstrap/Bloxstrap/Helpers/Utilities.cs | 84 + Bloxstrap/Bloxstrap/Helpers/WindowScaling.cs | 38 + Bloxstrap/Bloxstrap/Models/ClientVersion.cs | 19 + Bloxstrap/Bloxstrap/Models/GithubRelease.cs | 32 + .../Models/ReShadeVersionManifest.cs | 9 + Bloxstrap/Bloxstrap/Models/RobloxAsset.cs | 13 + .../Bloxstrap/Models/RobloxThumbnails.cs | 17 + Bloxstrap/Bloxstrap/Models/SettingsFormat.cs | 41 + .../Properties/Resources.Designer.cs | 263 + Bloxstrap/Bloxstrap/Properties/Resources.resx | 181 + .../Bloxstrap/Properties/Settings.Designer.cs | 26 + .../Bloxstrap/Properties/Settings.settings | 6 + .../Bloxstrap/Properties/launchSettings.json | 8 + .../Bloxstrap/Resources/CancelButton.png | Bin 0 -> 768 bytes .../Bloxstrap/Resources/CancelButtonHover.png | Bin 0 -> 944 bytes .../Bloxstrap/Resources/DarkCancelButton.png | Bin 0 -> 1134 bytes .../Resources/DarkCancelButtonHover.png | Bin 0 -> 1175 bytes .../Bloxstrap/Resources/Icon2009-ico.ico | Bin 0 -> 16958 bytes .../Bloxstrap/Resources/Icon2009-png.png | Bin 0 -> 16441 bytes .../Bloxstrap/Resources/Icon2011-ico.ico | Bin 0 -> 16958 bytes .../Bloxstrap/Resources/Icon2011-png.png | Bin 0 -> 7754 bytes .../Bloxstrap/Resources/Icon2017-ico.ico | Bin 0 -> 16958 bytes .../Bloxstrap/Resources/Icon2017-png.png | Bin 0 -> 5563 bytes .../Bloxstrap/Resources/Icon2019-ico.ico | Bin 0 -> 16958 bytes .../Bloxstrap/Resources/Icon2019-png.png | Bin 0 -> 13984 bytes .../Bloxstrap/Resources/Icon2022-ico.ico | Bin 0 -> 16958 bytes .../Bloxstrap/Resources/Icon2022-png.png | Bin 0 -> 5968 bytes .../Bloxstrap/Resources/IconBloxstrap-ico.ico | Bin 0 -> 16958 bytes .../Bloxstrap/Resources/IconBloxstrap-png.png | Bin 0 -> 4034 bytes .../Bloxstrap/Resources/IconEarly2015-ico.ico | Bin 0 -> 16958 bytes .../Bloxstrap/Resources/IconEarly2015-png.png | Bin 0 -> 5941 bytes .../Bloxstrap/Resources/IconLate2015-ico.ico | Bin 0 -> 16958 bytes .../Bloxstrap/Resources/IconLate2015-png.png | Bin 0 -> 10100 bytes .../Bloxstrap/Resources/Mods/OldCursor.png | Bin 0 -> 232 bytes .../Bloxstrap/Resources/Mods/OldDeath.ogg | Bin 0 -> 6271 bytes .../Bloxstrap/Resources/Mods/OldFarCursor.png | Bin 0 -> 235 bytes Bloxstrap/Bloxstrap/SettingsManager.cs | 83 + 83 files changed, 23410 insertions(+), 1 deletion(-) create mode 100644 Bloxstrap/Bloxstrap/Bloxstrap.ico create mode 100644 Bloxstrap/Bloxstrap/Bootstrapper.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/BootstrapperDialogForm.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/IBootstrapperDialog.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.Designer.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2009.resx create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.Designer.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/LegacyDialog2011.resx create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.Designer.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/ProgressDialog.resx create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.Designer.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/BootstrapperDialogs/VistaDialog.resx create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/ModHelp.xaml create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/ModHelp.xaml.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/Preferences.xaml create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/Preferences.xaml.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/ReShadeHelp.xaml create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/ReShadeHelp.xaml.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/Themes/ColourfulDarkTheme.xaml create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/Themes/ColourfulDarkTheme.xaml.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/Themes/ColourfulLightTheme.xaml create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/Themes/ColourfulLightTheme.xaml.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/Themes/DarkTheme.xaml create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/Themes/DarkTheme.xaml.cs create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/Themes/LightTheme.xaml create mode 100644 Bloxstrap/Bloxstrap/Dialogs/Menu/Themes/LightTheme.xaml.cs create mode 100644 Bloxstrap/Bloxstrap/Enums/BootstrapperIcon.cs create mode 100644 Bloxstrap/Bloxstrap/Enums/BootstrapperStyle.cs create mode 100644 Bloxstrap/Bloxstrap/Enums/Theme.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/DeployManager.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/Directories.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/Integrations/DiscordRichPresence.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/Integrations/RbxFpsUnlocker.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/Integrations/ReShade.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/Protocol.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/RSMM/Package.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/RSMM/PackageManifest.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/RSMM/SystemEvent.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/ResourceHelper.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/Updater.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/Utilities.cs create mode 100644 Bloxstrap/Bloxstrap/Helpers/WindowScaling.cs create mode 100644 Bloxstrap/Bloxstrap/Models/ClientVersion.cs create mode 100644 Bloxstrap/Bloxstrap/Models/GithubRelease.cs create mode 100644 Bloxstrap/Bloxstrap/Models/ReShadeVersionManifest.cs create mode 100644 Bloxstrap/Bloxstrap/Models/RobloxAsset.cs create mode 100644 Bloxstrap/Bloxstrap/Models/RobloxThumbnails.cs create mode 100644 Bloxstrap/Bloxstrap/Models/SettingsFormat.cs create mode 100644 Bloxstrap/Bloxstrap/Properties/Resources.Designer.cs create mode 100644 Bloxstrap/Bloxstrap/Properties/Resources.resx create mode 100644 Bloxstrap/Bloxstrap/Properties/Settings.Designer.cs create mode 100644 Bloxstrap/Bloxstrap/Properties/Settings.settings create mode 100644 Bloxstrap/Bloxstrap/Properties/launchSettings.json create mode 100644 Bloxstrap/Bloxstrap/Resources/CancelButton.png create mode 100644 Bloxstrap/Bloxstrap/Resources/CancelButtonHover.png create mode 100644 Bloxstrap/Bloxstrap/Resources/DarkCancelButton.png create mode 100644 Bloxstrap/Bloxstrap/Resources/DarkCancelButtonHover.png create mode 100644 Bloxstrap/Bloxstrap/Resources/Icon2009-ico.ico create mode 100644 Bloxstrap/Bloxstrap/Resources/Icon2009-png.png create mode 100644 Bloxstrap/Bloxstrap/Resources/Icon2011-ico.ico create mode 100644 Bloxstrap/Bloxstrap/Resources/Icon2011-png.png create mode 100644 Bloxstrap/Bloxstrap/Resources/Icon2017-ico.ico create mode 100644 Bloxstrap/Bloxstrap/Resources/Icon2017-png.png create mode 100644 Bloxstrap/Bloxstrap/Resources/Icon2019-ico.ico create mode 100644 Bloxstrap/Bloxstrap/Resources/Icon2019-png.png create mode 100644 Bloxstrap/Bloxstrap/Resources/Icon2022-ico.ico create mode 100644 Bloxstrap/Bloxstrap/Resources/Icon2022-png.png create mode 100644 Bloxstrap/Bloxstrap/Resources/IconBloxstrap-ico.ico create mode 100644 Bloxstrap/Bloxstrap/Resources/IconBloxstrap-png.png create mode 100644 Bloxstrap/Bloxstrap/Resources/IconEarly2015-ico.ico create mode 100644 Bloxstrap/Bloxstrap/Resources/IconEarly2015-png.png create mode 100644 Bloxstrap/Bloxstrap/Resources/IconLate2015-ico.ico create mode 100644 Bloxstrap/Bloxstrap/Resources/IconLate2015-png.png create mode 100644 Bloxstrap/Bloxstrap/Resources/Mods/OldCursor.png create mode 100644 Bloxstrap/Bloxstrap/Resources/Mods/OldDeath.ogg create mode 100644 Bloxstrap/Bloxstrap/Resources/Mods/OldFarCursor.png create mode 100644 Bloxstrap/Bloxstrap/SettingsManager.cs 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 0000000000000000000000000000000000000000..93904fb8b943e6451a9eb2a4fdb1262882464c16 GIT binary patch literal 123254 zcmds=2b>kv^~Y!FNEJm;6tIIODp*j;QUpX{cM-6FG<)wFV+q(b#u8&UMiXO=i7~NP zj7DRLv6mPHdx-_52r4`O@9)mL@7*^$@4cC~Z+GAO@8|Q~>Gz&F=iYPAy?17w=T&*_ zyzbpSmi@hh+j!oAp6B)M9c-W1(eqwlU;qBW_N|<^M_13=dh68o#-3N-*YoDgNo`-| zc{lIudGqH7+XFgOc{Oi&-T>0%QgR6Pv+z99eS)-v{j0C9_YA96dG#w+daHcj^Eqhf zx`F5KHocucqGkhs_@rrY4gB0c@6uK}&s=BEZ@aeV_Z;Z?hyJvcKWcJUuIa(KN5G%p zZCDOIJnxU&qleNr*Wdk6l|OoSoxcso_JXDgaC&5$j z1=NGiT?LZ&cd2dndY`-w_r0E8{rBJN`fDT489wQJ_8*et*scCS$F=!E_dcR#56-do z=H~MR`*(&9;772>B-_L2V0EM8QFR-!E&bTzTI&4Up)YI?H^Gze3s8Q;{4rD3G}rI3 ze|xt74DR)xu)QPb{=~X41`dWB;C19a-dyjkrh7PweUjl;I2y*mdSI`YZQcVnkGuWJ zvpbqJs%zKanCvwf&VWbYX4n?`fb8%CxYsZAN7W7Ry;h#T^VF^!yBofD&e8cFf@FEr z*>BTWf!s4qc|G~;)*SzR#5p=w_a>R_b?)-_^($6*ew*IX!GSr>xrP0@FITp&*?u0* zFlEqN-SbD*4Pskm&`n?KZ^JNn0B$3nBPhQ^{BhIP@Xx)Zwd#P;;Vyf2z{bG${${H8rR&&HM7v|K))_!pHzKTc~zO0 z%%<*Wq9^Eu04`Umt_&-537I+Ce%$b$xbvt5z8q zjm_Hi@%;4%d;WyI+V~?Tb@zwv){5)pcm4(oVI_piLp}C6!1uby4!UQP{`ba@w)Xt~ zg99HtqGolj-45h~RR8aRwc%6Sl#a+xzDjw_8U(vX1@A+fCQ5OV<6MJE+bImAPXgC)F+EQ|e}S;MhH&bme2;fji-N z7zUEhrfZj~6Q6=Bi(UT+zdz8Hqm&I)zs`jTupUSU>%y*ZIH(-hv~_G6d;>O3cljea z+S_ltP7v$hll3j=c!cH&|a+L>r&C~wascRZPBsWrS47o)4k<7NBjRr-5r+l zbCe?!&m30Z8eJ#7Nk_WZeCgw_C!L&0UpA=So@=DneAoS#8(V++IkCc|Dv)E{wWUY4+^p!z2vHp7CP>+~LzDz!8ZIIvjDf-{Y*f6B)1bH{e?`y?J>W^+S+Vj`hQhCZx zZ4LXvweSLb4c}7+X8B%c8($T)ch>!-;ziS5bnNT-TUL9-(W);UVH21N7s3^H4U`3UJCrRU83>A0)YO@?^>;GI=}A9Hni!hJyWb8UHr}2Z;$6*j;J}(#Ex`?c8@2)EN^QpC1nH>%+80#6 zUrTV^Qt}$-dmXasA^p-mr19?r>3qkzyMb&_IIhpO(P`%XoigAbc&v1J9mKJrJ?E@c z{)Z>L?mmCgKAnD4TahXQru_)UxuX1r%`d!sh|UCpuLx0dlKC1 zqtK?|x8W7IcZqMP^m!LS-O~+kd z#l7yQxI9${s#G8RUH4WW^ltbcEP!8t#@RLi`FrIdTdoJvwawFAO4o6G;&1%Z&a>ko zCVr{E{uY&itzj#WEH>SqtXIPcptAfH>!gk7+&YQG-8>aK8JuvxK*A69}uOZ*H?y+|{ zcYRn3UQTeG+T*FI{EZGw{wkNlC;clyI<8MtxpZaAx2_oLTsMEyk2v!})$wZ*q@z5} z1C@b;A)oJ&UKLYb!@bX;Uos#oe`AAAs)utqCrNw!80W7BgF$u2-nV?gn{X>>9z~rS zN?X^{KmXEJ>W@g@PW~AiRM*M}ed?rR)62C~+pV~{E67JZ0Q1Om7k~8R_52x!v@^C< z`Bt8ae^#aYaIOqhOXZ7$xrfI|BUgTQzjUO$7m(MsCXX?cAI8{%{5qKYLVax;f1CIt zeE*Vdg=82$>4F^jf5ZM4;X3l%ADs@;_`iP+b0?`fY~m|(k7hhQUw-br8~v;AWo$E| zW_R*X*_I9E^Z!VG$C2L%=GuDsKRtu-j*jO3UQt^fonO>FX61=FvbNDj$`9;Lovt~H zJoms)Y~)Ybx4rMRCBH5i8wN3?dw&Jvf=WBSNh5nSc#wj#cGzZe-6dOg4Q*cVo zKli?3tcP{ha9e$1=D$rj4%%gvNl^ugfp)?+!~CO9&BPHId!%ieOW+tXrqkfuhVn)2 z3;OYa%sWm9DAxRq_CA;>0 zK^-8Lrw;sp?lQWJmS1H8+nDjpEk=0$u!(`Z!zQ*OtuI0SwNFTrB4d4J5h;ta_z zTRaB#Sg}jRYeyBT1Ex&$*u?X9+BJ}O)MUxf1-5`0a49?mibryd->Iz30$rx?X!%B#or|PO~rS#;_!#*)?I_`O|5o7c> zHpnG^P+rwHR4f#ykL#xIuK$zk+N5{DJzmWE66)LpBfsin<{r{@fINo5XC=r> zK5|Ep{EGS2HWedt_?n+b)B#ghRbT0=sBSJOL0-5TH%(phG5nEsV4X<$@vCO8E{MC9Buqa_jzy%Q@+kS`+pdGFL2bF( z#vdT+I_;CKzJzDtDwqRXLwA4Xp^@cB;~ly$Vx@kZ_d4WCyU>2cE4u&ZK(UhKmv56T z)DO`1wh(S>bj@pUGaLnKk9vWkt>}3!3KMaaZ?uMVkF3=B@f7CVI*}o0cFxPCJWp>)(;2tt{;Aqa19;0N; zw@-1>bco8|-6x$&md!vu!#!r#)plK%6E2s4zj14gcqkez( zIA1=Q!^bY?S{uI^Styp~kNsA*sHJSRF35H#!(ZUP5EcI&XXMZBQ=0oQbzo$j%ALw@ zShhmj%Ij9>0$H+0+=Cq_8N#`T!j14Ud;^8@x!tds+0mINP1`_YiRKxhz*iJXN9E-` zxF61diJ3gzV<*Vz6B#s=B3gc)}=&ua(Isf2ypkK`l2 z@elYFsH|@eX??S92NPBf)$V}AGIA9z<8JsSA%?``UCK4GgXB@Xwkt?}mBZgbTsiZdcd7sL?C1V%n{4J-N*hnh4w7FS z0!e*l)P4Mec-lU_{irJh`KiEK9xHgE9}7f=Osv2&suD6%8<%^K)NmS zc>#|yG>&|V^%3qpE+Uq*qFim+HHG zmgJWYy%dU(Ieg9a%=5R4lHbGxw^$NyokB-@HHB&yhc4 z16v11m}js`QQy>7^Z?ml3wR$~S&CWzhB96gDZh;+^lWJG+)pv-yVt1A+7=|g+V=;* zJ)UG;ZJ+uBk0Af9k@6E8nL403@65Y;N%9DvtNL~zNPcl9gwM&ftu|3(0MEm<$h;rE zXRzsuX?`Z-e=_Aqv7O4$F{ifX8ud$ZrJZE|Z%N0-c9U3FJD2Nv9eV?AgJY3-1kb4U z_V+uwjnS+8J4i;wxRLi@>HyC$>KX6G&jm@&7hr9W{x^XCfR$f9Y7yKAr<3Py#QE#< zJb3C|g+N~U6%%(lv42kaGj%}oDj$H&tJvjY(ijbrzXMzgPs1h1ySItScG2?ae=hi>RW+<)kdnguq2#S^4UZ*lV+uwGqSAX9l9=@-ai2*LUl4oWD zdDC%y9(g6ROHTc#>VT;S!E>Wlj=0NZ$g48)CpZxukK)x^I1^O#0n{3}aart8DwVxhVgx`$XYTlsP=mA}{8^A@L; z<~+8umi9@eT+7ty&9kVQALP?|LH5$_5B4_K|M{tmaxE96j%O{qd7jxu`Fc)k4rn|5 zoYp1DpVQ*w!8_#0@fKrx=;rZec9w>52d{&qW)e-rAzjC7uL zzx6-<7>t=p-+KP3x;B^2yQ0$ppfRQ6;3km1KLgF(gyX_H>2KL@FBK{eSu&Nf4iusH zLb4`3o^@|Vp6`E1>74KD=y^UpCztKpHAbbeqdqVS4udOU0chMneZ@j!GB1*LFg~dG zD`p!Y`HJ}~LeEw&g=EWh++Oc4b*{Ue_9;QENg3C>P!07h{da_3puX}<(0H5roNt01 zOAnXveA{1e-JEo6+XVV=aIfKe&%K>Xr*ZdZ>o}Z`eJ)r&Mepnf&#Z>axZ2G%K>DtM z)8Nk_eM@)hOXkyWoMRVY?~dkMRQcP02IVGS9Vk-oadlhhylCBj_g&yq6suI5cm2Zp z);OHX`1&vbtZ(YT^5u!{D@y+f^j;{hq{pLmo+|go zw?vom&gfI)ecQu9a1}fb??9okl(;(1ciw-=hi_Z)KJ3bsh3+})9*fX_q4bM69>^4o zX9Ulc_vagJ(YjW7SGy@6J{b0ZbKriEzP|>If#=g#sgKFV2l)pb-^PrCCh$Q8^e1#O zPT#(}WWK+&E#JTy%y((Bb!oTN*IW7eTw=$!8hWpp0$*I+QJ$z1onlC;68X46r<%T#vsb`l zARoRGit#B)?@fKI10k$``MeWBJ}PONO?Ccj#Gylcud7$Tbg7LIb1ie}UwT)&ITPf= z9{|OL-$GN#T`1j4$y?(*VH-?j-@+D@D=fF-(3$?_zt!()#Ca|xZG*J`3Hp~Ola3uP z4Cm1j+v)V@a7>`cS7gilG`me54+@pR^ImH$)AWu{qpmEX%Axal5u}@nl?D$a}j!$%Y45tcz>mw z+JNQgcRqyM0QsQlP)_l}d&pB;l>Yf{K}!EUN#j;1r!mGqBTrNtupRqfhL$MnQF*Fw zda{4W39Xa%1L}Eh-?Rav&2L(%f2p*h@@R>D8tag*!)@?7?9*I9OURTf&zpEQF8Ee@ zf;makUyu*d?=}Qu^%~pBl}B^$S9>%I!unSnIv>>kX>OTv<@W+{D8IYLa}oT`O5A>! zIZwaKqTh-OzMb%78Qqh7q1sx_0f%icm3```<WYbj=nn+_ z&baecxnwEj{&$dRIE3}ziG6>7Qs&jzwC-i@WNoAW_~X$T{a3A}dmIb@DWm(+Ji`H? zK8kCDHf$dc-$G;cR;ugY2-<+uyJzvnA~O0{e>nKw&m(1YPikYY0Qp8&|611=-3JiX zpZf6{BWmhcPU(LE8n0KZd_4MJH~ns968+O&q}l+DG5#99hwy#1)b>A+C8`bR$Nnc$ za;iV3{-egR&I8qrXQ0$FtG>Mn55Pq*6Sjt4(9s{Vi=GKe@=Qcf{?WhYG5slXRnO-_ zDd%(>tAme_XB>pfzvf2|hfCpLP&*+#$T#VEBh{P6*-LrtGGtMCmR?;O=-#DgwZ-m!yDnWN z9j8!xqdw4!p!WS((ERrR=n6LNhRdP6bAu00z5ir&oNt+}f94PT*8G-t@chBwSe8YIxtd_f>Pgd~>0^Ro7pJzrxut1qQ;Jpt0fjdFnZCzDa3(P~m4o zvu$I?B1YD&MxLibbGHF%YZt-YZ~@E)jX$go;j$k`C%w7uesJ|KpQ848A>0QS!oIL0 zNZ)Ot3gV<6K1cPj9c^%Deh;;s#x@J|!?N|SKAU{d>KiNF$?z4pvN!d*e8qg&5Tg3L z>HF2(MXo=twdbkq%8t4Z`Ea$z>R%oLBSHCe1bcolOT{Z|!B(&jTnxYB*|=aly6|%m z**5s*+bUnbsTs8C&$Dc5oxADF_nrv_{Y)<1Yd_apR35A07}yP@Z`GxO>2r=vBWl@$ z<6A-voC5NXuY+Py`3x;5nYKW6z-?m-tuuC)ibG}78^P62bF8b~=?Pi)oloE4W1(wh zgJOJ_&0n@s8Sf9fz;W8a3EX>kHI^jI%;LjwUj;hHuvPG8<(jA>9GhS<8MeuF5Nxt@6 ztM50s{?Bk4)WT0d{v}aAGpQX&KW#(ScY*(cm906Js%LzUIM#eX#{Vb=%FIc2A&m#>PF<-3f6&kpx7>!enaOe->hpJsek#mD?sgh_&%C)ThB+Q z#v+zwm+?Y<;l+$a1kXimkIb*7_F%W#pv2z)0XSymj;0R)JRCT z3HkKTc{Yt)OWATiP(0t1`VOc4A^D9>eG@&I{?!I>)3hP#3;q)BfpcIgNZ-lFXB+BC z{%unj2WlgK4==*!@CWDyX`SY>fqPDlb;bMBKy@{&qo&-}_`%Wso(Hsx@!hy=``EFF zQFV&nU*iR~2SHl@G1pbrRhJaU4ub>XSFiw|{eTJ(vCmHWMCAoAYzbic`=9vMiNB4tbBE<(`dEIUK zIbD-0?Wp}~H@1Q^LG|@jXsZ5;D{H>>7a5Px&A4#Os@akZBgG2&8vT6 zpx_&h(f2Q%=>Ap4`-9qH`S9!DDUiOu2kEic`~M5q_66nD9@c}M;m8!Sb=Wpru8D1MT@!*yT+`{Z|%q}P<^s{BNKJ2XoF*>R{HiwK?pyqt6tQ#GZmNz#4}AFxf;jVUWpq!{*|pFe!ZsMgzPF&M^%^&Q#cT)q2OQ&m zJ2amD`Oc{MuHo3JiuKQfa%uyfL>|TbVg0YmzK0;LewzBcACcuM=EQ<;hX!L2uKu-d z{ZhkaW=?D->1cecspXBEzGRsQVf`yE(zuWMfN|4m>hqpqJSS)ag86{t`X5oVE%)>9 zGP)<}_h+EG>Doa3g4ysTG__virvEYhfbnK-CFqC6*FSZ`v;kxI4TxYqF>anseV$^w z+o318`q%m(D5o}H3G$p|f5$}Q>F#)2UjIWJYMzUzo27BDc~DMq=&Q)HC4}|gn|&HX zDyOlC+x#(8x~nYfFQk8MH}w75mwGVWY<)ioP&KmRJDds>Ms7eLflbshUPR@Bsbjhnv8t@(Cn zPy-s&f#~ut{o6UoF;g^8`&%fdIP_lR=>=gQr1_X@O~NH zm*z7MhOqv%eLS>eS&{$KoY38H4veJ@?p&1qmn}=R0ks>GPC4~2RZlL1&JebN<}KcZ zxOJ&9=gGgw)(b&%-50}t#Qi%l2h-E9*{7X<(wVLFTarcTpMIO(>)`h+bf4;9Hb&M` zU#t1CO(3lQ4cYhiCX`jN_{X4j<~MLCj3mz4$e*xRClj-vY%R@mDC&EG)xVap<52ma z%vglxAHRlD>#^AMK0ub8A*_GJq1S-MV2VxKy-sEPQ+NUX1g8>{?}olN#IN-5XCL0y z_c|JVr+@!DrE}g7PQMq~X#J0<8Ajf(f-74~uCG9rqd1Pq-0|rF@UVr}X9fp56RY z&f)jiS8w3MjgIM;CVJO8WtXD$A3U2_<)87(R>-+0d=Aa6=Un;8FRRb1XR+Kmpm;%L zMRi2iYD_{g>q~GeoCZ_S_h$assa<$x)6A3DwiBOHW#+BEW9~3!`A%B?BkDlzI%M16 z5jC43w_@8|`e^R`nrAv4f;PbXUMF@4ew%3u`_)f-5YB_WDC64_7p~*aJhY9~^_Tc> zwI?DLwe2Z|kv=5QrkbZ6Xrta2%Pkh({I>!#B)4!feQCZUcX1)dH zmdU1AUqW7r!ykrAU^e-0gMaBwpL{jz!;S8fymM7z-|p%;*LtJNzkN1t?9}e);TkBl zzU5Qo*HpJ2hpXUl^gRY2zAiqzgMaMLi3$&q*onHKnV!b0leZmXVtI18IFhXCN3N|t-BfjPU%~}JCSZT%345ozSrf8c}6xQqO7G5Xi|tn__1sBb@wGQJsQyr|7 zV%c8)B*teix~i4+De2!2 zzlNx}ZjH~%hd&R$fna=gWZlO3#%E1jm}mS78KY$?et#l;TOAdv<9yfIv{TF6_ldui z5B3i^QDcmALB2$F;4{!z%pGtV<$EIUp>0eV@8QonjPco?#;2^ncG*59Gd|k{`VHSl zQTmonn&U6F4djbe2Zrvx1-iZh&V?DYnS*)va&^Az+P41b=d#CV^$f5XpQX(#zPu*A zHnF~&L(jSLNTmONVS|lN>QX;sul4-hW^}OrTTAoKL13exui3n@VcF{3_x%Ma-Loo9 znZJ3N-QGMuQ`M^d74{UbI(h=ez6aXEr#VYrVB|ZDZEm z`N3T4PF5AL+B#!E&t0!}_Rq_;?pSk(VN?&4q^&!mIPqJ7lEl;J{-|DW2 zWm20p=KQpNqvSNt3=HEt{e|m$N6uLjHh>XuFkB9g!!vLMbModH{`zF!x6P8ToXbFz z{$lQHjQx^co2AbN@-18L`M*8tbnSA4tC2u*|zk%+Yd8z`(;@*2H&dbtY1G!eMGV`sxXJ6>MO&Tle3hRKL zpQr;p|8YCK0w03ralePC@zm#N12W%Njz8v~C4WoWzk%-J+gtZg3R}iaBTHvtxi!zo z?@f5j738bqYQsB$+HgIaH5QJ6>)>fv2wym~j^Kd`f(sWZh45mVvPD z5^Tq{Rh;we{pUK@9*8kdi%--Y&l z$#thw=GIVu&WvN0q5TusH>~Siwkc+RIBmOa)3V#q%kNfr^*7x?8$P9wz4g4=>Y#bB ztza)W8~zG<=Imqm27ZJ>_APW=bMk+t4e0NCoxJ+Y_eAVHTuAsZJu{wnt0Aw74bx#%K%Kx6D5 zTc^=&dlGqzIXAZbjlK8Szfc>l`o9{i1$zH>G#mlfz?1MU{0}s@UC4H&IQ|j&%wgWw zykBFUdoRNNvSE_HM%cU<8y0&_ZrfA8Q|};oJZD|Vr?*GPYe0V(0(0Raco5!#Phlyj zUsej6CP`y4d0t3AF!-GtJs;6T_Ak=rN#tqlxw-8fD&OcVpUu^V>sh)!ptyf$~stn1&a%;6=`Q9~)@C|AGx%!D(cXb(BPinKqoS&9A zB!k&++VH&(iqnR>_V0=Qw;OIL^`i(s| zuf6M+F7^ELf9?6B3y!V2_71OWEJpqDmTJS}*f`%glKX060DkAFjc4BFXd3%B)|PRu z&u?%2uJo5*>%IFz{jxUbq#LLW?+^0tRS>qlwl&_aG1qeS@A+)6V~RVUAO;v{#)0&V zRb|;fx6OHi!T9tka|`)&+1AdFZp!+2I1bc?-vu?GvA8JvYrIB%cJAGBxtIe9| z?|o1^^Q?5TcV5!^k81brz652eQ29&e=d>y5ea7B14~sLl>duc2=l&0Z%ivMa*s9ua z^}`jroeAp0McH5RzRJhDP_FhZB$vwW&*;kr-}P*QvA{;yKW*D6doynB`Ilc8cYaiL zbv+maGvGY98{GNPRcyQZl3!Qftv^KBU;8w6_YaUSb+4_Ib(Qm*i2>F%^TB$bv&3yc zvFSCiIrF2Yj!&FXs0~-YtShVyn?Ws{3b(<_@F9E+amH5FZ`asOF8eEwYeBJ8rPw?y z=L^ID=9_|M9H7MW!3FG}w~iZoPunj}8?O0Ljje7BJHyd%9XtgKA<6uxs~?To>L{@UUw0wY(?2$zGe(4&q}hnE9+OtewIJ7Hu#Mhz7ZF)S-$Nod)k=KT{iIJ?tG5s zM{mE&^LI`>Kf0%pIf&6;XT6m3qpof=-})eI1X1?aKKUG-Q;NA+H}B?J{~p=zq77JI z-~B4}Tu_$%?fhsR^P?mArblj_w&Ixj-fCYJV@-sg!Oidj$Y-hCO7^DK(_7@BIkPmXNtE6?wzCZ8tb~a-{4S#F0=r(|KqjaWy z?)>NuY|nuU;Q>&clit;aw{+i1GAPDh8*<%?`UvVPRFchIS>?Mfp&!`G*uRAH!H&+_ zvhL20PG$Ra_%pl;inSNR58%qy((5XJPlDQ^T=wtJF^v_hgqD`KnEVyHJVXqzvClhP zrJN5YUFl4A9}6eK@8LOk4>ZPB2|Cjl-y~2Ojw%D{i<}9HNs7^bIb8D}WFPAvcw8Iv zj7y2ef=RO#$UiOs*?T#Z!~G_aL9xflpmD?~`>QS61Kx)u>6X*E>N6khkK3c0X#-01 zZV*02`qDVu(-o{6jg#I28bgk^bz~n3RS;DMdb9s3SOVpKFS+D<8T${)!~n714T_8b(!YCFU90ikx$sd1 z>qg_G2S6u?vVRx$9|n~i=X{9#Gx$zSM^gq?^8NZEVgTy{G?&0Ue(;UGU>u;5$4S3M zHY)MfL&G^pTX;yY2M{@ZWOkE+u+=~Q?Ra_P6+_iLQ=7|@tT zl>HUw%!W#ib3Q@-gLq$}i-`dmF&}K~uV)I?58P^`^tA;%2jy<(T=FeL_TR!f5M_Vu z+a6wpTxpd1e#JSLF>cq}*k8ZNP-G04zV6Gfs{C!oN;iGrMyTX*Qq5&(P9vB7`*G|~ zPziIJQDt!zvOh`;uxYv<*jW27Ck9+^Yw1hlq$k4X5S4c&>|2QJb1YLw>)Hvzw&=;cYuYH@tvk<N(OML*vxug7fxD5Bq8Gz4-X` zUpBX6!5RCj55c$Q{6S-+FO8Gl3za-hdJpn%08#eWzD;2P*!r%xLH1q z|MtA*SgL)OZ_;??qi`7<1j9l1v?g@)cbs72_Jr?*=8 zus!=!K1wO0{Hp4f%9Uc(J76B{4(gjIK2?mOb}CAC^Paul$xZq4r1rNyAp1Hy7Tk6n z+y@uH zERdd+M^xFfGBjA$fAOWB0rKiU{hWEg?~C;T+4h&;;JKpIe6VCvxs-2ePP;WGFOBKS zkG=;_!zrM)(&#T_{{}j>=Y^KC%Y0C}Nn}&sQf>G<@Hkuvhe9{`%W0FWvV%NP6E%bkx{$)fcjUR6bhuRJNJ|TY&0*+J_ct zfB6Tcw+Qmd>z2LeIal*2C&I3vF}2>%1=PQ4Xv<>v=^c*K&hzv;n5GOAZu=YB+`T3} zA3RyUQSDKo`_8A6raUIU^ekKqvq9~2KTx@i^y#vH+&|KAZKSgF9Ju$R_ww{^rrPk= zK+n#c0nxKjeZf~>SNWR{lYELpZ-8Y`E_SzaD4x0k)`GmcOk#I? zZYWJXPcj{T2}i+b*a(#OYLGYoB9ALJ=m{#{Bj6Cw{Vo8FTd5o_2gR29R^)*6Sg`BQ zi&^LWLNgy&U8_FGaiF}c-pgef+;`BnHqgJdV`G-wvrSj^Ph~*%*0MF&W2IWEy;a-Q z7q$iUzkdPuz#H%hsLZc|uniPr?&{Atq;1d!BziZfp$}j_c%{A#GIpxknep&;M4n+8 zT4G!GJrT0n*v8o0y_R%KW!tE9q+8W5`B=5H8i)8f{1IM(58-S00Sf6>_47o3?6lxp zk>7pS(56ZKfaZgPvEWTe`%#E0cZK9?>f`@Iu9HBqP+G^X4I8uWrdfh@jW=`x^+}}D zad0eL4~nrCf?^uQwMq4+cA(hb4va1HiL1=iILtq$wKHn2CG1Al|p;bTxeY6@SfF|TQU?aX4n71^*11n&lU{s108V^$_=dn(DmQu>TM<4y^Y&ll^vJLm$BVr6vX#S*N2e8t3U9)F@JpBt+e1I-0qHiL^#*pg`=V^v zn0+?g)KaqC0rE+uxc5?~@eMLv#8_}Kza7}n{)z$A58T$gAFP=6HYiowrZ!ytceUYv zhpXT)7zy&RYGYS}oOTW$YmANaT^q_rfC%bO>!)hw&K4!SO}%C zS*g=_3%SPlho4l;ZwEFk1H5ByVt`R*KDai)eXA_WAF0p#8r%troA!V$p$}->t}Qgm z&bh8nV&g*RhVs~qZN&hkwp}UGQyW_BZwEHCzrHh?IF`_%nz1JmIO_#ABB<+PMfJ~_s32gZGFIC#!< z&;6xS*`+D$t+6$=6}?~+7z@Y1Z$YuvLQvmh1?1ATj-LXuRhIqT?WlG61ZTgMMgB}> zPyS8m-Ucth2cWX27&n(JExBLq@-4&w>-k>id@(>S``h;lf~9Vm{K3l_Ha5nrEUWbq2TWCzZQvKWp3dyQ|g6etH zdGbU1!a_)*^Kv@(ePrLupR`Z#tw`qbBHk0r*f+}N5&J^*BuaPgKHG-Nj}C-Mpm_G}S)Kx45#X>}kAVUDyIt{#FC`9J?;LcLw>O#>&%D*GsQw7x-3W zLHopnBXJ)F+q-C&A6|JlqQE zJLb9<$^RfU#uv4e?Uk?U)L&@>*3a|Xfeq|$ZLOvAo_MgXHo)s1)>nJ-(YQ{37y|N} z7lCy57JLdClcbUB9_@1<_GOYXf2AV_erpQCT z9XPc9raL0P73tbK(%xbJkNwSW1_Zww(+<6;Ua22^2HXj+!ABsSwN$-PIa7I4`}YYf zfvB?g8T*fdt`KE^`LVg6`)LZ>G)J0>r*i&wU@V)5%fM65dH&Fe(pNuFJN_Pg1*&(= zaZjbnLuEwm(%KMZf9;b$Q<*MRp3R+(>fx_)y&II!{z3fs6=T7{?*=J`R~xE!rn&E{ z6#2;~`~wu1=CXf(j>)f8TG_Vxkq>^D7+@34;f3E1Enxqs{z6r~z8Bs%{jEsFQ)*Lf zewDU-7ujn-eS@enptweD+m}#j_h#i)A7-4t|1k}|6BS|qXq&t5j@WNSjyB(lR7|b7 z%H~yR%P)}q6j&Xi?625X@r~NJO1n2Jui~6zqJBHDDEq4{WafkAI~7|;#aA}}a$c$* zsy0k*S(N>?ZwpX=v7GNCpKQyJJ@U5$i?+X+4>sS5RP1y$D8|kwLpdMQIMg^h zb|Wl@a=wpzvOP@<(BQWNi?)AeK3MbMhlBd~`SLI4V;>;<-p~P}%7DfUPlV5*obMx_ zYzvXS&fk4*`^EI(?R;?2_P6uFlV@w}WE8wr!Mai3^&HSVRFwS{8}15~9_O4E{_VgB z`)i!9a159_$BYGzsL?p-{qO_iD}&{FO#Rot!3Gdzf9)Fpk3*%#IV+I;cG`ff-wte) z{q>zN5-`u251(`)=`V(IwQ(W2UPty}Pz6zCpf~%kgi4NcUcvr@)8hcV+tDcd+iykg zy0_+7=0K&7lOD)D=w71iuQ=yW(0Fp8vQ)0eKSXxtw*yxi`$v?4c>X{2Zjk8Sq;lKLVRTl>N1DL%0trInKEUTk+e0?Yw%%@dE2N!u}cyCK2<_`TvjK z4jhMl+dxzq=*j*IppxSp{dV98|E!BNA8gHEeEqlIimaQZzVlS5^l{Q-xrc5LW&e)s zp9Pg1=O}(T*#354x&W|JR2yL550(!YQKNCv=b%tKRIbN0PI^7815x(ZzU|-@DA#)^ zB$wix%jvW6+ku^2^A~4-#R+;pSTR6#t;R`z50yMl`cLHF9CF#eAII(h^`{Hzx*U%y z&UuWw%5Mjj$o_n1#KZvOrfHn?1gP|J(#hOIJBTU+J=i}FDtSIwbKVpD=?AyVasUn6 zfO!2t;z;vM*v!K;PC5bfTwOVq{Wvmxf$TpA**VJoDi^!MB8Zb-xt*i2PW55;^~dkg zwSfKACd4ZPnhz#1<5z<3)5_13+r7t;M}GZ}&=;cYuYGDCo`E>&we&f%x8^rA$MquI z1}9*rZTRiT_9e5wz7r+;E3OFMi>vH$(%}2Mx$Lj_@CH!4*wXe5=dXIF81G}yn6~CM z&Vjv+%}3O%M_+w)f6Cl;^>^Nr?*H3zP*fj4KEloi>o>`RXRa%KoOCAlq5fV}8BqW5 zI4GAmN9FJ{cpDypU&Dd)^M_&c4b1x=lV+HAfPAkT&+mKY8E%c+TD!Z;P#K8l|ATKu zR{8VJZ$-Y<l$>6MS$l zf5Kj!{mZXw<$E2By=8Or?5BPkfH7N*{i$tt?QYjY_KzO}PzE%vhzuGhy$33FoK$j4 ze%(iu{k3mXcodo=&XGJSlbXMI1r(nsMxBhVH^<)V(3W@e_dTkOX@fDOmW=P2?|0Vo zdp`Ai$FE*vHKBOU+T2|hZGZi4kn*l(ej%tolDQ@7M=A1@{~7_B_lYV4nkTvp)Yg_F zKQ|4PsU@H`RC1^+${)?d-rGW7^Bl+IIqd^`cQp1kzXLCutB&*S<~Y9z>Dt|{7j1w0 zt;pS`uV!TZ|NYy6ow>(@KsIu1SM0jR%oI>WN9w!7Uf%KmCw zOdCM?l#dG9Da9%+ZOdZvmk(AwR68GKf9=}@BQ-T5u=w$|2Knl_xcDqkBOwrP@WYwz%~X!}zxjr=3)dT?)wQ52^Z(oMM@moJ_G zx#ok{a-}C)8=mQxmD0xf(sJz`UJuznUOzB92GBUE`lp5T zT&~9zlc`@Qzs=7Z^zs9ze4O)O(st~M?t*Yn7}C3yZY-g!qs)Zf6C zBkNQSG~T%r_WlX?yRP~Dt-TLw7u4;*=6cS?*o^1x>Un0iCG8z9JFdOM>qXn&{&t|o zB!Y1%jZGHPbGaUW6WMpe_HF&qlRf|3U&)r{_qNsF<{QgZ{=p|uzcX$4_p-Oz@Nd2~ z{^;G?`Q0`Q zesA5zV;V!n&Zd4_dpl#R3G5J0uQuP}mqFQezUR@v;GrEM`^Sp`Bp>=PbwcCO!JLHp zQ{`C4)pt|;uK7{5;kUu5P>WCB%(USece(60txTN`_qVmJ7(KkdcpH_vEHtpWYtxXq z8*IAgrq<)hmzfXlg)V;$O)*YdOnFi{lq?#z)7Xvr@8`mF>^%t5^P_=JZ)My3qQ=+I zSv-A}?!3JA4%@SV!9zR3=f}Pu``dw(Dspvu~}oUQT(aCoZ6vBLFG~Nqr`O(K&zY-3G zk=Q#uKe|GF^3?q3>$VMV#QbP8*}tsp9k#!=i}C^4W5N2qj-Ee_Yya^4=#5-^BJ7I2 z2hfHG^P@jIi?Nrs*=;z#S5^*Vs|CtVluiroi?U5~>?_LtYU@ltkY^6nuL{1irSX-h zGGN)CT7F}zA^R7!ZG?`S!?dyTh>$H_3~1gD zXKYl@90%XovoV0?N0)+~AANxJi(oEgWeD^)^P{zPezc3RcXQ8=hHMz`o`V>#qU{}3 zC_-f*o-T;5)tC0`_HFMEubGH_{{?#H{(j)2kF+nFD&XY8<3woh-N`OyUSFUsE1 zX{G#y?63PRTn17xK>dmDwyNKDdf)n?wKK7I4ff8=j~;n4ZFn%wrZG1&&Z^%oFN68f z0{2*y%`3&;VL2OR|B3gtu3!7CF7?|S(4&6R0rbn%C*yYn$Ry~O1+hZh=T-~YrEs~7 zQ+6Ak^Z(A?zW$7%yYauh*<5V4JEXVgn{EATt>Y`wo5A+{>}}_;SHOR=GabOG%vjttR&s84qabpyI<)1q}wur`V=!S>RF*}x7z9a1?~l<-tHDk-)p$N z$j#rImo1MsFZ+JId4bPKUsE00?%zFqC|C(^rw_MDuLUbdXK#lqL?|C`e)j2Qzr7$N zZ_@2Wq3a86hxL_oJFA$Ichc=p``{JY4wW-6>2`L#OE*Aitra-l+Ml03RV)3nx2fk@ k$NOb(b2zKsQ0=m|*>tyc*ljT9Wp4+K`yz>9{> 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 @@ + + + + + + + + + + + + + + + + + + + + + + +