diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 4d5ac5b..f779439 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -1,10 +1,10 @@ using System.Reflection; -using System.Web; using System.Windows; using System.Windows.Threading; -using Windows.Win32; -using Windows.Win32.Foundation; +using Microsoft.Win32; + +using Bloxstrap.Resources; namespace Bloxstrap { @@ -15,29 +15,27 @@ namespace Bloxstrap { public const string ProjectName = "Bloxstrap"; public const string ProjectRepository = "pizzaboxer/bloxstrap"; + public const string RobloxPlayerAppName = "RobloxPlayerBeta"; public const string RobloxStudioAppName = "RobloxStudioBeta"; - // used only for communicating between app and menu - use Directories.Base for anything else - public static string BaseDirectory = null!; - public static string? CustomFontLocation; - - public static bool ShouldSaveConfigs { get; set; } = false; - - public static bool IsSetupComplete { get; set; } = true; - public static bool IsFirstRun { get; set; } = true; + // simple shorthand for extremely frequently used and long string - this goes under HKCU + public const string UninstallKey = $@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{ProjectName}"; public static LaunchSettings LaunchSettings { get; private set; } = null!; public static BuildMetadataAttribute BuildMetadata = Assembly.GetExecutingAssembly().GetCustomAttribute()!; + public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2]; - public static NotifyIconWrapper? NotifyIcon { get; private set; } + public static NotifyIconWrapper? NotifyIcon { get; set; } public static readonly Logger Logger = new(); public static readonly JsonManager Settings = new(); + public static readonly JsonManager State = new(); + public static readonly FastFlagManager FastFlags = new(); public static readonly HttpClient HttpClient = new( @@ -52,18 +50,10 @@ namespace Bloxstrap public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS) { - if (IsFirstRun) - { - if (exitCode == ErrorCode.ERROR_CANCELLED) - exitCode = ErrorCode.ERROR_INSTALL_USEREXIT; - } - int exitCodeNum = (int)exitCode; Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})"); - Settings.Save(); - State.Save(); NotifyIcon?.Dispose(); Environment.Exit(exitCodeNum); @@ -98,16 +88,7 @@ namespace Bloxstrap #endif } - private void StartupFinished() - { - const string LOG_IDENT = "App::StartupFinished"; - - Logger.WriteLine(LOG_IDENT, "Successfully reached end of main thread. Terminating..."); - - Terminate(); - } - - protected override async void OnStartup(StartupEventArgs e) + protected override void OnStartup(StartupEventArgs e) { const string LOG_IDENT = "App::OnStartup"; @@ -128,19 +109,62 @@ namespace Bloxstrap // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); + HttpClient.Timeout = TimeSpan.FromSeconds(30); + HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository); + LaunchSettings = new LaunchSettings(e.Args); - using (var checker = new InstallChecker()) + // installation check begins here + using var uninstallKey = Registry.CurrentUser.OpenSubKey(UninstallKey); + string? installLocation = null; + + if (uninstallKey?.GetValue("InstallLocation") is string value && Directory.Exists(value)) + installLocation = value; + + // silently change install location if we detect a portable run + // this should also handle renaming of the user profile folder + if (installLocation is null && Directory.GetParent(Paths.Process)?.FullName is string processDir) { - checker.Check(); + var files = Directory.GetFiles(processDir).Select(x => Path.GetFileName(x)).ToArray(); + var installer = new Installer + { + InstallLocation = processDir, + IsImplicitInstall = true + }; + + // check if settings.json and state.json are the only files in the folder, and if we can write to it + if (files.Length <= 3 + && files.Contains("Settings.json") + && files.Contains("State.json") + && installer.CheckInstallLocation()) + { + Logger.WriteLine(LOG_IDENT, $"Changing install location to '{processDir}'"); + installer.DoInstall(); + installLocation = processDir; + } } - Paths.Initialize(BaseDirectory); - - // 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) + if (installLocation is null) { + Logger.Initialize(true); + LaunchHandler.LaunchInstaller(); + } + else + { + Paths.Initialize(installLocation); + + // ensure executable is in the install directory + if (Paths.Process != Paths.Application && !File.Exists(Paths.Application)) + File.Copy(Paths.Process, Paths.Application); + + Logger.Initialize(LaunchSettings.IsUninstall); + + if (!Logger.Initialized && !Logger.NoWriteMode) + { + Logger.WriteLine(LOG_IDENT, "Possible duplicate launch detected, terminating."); + Terminate(); + } + Settings.Load(); State.Load(); FastFlags.Load(); @@ -152,145 +176,14 @@ namespace Bloxstrap } Locale.Set(Settings.Prop.Locale); + + if (!LaunchSettings.IsUninstall) + Installer.HandleUpgrade(); + + LaunchHandler.ProcessLaunchArgs(); } - LaunchSettings.ParseRoblox(); - - HttpClient.Timeout = TimeSpan.FromSeconds(30); - HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository); - - // TEMPORARY FILL-IN FOR NEW FUNCTIONALITY - // REMOVE WHEN LARGER REFACTORING IS DONE - var connectionResult = await RobloxDeployment.InitializeConnectivity(); - - if (connectionResult is not null) - { - Logger.WriteException(LOG_IDENT, connectionResult); - - Frontend.ShowConnectivityDialog( - Bloxstrap.Resources.Strings.Dialog_Connectivity_UnableToConnect, - Bloxstrap.Resources.Strings.Bootstrapper_Connectivity_Preventing, - connectionResult - ); - - return; - } - - if (LaunchSettings.IsUninstall && IsFirstRun) - { - Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Bootstrapper_FirstRunUninstall, MessageBoxImage.Error); - Terminate(ErrorCode.ERROR_INVALID_FUNCTION); - return; - } - - // 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) - { - Logger.Initialize(LaunchSettings.IsUninstall); - - if (!Logger.Initialized && !Logger.NoWriteMode) - { - Logger.WriteLine(LOG_IDENT, "Possible duplicate launch detected, terminating."); - Terminate(); - } - } - - if (!LaunchSettings.IsUninstall && !LaunchSettings.IsMenuLaunch) - NotifyIcon = new(); - -#if !DEBUG - if (!LaunchSettings.IsUninstall && !IsFirstRun) - InstallChecker.CheckUpgrade(); -#endif - - if (LaunchSettings.IsMenuLaunch) - { - Process? menuProcess = Utilities.GetProcessesSafe().Where(x => x.MainWindowTitle == $"{ProjectName} Menu").FirstOrDefault(); - - if (menuProcess is not null) - { - var handle = menuProcess.MainWindowHandle; - Logger.WriteLine(LOG_IDENT, $"Found an already existing menu window with handle {handle}"); - PInvoke.SetForegroundWindow((HWND)handle); - } - else - { - bool showAlreadyRunningWarning = Process.GetProcessesByName(ProjectName).Length > 1 && !LaunchSettings.IsQuiet; - Frontend.ShowMenu(showAlreadyRunningWarning); - } - - StartupFinished(); - return; - } - - if (!IsFirstRun) - ShouldSaveConfigs = true; - - if (Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _)) - { - // this currently doesn't work very well since it relies on checking the existence of the singleton mutex - // which often hangs around for a few seconds after the window closes - // it would be better to have this rely on the activity tracker when we implement IPC in the planned refactoring - - var result = Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Bootstrapper_ConfirmLaunch, MessageBoxImage.Warning, MessageBoxButton.YesNo); - - if (result != MessageBoxResult.Yes) - { - StartupFinished(); - return; - } - } - - // start bootstrapper and show the bootstrapper modal if we're not running silently - Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); - Bootstrapper bootstrapper = new(LaunchSettings.RobloxLaunchArgs, LaunchSettings.RobloxLaunchMode); - IBootstrapperDialog? dialog = null; - - if (!LaunchSettings.IsQuiet) - { - Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog"); - dialog = Settings.Prop.BootstrapperStyle.GetNew(); - bootstrapper.Dialog = dialog; - dialog.Bootstrapper = bootstrapper; - } - - Task bootstrapperTask = Task.Run(async () => await bootstrapper.Run()).ContinueWith(t => - { - Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished"); - - // notifyicon is blocking main thread, must be disposed here - NotifyIcon?.Dispose(); - - if (t.IsFaulted) - Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper"); - - if (t.Exception is null) - return; - - Logger.WriteException(LOG_IDENT, t.Exception); - - Exception exception = t.Exception; - -#if !DEBUG - if (t.Exception.GetType().ToString() == "System.AggregateException") - exception = t.Exception.InnerException!; -#endif - - FinalizeExceptionHandling(exception, false); - }); - - // this ordering is very important as all wpf windows are shown as modal dialogs, mess it up and you'll end up blocking input to one of them - dialog?.ShowBootstrapper(); - - if (!LaunchSettings.IsNoLaunch && Settings.Prop.EnableActivityTracking) - NotifyIcon?.InitializeContextMenu(); - - Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish"); - - bootstrapperTask.Wait(); - - StartupFinished(); + Terminate(); } } } diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index ae26c5e..fe9dcbd 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -7,8 +7,8 @@ true True Bloxstrap.ico - 2.7.0 - 2.7.0 + 2.8.0 + 2.8.0 app.manifest true false @@ -20,7 +20,6 @@ - diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 078a545..97b7dbb 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -25,11 +25,11 @@ namespace Bloxstrap private bool FreshInstall => String.IsNullOrEmpty(_versionGuid); private string _playerFileName => _launchMode == LaunchMode.Player ? "RobloxPlayerBeta.exe" : "RobloxStudioBeta.exe"; - // TODO: change name private string _playerLocation => Path.Combine(_versionFolder, _playerFileName); private string _launchCommandLine; private LaunchMode _launchMode; + private bool _installWebView2; private string _versionGuid { @@ -81,10 +81,11 @@ namespace Bloxstrap #endregion #region Core - public Bootstrapper(string launchCommandLine, LaunchMode launchMode) + public Bootstrapper(string launchCommandLine, LaunchMode launchMode, bool installWebView2) { _launchCommandLine = launchCommandLine; _launchMode = launchMode; + _installWebView2 = installWebView2; _packageDirectories = _launchMode == LaunchMode.Player ? PackageMap.Player : PackageMap.Studio; } @@ -96,7 +97,7 @@ namespace Bloxstrap string productName = "Roblox"; if (_launchMode != LaunchMode.Player) - productName += " Studio"; + productName = "Roblox Studio"; message = message.Replace("{product}", productName); @@ -124,47 +125,41 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Running bootstrapper"); - if (App.LaunchSettings.IsUninstall) - { - Uninstall(); - return; - } - // connectivity check App.Logger.WriteLine(LOG_IDENT, "Performing connectivity check..."); - SetStatus(Resources.Strings.Bootstrapper_Status_Connecting); + SetStatus(Strings.Bootstrapper_Status_Connecting); - try - { - await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); - } - catch (Exception ex) + var connectionResult = await RobloxDeployment.InitializeConnectivity(); + + if (connectionResult is not null) { App.Logger.WriteLine(LOG_IDENT, "Connectivity check failed!"); - App.Logger.WriteException(LOG_IDENT, ex); + App.Logger.WriteException(LOG_IDENT, connectionResult); - string message = Resources.Strings.Bootstrapper_Connectivity_Preventing; + string message = Strings.Bootstrapper_Connectivity_Preventing; - if (ex.GetType() == typeof(HttpResponseException)) - message = Resources.Strings.Bootstrapper_Connectivity_RobloxDown; - else if (ex.GetType() == typeof(TaskCanceledException)) - message = Resources.Strings.Bootstrapper_Connectivity_TimedOut; - else if (ex.GetType() == typeof(AggregateException)) - ex = ex.InnerException!; + if (connectionResult.GetType() == typeof(HttpResponseException)) + message = Strings.Bootstrapper_Connectivity_RobloxDown; + else if (connectionResult.GetType() == typeof(TaskCanceledException)) + message = Strings.Bootstrapper_Connectivity_TimedOut; + else if (connectionResult.GetType() == typeof(AggregateException)) + connectionResult = connectionResult.InnerException!; - Frontend.ShowConnectivityDialog(Strings.Dialog_Connectivity_UnableToConnect, message, ex); + Frontend.ShowConnectivityDialog(Strings.Dialog_Connectivity_UnableToConnect, message, connectionResult); App.Terminate(ErrorCode.ERROR_CANCELLED); - } - finally - { - App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished"); + + return; } + App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished"); + + await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel); + #if !DEBUG - if (!App.IsFirstRun && App.Settings.Prop.CheckForUpdates) + if (App.Settings.Prop.CheckForUpdates) await CheckForUpdates(); #endif @@ -177,7 +172,7 @@ namespace Bloxstrap { Mutex.OpenExisting("Bloxstrap_SingletonMutex").Close(); App.Logger.WriteLine(LOG_IDENT, "Bloxstrap_SingletonMutex mutex exists, waiting..."); - SetStatus(Resources.Strings.Bootstrapper_Status_WaitingOtherInstances); + SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances); mutexExists = true; } catch (Exception) @@ -199,37 +194,30 @@ namespace Bloxstrap await CheckLatestVersion(); // install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist - if (App.IsFirstRun || _latestVersionGuid != _versionGuid || !File.Exists(_playerLocation)) + if (_latestVersionGuid != _versionGuid || !File.Exists(_playerLocation)) await InstallLatestVersion(); - if (App.IsFirstRun) - App.ShouldSaveConfigs = true; - MigrateIntegrations(); - await InstallWebView2(); + if (_installWebView2) + await InstallWebView2(); App.FastFlags.Save(); await ApplyModifications(); - if (App.IsFirstRun || FreshInstall) - { - Register(); + // TODO: move this to install/upgrade flow + if (FreshInstall) RegisterProgramSize(); - } CheckInstall(); // at this point we've finished updating our configs App.Settings.Save(); App.State.Save(); - App.ShouldSaveConfigs = false; await mutex.ReleaseAsync(); - if (App.IsFirstRun && App.LaunchSettings.IsNoLaunch) - Dialog?.ShowSuccess(Resources.Strings.Bootstrapper_SuccessfullyInstalled); - else if (!App.LaunchSettings.IsNoLaunch && !_cancelFired) + if (!App.LaunchSettings.IsNoLaunch && !_cancelFired) await StartRoblox(); } @@ -272,21 +260,7 @@ namespace Bloxstrap { const string LOG_IDENT = "Bootstrapper::StartRoblox"; - SetStatus(Resources.Strings.Bootstrapper_Status_Starting); - - if (!File.Exists(Path.Combine(Paths.System, "mfplat.dll"))) - { - Frontend.ShowMessageBox( - Resources.Strings.Bootstrapper_WMFNotFound, - MessageBoxImage.Error - ); - - if (!App.LaunchSettings.IsQuiet) - Utilities.ShellExecute("https://support.microsoft.com/en-us/topic/media-feature-pack-list-for-windows-n-editions-c1c6fffa-d052-8338-7a79-a4bb980a700a"); - - Dialog?.CloseBootstrapper(); - return; - } + SetStatus(Strings.Bootstrapper_Status_Starting); if (_launchMode != LaunchMode.StudioAuth) { @@ -450,9 +424,7 @@ namespace Bloxstrap try { // clean up install - if (App.IsFirstRun) - Directory.Delete(Paths.Base, true); - else if (Directory.Exists(_versionFolder)) + if (Directory.Exists(_versionFolder)) Directory.Delete(_versionFolder, true); } catch (Exception ex) @@ -468,38 +440,6 @@ namespace Bloxstrap #endregion #region App Install - public static void Register() - { - const string LOG_IDENT = "Bootstrapper::Register"; - - using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}")) - { - applicationKey.SetValue("InstallLocation", Paths.Base); - } - - // set uninstall key - using (RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}")) - { - uninstallKey.SetValue("DisplayIcon", $"{Paths.Application},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", Paths.Base); - uninstallKey.SetValue("NoRepair", 1); - uninstallKey.SetValue("Publisher", "pizzaboxer"); - uninstallKey.SetValue("ModifyPath", $"\"{Paths.Application}\" -menu"); - uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet"); - uninstallKey.SetValue("UninstallString", $"\"{Paths.Application}\" -uninstall"); - uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); - uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); - } - - App.Logger.WriteLine(LOG_IDENT, "Registered application"); - } - public void RegisterProgramSize() { const string LOG_IDENT = "Bootstrapper::RegisterProgramSize"; @@ -539,57 +479,6 @@ namespace Bloxstrap ProtocolHandler.RegisterExtension(".rbxl"); ProtocolHandler.RegisterExtension(".rbxlx"); #endif - - if (Environment.ProcessPath is not null && Environment.ProcessPath != Paths.Application) - { - // in case the user is reinstalling - if (File.Exists(Paths.Application) && App.IsFirstRun) - { - Filesystem.AssertReadOnly(Paths.Application); - File.Delete(Paths.Application); - } - - // check to make sure bootstrapper is in the install folder - if (!File.Exists(Paths.Application)) - File.Copy(Environment.ProcessPath, Paths.Application); - } - - // 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(Paths.StartMenu)) - { - Directory.CreateDirectory(Paths.StartMenu); - } - else - { - // v2.0.0 - rebadge configuration menu as just "Bloxstrap Menu" - string oldMenuShortcut = Path.Combine(Paths.StartMenu, $"Configure {App.ProjectName}.lnk"); - - if (File.Exists(oldMenuShortcut)) - File.Delete(oldMenuShortcut); - } - - Utility.Shortcut.Create(Paths.Application, "", Path.Combine(Paths.StartMenu, "Play Roblox.lnk")); - Utility.Shortcut.Create(Paths.Application, "-menu", Path.Combine(Paths.StartMenu, $"{App.ProjectName} Menu.lnk")); -#if STUDIO_FEATURES - Utility.Shortcut.Create(Paths.Application, "-ide", Path.Combine(Paths.StartMenu, $"Roblox Studio ({App.ProjectName}).lnk")); -#endif - - if (App.Settings.Prop.CreateDesktopIcon) - { - try - { - Utility.Shortcut.Create(Paths.Application, "", Path.Combine(Paths.Desktop, "Play Roblox.lnk")); - - // one-time toggle, set it back to false - App.Settings.Prop.CreateDesktopIcon = false; - } - catch (Exception) - { - // suppress, we likely just don't have write perms for the desktop folder - } - } } private async Task CheckForUpdates() @@ -606,6 +495,7 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"Checking for updates..."); GithubRelease? releaseInfo; + try { releaseInfo = await Http.GetJson($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest"); @@ -622,16 +512,16 @@ namespace Bloxstrap return; } - int versionComparison = Utilities.CompareVersions(App.Version, releaseInfo.TagName); + var versionComparison = Utilities.CompareVersions(App.Version, releaseInfo.TagName); // check if we aren't using a deployed build, so we can update to one if a new version comes out - if (versionComparison == 0 && App.BuildMetadata.CommitRef.StartsWith("tag") || versionComparison == 1) + if (versionComparison == VersionComparison.Equal && App.BuildMetadata.CommitRef.StartsWith("tag") || versionComparison == VersionComparison.GreaterThan) { App.Logger.WriteLine(LOG_IDENT, $"No updates found"); return; } - SetStatus(Resources.Strings.Bootstrapper_Status_UpgradingBloxstrap); + SetStatus(Strings.Bootstrapper_Status_UpgradingBloxstrap); try { @@ -660,7 +550,6 @@ namespace Bloxstrap startInfo.ArgumentList.Add(arg); App.Settings.Save(); - App.ShouldSaveConfigs = false; Process.Start(startInfo); @@ -672,177 +561,11 @@ namespace Bloxstrap App.Logger.WriteException(LOG_IDENT, ex); Frontend.ShowMessageBox( - string.Format(Resources.Strings.Bootstrapper_AutoUpdateFailed, releaseInfo.TagName), + string.Format(Strings.Bootstrapper_AutoUpdateFailed, releaseInfo.TagName), MessageBoxImage.Information ); } } - - private void Uninstall() - { - const string LOG_IDENT = "Bootstrapper::Uninstall"; - - // prompt to shutdown roblox if its currently running - if (Process.GetProcessesByName(App.RobloxPlayerAppName).Any() || Process.GetProcessesByName(App.RobloxStudioAppName).Any()) - { - App.Logger.WriteLine(LOG_IDENT, $"Prompting to shut down all open Roblox instances"); - - MessageBoxResult result = Frontend.ShowMessageBox( - Resources.Strings.Bootstrapper_Uninstall_RobloxRunning, - MessageBoxImage.Information, - MessageBoxButton.OKCancel - ); - - if (result != MessageBoxResult.OK) - App.Terminate(ErrorCode.ERROR_CANCELLED); - - try - { - foreach (Process process in Process.GetProcessesByName(App.RobloxPlayerAppName)) - { - process.Kill(); - process.Close(); - } - -#if STUDIO_FEATURES - foreach (Process process in Process.GetProcessesByName(App.RobloxStudioAppName)) - { - process.Kill(); - process.Close(); - } -#endif - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to close process! {ex}"); - } - - App.Logger.WriteLine(LOG_IDENT, $"All Roblox processes closed"); - } - - SetStatus(Resources.Strings.Bootstrapper_Status_Uninstalling); - - App.ShouldSaveConfigs = false; - bool robloxPlayerStillInstalled = true; - bool robloxStudioStillInstalled = true; - - // check if stock bootstrapper is still installed - using RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); - if (bootstrapperKey is null) - { - robloxPlayerStillInstalled = false; - - ProtocolHandler.Unregister("roblox"); - ProtocolHandler.Unregister("roblox-player"); - } - else - { - // revert launch uri handler to stock bootstrapper - - string bootstrapperLocation = (string?)bootstrapperKey.GetValue("InstallLocation") + "RobloxPlayerLauncher.exe"; - - ProtocolHandler.Register("roblox", "Roblox", bootstrapperLocation); - ProtocolHandler.Register("roblox-player", "Roblox", bootstrapperLocation); - } - -#if STUDIO_FEATURES - using RegistryKey? studioBootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio"); - if (studioBootstrapperKey is null) - { - robloxStudioStillInstalled = false; - - ProtocolHandler.Unregister("roblox-studio"); - ProtocolHandler.Unregister("roblox-studio-auth"); - - ProtocolHandler.Unregister("Roblox.Place"); - ProtocolHandler.Unregister(".rbxl"); - ProtocolHandler.Unregister(".rbxlx"); - } - else - { - string studioLocation = (string?)studioBootstrapperKey.GetValue("InstallLocation") + "RobloxStudioBeta.exe"; // points to studio exe instead of bootstrapper - ProtocolHandler.Register("roblox-studio", "Roblox", studioLocation); - ProtocolHandler.Register("roblox-studio-auth", "Roblox", studioLocation); - - ProtocolHandler.RegisterRobloxPlace(studioLocation); - } -#endif - - // if the folder we're installed to does not end with "Bloxstrap", we're installed to a user-selected folder - // in which case, chances are they chose to install to somewhere they didn't really mean to (prior to the added warning in 2.4.0) - // if so, we're walking on eggshells and have to ensure we only clean up what we need to clean up - bool cautiousUninstall = !Paths.Base.ToLower().EndsWith(App.ProjectName.ToLower()); - - var cleanupSequence = new List - { - () => Registry.CurrentUser.DeleteSubKey($@"Software\{App.ProjectName}"), - () => Directory.Delete(Paths.StartMenu, true), - () => File.Delete(Path.Combine(Paths.Desktop, "Play Roblox.lnk")), - () => Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}") - }; - - if (cautiousUninstall) - { - cleanupSequence.Add(() => Directory.Delete(Paths.Downloads, true)); - cleanupSequence.Add(() => Directory.Delete(Paths.Modifications, true)); - cleanupSequence.Add(() => Directory.Delete(Paths.Versions, true)); - cleanupSequence.Add(() => Directory.Delete(Paths.Logs, true)); - - cleanupSequence.Add(() => File.Delete(App.Settings.FileLocation)); - cleanupSequence.Add(() => File.Delete(App.State.FileLocation)); - } - else - { - cleanupSequence.Add(() => Directory.Delete(Paths.Base, true)); - } - - string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox"); - - if (!robloxPlayerStillInstalled && !robloxStudioStillInstalled && Directory.Exists(robloxFolder)) - cleanupSequence.Add(() => Directory.Delete(robloxFolder, true)); - - foreach (var process in cleanupSequence) - { - try - { - process(); - } - catch (Exception ex) - { - App.Logger.WriteLine(LOG_IDENT, $"Encountered exception when running cleanup sequence (#{cleanupSequence.IndexOf(process)})"); - App.Logger.WriteException(LOG_IDENT, ex); - } - } - - Action? callback = null; - - if (Directory.Exists(Paths.Base)) - { - callback = delegate - { - // this is definitely one of the workaround hacks of all time - // could antiviruses falsely detect this as malicious behaviour though? - // "hmm whats this program doing running a cmd command chain quietly in the background that auto deletes an entire folder" - - string deleteCommand; - - if (cautiousUninstall) - deleteCommand = $"del /Q \"{Paths.Application}\""; - else - deleteCommand = $"del /Q \"{Paths.Base}\\*\" && rmdir \"{Paths.Base}\""; - - Process.Start(new ProcessStartInfo() - { - FileName = "cmd.exe", - Arguments = $"/c timeout 5 && {deleteCommand}", - UseShellExecute = true, - WindowStyle = ProcessWindowStyle.Hidden - }); - }; - } - - Dialog?.ShowSuccess(Resources.Strings.Bootstrapper_SuccessfullyUninstalled, callback); - } #endregion #region Roblox Install @@ -852,7 +575,7 @@ namespace Bloxstrap _isInstalling = true; - SetStatus(FreshInstall ? Resources.Strings.Bootstrapper_Status_Installing : Resources.Strings.Bootstrapper_Status_Upgrading); + SetStatus(FreshInstall ? Strings.Bootstrapper_Status_Installing : Strings.Bootstrapper_Status_Upgrading); Directory.CreateDirectory(Paths.Base); Directory.CreateDirectory(Paths.Downloads); @@ -866,7 +589,7 @@ namespace Bloxstrap if (Filesystem.GetFreeDiskSpace(Paths.Base) < totalSizeRequired) { Frontend.ShowMessageBox( - Resources.Strings.Bootstrapper_NotEnoughSpace, + Strings.Bootstrapper_NotEnoughSpace, MessageBoxImage.Error ); @@ -911,7 +634,7 @@ namespace Bloxstrap if (Dialog is not null) { Dialog.ProgressStyle = ProgressBarStyle.Marquee; - SetStatus(Resources.Strings.Bootstrapper_Status_Configuring); + SetStatus(Strings.Bootstrapper_Status_Configuring); } // wait for all packages to finish extracting, with an exception for the webview2 runtime installer @@ -996,7 +719,7 @@ namespace Bloxstrap } // don't register program size until the program is registered, which will be done after this - if (!App.IsFirstRun && !FreshInstall) + if (!FreshInstall) RegisterProgramSize(); if (Dialog is not null) @@ -1004,20 +727,10 @@ namespace Bloxstrap _isInstalling = false; } - + private async Task InstallWebView2() { const string LOG_IDENT = "Bootstrapper::InstallWebView2"; - - // check if the webview2 runtime needs to be installed - // webview2 can either be installed be per-user or globally, so we need to check in both hklm and hkcu - // https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#detect-if-a-suitable-webview2-runtime-is-already-installed - - using RegistryKey? hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); - using RegistryKey? hkcuKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); - - if (hklmKey is not null || hkcuKey is not null) - return; App.Logger.WriteLine(LOG_IDENT, "Installing runtime..."); @@ -1036,7 +749,7 @@ namespace Bloxstrap await ExtractPackage(package); } - SetStatus(Resources.Strings.Bootstrapper_Status_InstallingWebView2); + SetStatus(Strings.Bootstrapper_Status_InstallingWebView2); ProcessStartInfo startInfo = new() { @@ -1065,7 +778,7 @@ namespace Bloxstrap if (File.Exists(injectorLocation)) { Frontend.ShowMessageBox( - Resources.Strings.Bootstrapper_HyperionUpdateInfo, + Strings.Bootstrapper_HyperionUpdateInfo, MessageBoxImage.Warning ); @@ -1086,7 +799,7 @@ namespace Bloxstrap return; } - SetStatus(Resources.Strings.Bootstrapper_Status_ApplyingModifications); + SetStatus(Strings.Bootstrapper_Status_ApplyingModifications); // set executable flags for fullscreen optimizations App.Logger.WriteLine(LOG_IDENT, "Checking executable flags..."); @@ -1212,12 +925,6 @@ namespace Bloxstrap string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families"); - if (App.IsFirstRun && App.CustomFontLocation is not null) - { - Directory.CreateDirectory(Path.GetDirectoryName(Paths.CustomFont)!); - File.Copy(App.CustomFontLocation, Paths.CustomFont, true); - } - if (File.Exists(Paths.CustomFont)) { App.Logger.WriteLine(LOG_IDENT, "Begin font check"); diff --git a/Bloxstrap/Enums/ErrorCode.cs b/Bloxstrap/Enums/ErrorCode.cs index f6b6609..4d830dd 100644 --- a/Bloxstrap/Enums/ErrorCode.cs +++ b/Bloxstrap/Enums/ErrorCode.cs @@ -9,9 +9,11 @@ { ERROR_SUCCESS = 0, ERROR_INVALID_FUNCTION = 1, + ERROR_FILE_NOT_FOUND = 2, + + ERROR_CANCELLED = 1223, ERROR_INSTALL_USEREXIT = 1602, ERROR_INSTALL_FAILURE = 1603, - ERROR_CANCELLED = 1223, CO_E_APPNOTFOUND = -2147221003 } diff --git a/Bloxstrap/Enums/NextAction.cs b/Bloxstrap/Enums/NextAction.cs new file mode 100644 index 0000000..cf804f7 --- /dev/null +++ b/Bloxstrap/Enums/NextAction.cs @@ -0,0 +1,9 @@ +namespace Bloxstrap.Enums +{ + public enum NextAction + { + Terminate, + LaunchSettings, + LaunchRoblox + } +} diff --git a/Bloxstrap/Enums/VersionComparison.cs b/Bloxstrap/Enums/VersionComparison.cs new file mode 100644 index 0000000..8f65958 --- /dev/null +++ b/Bloxstrap/Enums/VersionComparison.cs @@ -0,0 +1,9 @@ +namespace Bloxstrap.Enums +{ + enum VersionComparison + { + LessThan = -1, + Equal = 0, + GreaterThan = 1 + } +} diff --git a/Bloxstrap/InstallChecker.cs b/Bloxstrap/InstallChecker.cs deleted file mode 100644 index 66989ef..0000000 --- a/Bloxstrap/InstallChecker.cs +++ /dev/null @@ -1,279 +0,0 @@ -using System.Windows; -using Microsoft.Win32; - -namespace Bloxstrap -{ - internal class InstallChecker : IDisposable - { - private RegistryKey? _registryKey; - private string? _installLocation; - - internal InstallChecker() - { - _registryKey = Registry.CurrentUser.OpenSubKey($"Software\\{App.ProjectName}", true); - - if (_registryKey is not null) - _installLocation = (string?)_registryKey.GetValue("InstallLocation"); - } - - internal void Check() - { - const string LOG_IDENT = "InstallChecker::Check"; - - if (_registryKey is null || _installLocation is null) - { - if (!File.Exists("Settings.json") || !File.Exists("State.json")) - { - FirstTimeRun(); - return; - } - - App.Logger.WriteLine(LOG_IDENT, "Installation registry key is likely malformed"); - - _installLocation = Path.GetDirectoryName(Paths.Process)!; - - var result = Frontend.ShowMessageBox( - string.Format(Resources.Strings.InstallChecker_NotInstalledProperly, _installLocation), - MessageBoxImage.Warning, - MessageBoxButton.YesNo - ); - - if (result != MessageBoxResult.Yes) - { - FirstTimeRun(); - return; - } - - App.Logger.WriteLine(LOG_IDENT, $"Setting install location as '{_installLocation}'"); - - if (_registryKey is null) - _registryKey = Registry.CurrentUser.CreateSubKey($"Software\\{App.ProjectName}"); - - _registryKey.SetValue("InstallLocation", _installLocation); - } - - // check if drive that bloxstrap was installed to was removed from system, or had its drive letter changed - - if (!Directory.Exists(_installLocation)) - { - App.Logger.WriteLine(LOG_IDENT, "Could not find install location. Checking if drive has changed..."); - - bool driveExists = false; - string driveName = _installLocation[..3]; - string? newDriveName = null; - - foreach (var drive in DriveInfo.GetDrives()) - { - if (drive.Name == driveName) - driveExists = true; - else if (Directory.Exists(_installLocation.Replace(driveName, drive.Name))) - newDriveName = drive.Name; - } - - if (newDriveName is not null) - { - App.Logger.WriteLine(LOG_IDENT, $"Drive has changed from {driveName} to {newDriveName}"); - - Frontend.ShowMessageBox( - string.Format(Resources.Strings.InstallChecker_DriveLetterChangeDetected, driveName, newDriveName), - MessageBoxImage.Warning, - MessageBoxButton.OK - ); - - _installLocation = _installLocation.Replace(driveName, newDriveName); - _registryKey.SetValue("InstallLocation", _installLocation); - } - else if (!driveExists) - { - App.Logger.WriteLine(LOG_IDENT, $"Drive {driveName} does not exist anymore, and has likely been removed"); - - var result = Frontend.ShowMessageBox( - string.Format(Resources.Strings.InstallChecker_InstallDriveMissing, driveName), - MessageBoxImage.Warning, - MessageBoxButton.OKCancel - ); - - if (result != MessageBoxResult.OK) - App.Terminate(); - - FirstTimeRun(); - return; - } - else - { - App.Logger.WriteLine(LOG_IDENT, "Drive has not changed, folder was likely moved or deleted"); - } - } - - App.BaseDirectory = _installLocation; - App.IsFirstRun = false; - } - - public void Dispose() - { - _registryKey?.Dispose(); - GC.SuppressFinalize(this); - } - - private static void FirstTimeRun() - { - const string LOG_IDENT = "InstallChecker::FirstTimeRun"; - - App.Logger.WriteLine(LOG_IDENT, "Running first-time install"); - - App.BaseDirectory = Path.Combine(Paths.LocalAppData, App.ProjectName); - App.Logger.Initialize(true); - - if (App.LaunchSettings.IsQuiet) - return; - - App.IsSetupComplete = false; - - App.FastFlags.Load(); - Frontend.ShowLanguageSelection(); - Frontend.ShowMenu(); - - // exit if we don't click the install button on installation - if (App.IsSetupComplete) - return; - - App.Logger.WriteLine(LOG_IDENT, "Installation cancelled!"); - App.Terminate(ErrorCode.ERROR_CANCELLED); - } - - internal static void CheckUpgrade() - { - const string LOG_IDENT = "InstallChecker::CheckUpgrade"; - - if (!File.Exists(Paths.Application) || Paths.Process == Paths.Application) - return; - - // 2.0.0 downloads updates to /Updates so lol - bool isAutoUpgrade = Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp")); - - FileVersionInfo existingVersionInfo = FileVersionInfo.GetVersionInfo(Paths.Application); - FileVersionInfo currentVersionInfo = FileVersionInfo.GetVersionInfo(Paths.Process); - - if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application)) - return; - - MessageBoxResult result; - - // silently upgrade version if the command line flag is set or if we're launching from an auto update - if (App.LaunchSettings.IsUpgrade || isAutoUpgrade) - { - result = MessageBoxResult.Yes; - } - else - { - result = Frontend.ShowMessageBox( - Resources.Strings.InstallChecker_VersionDifferentThanInstalled, - MessageBoxImage.Question, - MessageBoxButton.YesNo - ); - } - - if (result != MessageBoxResult.Yes) - return; - - Filesystem.AssertReadOnly(Paths.Application); - - // yes, this is EXTREMELY hacky, but the updater process that launched the - // new version may still be open and so we have to wait for it to close - int attempts = 0; - while (attempts < 10) - { - attempts++; - - try - { - File.Delete(Paths.Application); - break; - } - catch (Exception) - { - if (attempts == 1) - App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version"); - - Thread.Sleep(500); - } - } - - if (attempts == 10) - { - App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 5 seconds)"); - return; - } - - File.Copy(Paths.Process, Paths.Application); - - Bootstrapper.Register(); - - // update migrations - - if (App.BuildMetadata.CommitRef.StartsWith("tag") && currentVersionInfo.ProductVersion is not null) - { - if (existingVersionInfo.ProductVersion == "2.4.0") - { - App.FastFlags.SetValue("DFFlagDisableDPIScale", null); - App.FastFlags.SetValue("DFFlagVariableDPIScale2", null); - App.FastFlags.Save(); - } - else if (existingVersionInfo.ProductVersion == "2.5.0") - { - App.FastFlags.SetValue("FIntDebugForceMSAASamples", null); - - if (App.FastFlags.GetPreset("UI.Menu.Style.DisableV2") is not null) - App.FastFlags.SetPreset("UI.Menu.Style.ABTest", false); - - App.FastFlags.Save(); - } - else if (existingVersionInfo.ProductVersion == "2.5.4") - { - if (App.Settings.Prop.UseDisableAppPatch) - { - try - { - File.Delete(Path.Combine(Paths.Modifications, "ExtraContent\\places\\Mobile.rbxl")); - } - catch (Exception ex) - { - App.Logger.WriteException(LOG_IDENT, ex); - } - - App.Settings.Prop.EnableActivityTracking = true; - } - - if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ClassicFluentDialog) - App.Settings.Prop.BootstrapperStyle = BootstrapperStyle.FluentDialog; - - _ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x); - if (x == 0) - { - App.FastFlags.SetPreset("Rendering.Framerate", null); - App.FastFlags.Save(); - } - - App.Settings.Save(); - } - } - - if (isAutoUpgrade) - { - Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Release-notes-for-Bloxstrap-v{currentVersionInfo.ProductVersion}"); - } - else if (!App.LaunchSettings.IsQuiet) - { - Frontend.ShowMessageBox( - string.Format(Resources.Strings.InstallChecker_Updated, currentVersionInfo.ProductVersion), - MessageBoxImage.Information, - MessageBoxButton.OK - ); - - Frontend.ShowMenu(); - - App.Terminate(); - } - } - } -} diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs new file mode 100644 index 0000000..817b337 --- /dev/null +++ b/Bloxstrap/Installer.cs @@ -0,0 +1,470 @@ +using System.DirectoryServices; +using System.Reflection; +using System.Reflection.Metadata.Ecma335; +using System.Windows; +using System.Windows.Media.Animation; +using Bloxstrap.Resources; +using Microsoft.Win32; + +namespace Bloxstrap +{ + internal class Installer + { + private static string DesktopShortcut => Path.Combine(Paths.Desktop, "Bloxstrap.lnk"); + + private static string StartMenuShortcut => Path.Combine(Paths.WindowsStartMenu, "Bloxstrap.lnk"); + + public string InstallLocation = Path.Combine(Paths.LocalAppData, "Bloxstrap"); + + public bool CreateDesktopShortcuts = true; + + public bool CreateStartMenuShortcuts = true; + + public bool IsImplicitInstall = false; + + public string InstallLocationError { get; set; } = ""; + + public void DoInstall() + { + // should've been created earlier from the write test anyway + Directory.CreateDirectory(InstallLocation); + + Paths.Initialize(InstallLocation); + + if (!IsImplicitInstall) + { + Filesystem.AssertReadOnly(Paths.Application); + File.Copy(Paths.Process, Paths.Application, true); + } + + // TODO: registry access checks, i'll need to look back on issues to see what the error looks like + using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) + { + uninstallKey.SetValue("DisplayIcon", $"{Paths.Application},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", Paths.Base); + uninstallKey.SetValue("NoRepair", 1); + uninstallKey.SetValue("Publisher", "pizzaboxer"); + uninstallKey.SetValue("ModifyPath", $"\"{Paths.Application}\" -settings"); + uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet"); + uninstallKey.SetValue("UninstallString", $"\"{Paths.Application}\" -uninstall"); + uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}"); + uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest"); + } + + // only register player, for the scenario where the user installs bloxstrap, closes it, + // and then launches from the website expecting it to work + // studio can be implicitly registered when it's first launched manually + ProtocolHandler.Register("roblox", "Roblox", Paths.Application); + ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application); + + // TODO: implicit installation needs to reregister studio + + if (CreateDesktopShortcuts) + Shortcut.Create(Paths.Application, "", DesktopShortcut); + + if (CreateStartMenuShortcuts) + Shortcut.Create(Paths.Application, "", StartMenuShortcut); + + // existing configuration persisting from an earlier install + App.Settings.Load(); + App.State.Load(); + App.FastFlags.Load(); + } + + private bool ValidateLocation() + { + // prevent from installing to the root of a drive + if (InstallLocation.Length <= 3) + return false; + + // unc path, just to be safe + if (InstallLocation.StartsWith("\\\\")) + return false; + + // prevent from installing to a onedrive folder + if (InstallLocation.Contains("OneDrive", StringComparison.InvariantCultureIgnoreCase)) + return false; + + // prevent from installing to an essential user profile folder (e.g. Documents, Downloads, Contacts idk) + if (String.Compare(Directory.GetParent(InstallLocation)?.FullName, Paths.UserProfile, StringComparison.InvariantCultureIgnoreCase) == 0) + return false; + + return true; + } + + public bool CheckInstallLocation() + { + if (string.IsNullOrEmpty(InstallLocation)) + { + InstallLocationError = Strings.Menu_InstallLocation_NotSet; + } + else if (!ValidateLocation()) + { + InstallLocationError = Strings.Menu_InstallLocation_CantInstall; + } + else + { + if (!IsImplicitInstall + && !InstallLocation.EndsWith(App.ProjectName, StringComparison.InvariantCultureIgnoreCase) + && Directory.Exists(InstallLocation) + && Directory.EnumerateFileSystemEntries(InstallLocation).Any()) + { + string suggestedChange = Path.Combine(InstallLocation, App.ProjectName); + + MessageBoxResult result = Frontend.ShowMessageBox( + String.Format(Strings.Menu_InstallLocation_NotEmpty, suggestedChange), + MessageBoxImage.Warning, + MessageBoxButton.YesNoCancel, + MessageBoxResult.Yes + ); + + if (result == MessageBoxResult.Yes) + InstallLocation = suggestedChange; + else if (result == MessageBoxResult.Cancel || result == MessageBoxResult.None) + return false; + } + + try + { + // check if we can write to the directory (a bit hacky but eh) + string testFile = Path.Combine(InstallLocation, $"{App.ProjectName}WriteTest.txt"); + + Directory.CreateDirectory(InstallLocation); + File.WriteAllText(testFile, ""); + File.Delete(testFile); + } + catch (UnauthorizedAccessException) + { + InstallLocationError = Strings.Menu_InstallLocation_NoWritePerms; + } + catch (Exception ex) + { + InstallLocationError = ex.Message; + } + } + + return String.IsNullOrEmpty(InstallLocationError); + } + + public static void DoUninstall(bool keepData) + { + const string LOG_IDENT = "Installer::DoUninstall"; + + var processes = new List(); + processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName)); + +#if STUDIO_FEATURES + processes.AddRange(Process.GetProcessesByName(App.RobloxStudioAppName)); +#endif + + // prompt to shutdown roblox if its currently running + if (processes.Any()) + { + if (!App.LaunchSettings.IsQuiet) + { + MessageBoxResult result = Frontend.ShowMessageBox( + Strings.Bootstrapper_Uninstall_RobloxRunning, + MessageBoxImage.Information, + MessageBoxButton.OKCancel + ); + + if (result != MessageBoxResult.OK) + App.Terminate(ErrorCode.ERROR_CANCELLED); + } + + try + { + foreach (var process in processes) + { + process.Kill(); + process.Close(); + } + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"Failed to close process! {ex}"); + } + } + + string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox"); + bool playerStillInstalled = true; + bool studioStillInstalled = true; + + // check if stock bootstrapper is still installed + using var playerKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player"); + var playerFolder = playerKey?.GetValue("InstallLocation"); + + if (playerKey is null || playerFolder is not string) + { + playerStillInstalled = false; + + ProtocolHandler.Unregister("roblox"); + ProtocolHandler.Unregister("roblox-player"); + } + else + { + // revert launch uri handler to stock bootstrapper + string playerPath = Path.Combine((string)playerFolder, "RobloxPlayerBeta.exe"); + + ProtocolHandler.Register("roblox", "Roblox", playerPath); + ProtocolHandler.Register("roblox-player", "Roblox", playerPath); + } + + using RegistryKey? studioBootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio"); + if (studioBootstrapperKey is null) + { + studioStillInstalled = false; + +#if STUDIO_FEATURES + ProtocolHandler.Unregister("roblox-studio"); + ProtocolHandler.Unregister("roblox-studio-auth"); + + ProtocolHandler.Unregister("Roblox.Place"); + ProtocolHandler.Unregister(".rbxl"); + ProtocolHandler.Unregister(".rbxlx"); +#endif + } +#if STUDIO_FEATURES + else + { + string studioLocation = (string?)studioBootstrapperKey.GetValue("InstallLocation") + "RobloxStudioBeta.exe"; // points to studio exe instead of bootstrapper + ProtocolHandler.Register("roblox-studio", "Roblox", studioLocation); + ProtocolHandler.Register("roblox-studio-auth", "Roblox", studioLocation); + + ProtocolHandler.RegisterRobloxPlace(studioLocation); + } +#endif + + var cleanupSequence = new List + { + () => File.Delete(DesktopShortcut), + () => File.Delete(StartMenuShortcut), + + () => Directory.Delete(Paths.Versions, true), + () => Directory.Delete(Paths.Downloads, true), + }; + + if (!keepData) + { + cleanupSequence.AddRange(new List + { + () => Directory.Delete(Paths.Modifications, true), + () => Directory.Delete(Paths.Logs, true), + + () => File.Delete(App.Settings.FileLocation), + () => File.Delete(App.State.FileLocation), // TODO: maybe this should always be deleted? not sure yet + }); + } + + bool deleteFolder = false; + + if (Directory.Exists(Paths.Base)) + { + var folderFiles = Directory.GetFiles(Paths.Base); + deleteFolder = folderFiles.Length == 1 && folderFiles.First().EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase); + } + + if (deleteFolder) + cleanupSequence.Add(() => Directory.Delete(Paths.Base, true)); + + if (!playerStillInstalled && !studioStillInstalled && Directory.Exists(robloxFolder)) + cleanupSequence.Add(() => Directory.Delete(robloxFolder, true)); + + cleanupSequence.Add(() => Registry.CurrentUser.DeleteSubKey(App.UninstallKey)); + + foreach (var process in cleanupSequence) + { + try + { + process(); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"Encountered exception when running cleanup sequence (#{cleanupSequence.IndexOf(process)})"); + App.Logger.WriteException(LOG_IDENT, ex); + } + } + + if (Directory.Exists(Paths.Base)) + { + // this is definitely one of the workaround hacks of all time + + string deleteCommand; + + if (deleteFolder) + deleteCommand = $"del /Q \"{Paths.Base}\\*\" && rmdir \"{Paths.Base}\""; + else + deleteCommand = $"del /Q \"{Paths.Application}\""; + + Process.Start(new ProcessStartInfo() + { + FileName = "cmd.exe", + Arguments = $"/c timeout 5 && {deleteCommand}", + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden + }); + } + } + + public static void HandleUpgrade() + { + const string LOG_IDENT = "Installer::HandleUpgrade"; + + if (!File.Exists(Paths.Application) || Paths.Process == Paths.Application) + return; + + // 2.0.0 downloads updates to /Updates so lol + // TODO: 2.8.0 will download them to /Bloxstrap/Updates + bool isAutoUpgrade = Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp")); + + var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion; + var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion; + + if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application)) + return; + + // silently upgrade version if the command line flag is set or if we're launching from an auto update + if (!App.LaunchSettings.IsUpgrade && !isAutoUpgrade) + { + var result = Frontend.ShowMessageBox( + Strings.InstallChecker_VersionDifferentThanInstalled, + MessageBoxImage.Question, + MessageBoxButton.YesNo + ); + + if (result != MessageBoxResult.Yes) + return; + } + + Filesystem.AssertReadOnly(Paths.Application); + + // TODO: make this use a mutex somehow + // yes, this is EXTREMELY hacky, but the updater process that launched the + // new version may still be open and so we have to wait for it to close + int attempts = 0; + while (attempts < 10) + { + attempts++; + + try + { + File.Delete(Paths.Application); + break; + } + catch (Exception) + { + if (attempts == 1) + App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version"); + + Thread.Sleep(500); + } + } + + if (attempts == 10) + { + App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 5 seconds)"); + return; + } + + File.Copy(Paths.Process, Paths.Application); + + using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey)) + { + uninstallKey.SetValue("DisplayVersion", App.Version); + } + + // update migrations + + if (existingVer is not null) + { + if (Utilities.CompareVersions(existingVer, "2.5.0") == VersionComparison.LessThan) + { + App.FastFlags.SetValue("DFFlagDisableDPIScale", null); + App.FastFlags.SetValue("DFFlagVariableDPIScale2", null); + } + + if (Utilities.CompareVersions(existingVer, "2.5.1") == VersionComparison.LessThan) + { + App.FastFlags.SetValue("FIntDebugForceMSAASamples", null); + + if (App.FastFlags.GetPreset("UI.Menu.Style.DisableV2") is not null) + App.FastFlags.SetPreset("UI.Menu.Style.ABTest", false); + } + + if (Utilities.CompareVersions(existingVer, "2.6.0") == VersionComparison.LessThan) + { + if (App.Settings.Prop.UseDisableAppPatch) + { + try + { + File.Delete(Path.Combine(Paths.Modifications, "ExtraContent\\places\\Mobile.rbxl")); + } + catch (Exception ex) + { + App.Logger.WriteException(LOG_IDENT, ex); + } + + App.Settings.Prop.EnableActivityTracking = true; + } + + if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ClassicFluentDialog) + App.Settings.Prop.BootstrapperStyle = BootstrapperStyle.FluentDialog; + + _ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x); + if (x == 0) + { + App.FastFlags.SetPreset("Rendering.Framerate", null); + } + } + + if (Utilities.CompareVersions(existingVer, "2.8.0") == VersionComparison.LessThan) + { + string oldDesktopPath = Path.Combine(Paths.Desktop, "Play Roblox.lnk"); + string oldStartPath = Path.Combine(Paths.WindowsStartMenu, "Bloxstrap"); + + if (File.Exists(oldDesktopPath)) + File.Move(oldDesktopPath, DesktopShortcut); + + if (Directory.Exists(oldStartPath)) + { + try + { + Directory.Delete(oldStartPath, true); + } + catch (Exception ex) + { + App.Logger.WriteException(LOG_IDENT, ex); + } + + Shortcut.Create(Paths.Application, "", StartMenuShortcut); + } + + Registry.CurrentUser.DeleteSubKeyTree("Software\\Bloxstrap", false); + } + + App.Settings.Save(); + App.FastFlags.Save(); + } + + if (isAutoUpgrade) + { + Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Release-notes-for-Bloxstrap-v{currentVer}"); + } + else if (!App.LaunchSettings.IsQuiet) + { + Frontend.ShowMessageBox( + string.Format(Strings.InstallChecker_Updated, currentVer), + MessageBoxImage.Information, + MessageBoxButton.OK + ); + } + } + } +} diff --git a/Bloxstrap/JsonManager.cs b/Bloxstrap/JsonManager.cs index b002a93..e1078e7 100644 --- a/Bloxstrap/JsonManager.cs +++ b/Bloxstrap/JsonManager.cs @@ -37,12 +37,6 @@ namespace Bloxstrap { string LOG_IDENT = $"{LOG_IDENT_CLASS}::Save"; - if (!App.ShouldSaveConfigs) - { - App.Logger.WriteLine(LOG_IDENT, "Save request ignored"); - return; - } - App.Logger.WriteLine(LOG_IDENT, $"Saving to {FileLocation}..."); Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!); diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs new file mode 100644 index 0000000..cc40ef1 --- /dev/null +++ b/Bloxstrap/LaunchHandler.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +using Bloxstrap.UI.Elements.Dialogs; +using Bloxstrap.Resources; + +using Microsoft.Win32; +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Bloxstrap +{ + public static class LaunchHandler + { + public static void ProcessNextAction(NextAction action, bool isUnfinishedInstall = false) + { + switch (action) + { + case NextAction.LaunchSettings: + LaunchSettings(); + break; + + case NextAction.LaunchRoblox: + LaunchRoblox(); + break; + + default: + App.Terminate(isUnfinishedInstall ? ErrorCode.ERROR_INSTALL_USEREXIT : ErrorCode.ERROR_SUCCESS); + break; + } + } + + public static void ProcessLaunchArgs() + { + // this order is specific + + if (App.LaunchSettings.IsUninstall) + LaunchUninstaller(); + else if (App.LaunchSettings.IsMenuLaunch) + LaunchSettings(); + else if (App.LaunchSettings.IsRobloxLaunch) + LaunchRoblox(); + else if (!App.LaunchSettings.IsQuiet) + LaunchMenu(); + } + + public static void LaunchInstaller() + { + // TODO: detect duplicate launch, mutex maybe? + + if (App.LaunchSettings.IsUninstall) + { + Frontend.ShowMessageBox(Strings.Bootstrapper_FirstRunUninstall, MessageBoxImage.Error); + App.Terminate(ErrorCode.ERROR_INVALID_FUNCTION); + return; + } + + if (App.LaunchSettings.IsQuiet) + { + var installer = new Installer(); + + if (!installer.CheckInstallLocation()) + App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE); + + installer.DoInstall(); + + ProcessLaunchArgs(); + } + else + { + new LanguageSelectorDialog().ShowDialog(); + + var installer = new UI.Elements.Installer.MainWindow(); + installer.ShowDialog(); + + ProcessNextAction(installer.CloseAction, !installer.Finished); + } + + } + + public static void LaunchUninstaller() + { + bool confirmed = false; + bool keepData = true; + + if (App.LaunchSettings.IsQuiet) + { + confirmed = true; + } + else + { + var dialog = new UninstallerDialog(); + dialog.ShowDialog(); + + confirmed = dialog.Confirmed; + keepData = dialog.KeepData; + } + + if (!confirmed) + return; + + Installer.DoUninstall(keepData); + + Frontend.ShowMessageBox(Strings.Bootstrapper_SuccessfullyUninstalled, MessageBoxImage.Information); + } + + public static void LaunchSettings() + { + const string LOG_IDENT = "LaunchHandler::LaunchSettings"; + + // TODO: move to mutex (especially because multi language whatever) + + Process? menuProcess = Utilities.GetProcessesSafe().Where(x => x.MainWindowTitle == Strings.Menu_Title).FirstOrDefault(); + + if (menuProcess is not null) + { + var handle = menuProcess.MainWindowHandle; + App.Logger.WriteLine(LOG_IDENT, $"Found an already existing menu window with handle {handle}"); + PInvoke.SetForegroundWindow((HWND)handle); + } + else + { + bool showAlreadyRunningWarning = Process.GetProcessesByName(App.ProjectName).Length > 1 && !App.LaunchSettings.IsQuiet; + new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning).ShowDialog(); + } + } + + public static void LaunchMenu() + { + var dialog = new LaunchMenuDialog(); + dialog.ShowDialog(); + + ProcessNextAction(dialog.CloseAction); + } + + public static void LaunchRoblox() + { + const string LOG_IDENT = "LaunchHandler::LaunchRoblox"; + + bool installWebView2 = false; + + if (!File.Exists(Path.Combine(Paths.System, "mfplat.dll"))) + { + Frontend.ShowMessageBox(Strings.Bootstrapper_WMFNotFound, MessageBoxImage.Error); + + if (!App.LaunchSettings.IsQuiet) + Utilities.ShellExecute("https://support.microsoft.com/en-us/topic/media-feature-pack-list-for-windows-n-editions-c1c6fffa-d052-8338-7a79-a4bb980a700a"); + + App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND); + } + + { + using var hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); + using var hkcuKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"); + + if (hklmKey is null && hkcuKey is null) + installWebView2 = Frontend.ShowMessageBox(Strings.Bootstrapper_WebView2NotFound, MessageBoxImage.Warning, MessageBoxButton.YesNo, MessageBoxResult.Yes) == MessageBoxResult.Yes; + } + + if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _)) + { + // this currently doesn't work very well since it relies on checking the existence of the singleton mutex + // which often hangs around for a few seconds after the window closes + // it would be better to have this rely on the activity tracker when we implement IPC in the planned refactoring + + var result = Frontend.ShowMessageBox(Strings.Bootstrapper_ConfirmLaunch, MessageBoxImage.Warning, MessageBoxButton.YesNo); + + if (result != MessageBoxResult.Yes) + { + App.Terminate(); + return; + } + } + + App.NotifyIcon = new(); + + // start bootstrapper and show the bootstrapper modal if we're not running silently + App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper"); + var bootstrapper = new Bootstrapper(App.LaunchSettings.RobloxLaunchArgs, App.LaunchSettings.RobloxLaunchMode, installWebView2); + IBootstrapperDialog? dialog = null; + + if (!App.LaunchSettings.IsQuiet) + { + App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog"); + dialog = App.Settings.Prop.BootstrapperStyle.GetNew(); + bootstrapper.Dialog = dialog; + dialog.Bootstrapper = bootstrapper; + } + + Task bootstrapperTask = Task.Run(async () => await bootstrapper.Run()).ContinueWith(t => + { + App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished"); + + // notifyicon is blocking main thread, must be disposed here + App.NotifyIcon?.Dispose(); + + if (t.IsFaulted) + App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper"); + + if (t.Exception is null) + return; + + App.Logger.WriteException(LOG_IDENT, t.Exception); + + Exception exception = t.Exception; + +#if !DEBUG + if (t.Exception.GetType().ToString() == "System.AggregateException") + exception = t.Exception.InnerException!; +#endif + + App.FinalizeExceptionHandling(exception, false); + }); + + // this ordering is very important as all wpf windows are shown as modal dialogs, mess it up and you'll end up blocking input to one of them + dialog?.ShowBootstrapper(); + + if (!App.LaunchSettings.IsNoLaunch && App.Settings.Prop.EnableActivityTracking) + App.NotifyIcon?.InitializeContextMenu(); + + App.Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish"); + + bootstrapperTask.Wait(); + } + } +} diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs index 07e3eee..54350f1 100644 --- a/Bloxstrap/LaunchSettings.cs +++ b/Bloxstrap/LaunchSettings.cs @@ -12,8 +12,11 @@ namespace Bloxstrap { public class LaunchSettings { - [LaunchFlag(new[] { "-preferences", "-menu" })] - public bool IsMenuLaunch { get; private set; } = false; + [LaunchFlag(new[] { "-preferences", "-menu", "-settings" })] + public bool IsMenuLaunch { get; set; } = false; + + [LaunchFlag("-player")] + public bool IsRobloxLaunch { get; set; } = false; [LaunchFlag("-quiet")] public bool IsQuiet { get; private set; } = false; diff --git a/Bloxstrap/Logger.cs b/Bloxstrap/Logger.cs index 344d55a..2eea25d 100644 --- a/Bloxstrap/Logger.cs +++ b/Bloxstrap/Logger.cs @@ -16,6 +16,7 @@ { const string LOG_IDENT = "Logger::Initialize"; + // TODO: /Bloxstrap/Logs/ string directory = useTempDir ? Path.Combine(Paths.LocalAppData, "Temp") : Path.Combine(Paths.Base, "Logs"); string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'"); string filename = $"{App.ProjectName}_{timestamp}.log"; diff --git a/Bloxstrap/Paths.cs b/Bloxstrap/Paths.cs index ecdc71c..cd04cc8 100644 --- a/Bloxstrap/Paths.cs +++ b/Bloxstrap/Paths.cs @@ -7,7 +7,7 @@ public static string UserProfile => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); public static string LocalAppData => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); public static string Desktop => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); - public static string StartMenu => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", App.ProjectName); + public static string WindowsStartMenu => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs"); public static string System => Environment.GetFolderPath(Environment.SpecialFolder.System); public static string Process => Environment.ProcessPath!; @@ -35,8 +35,6 @@ Modifications = Path.Combine(Base, "Modifications"); Application = Path.Combine(Base, $"{App.ProjectName}.exe"); - - } } } diff --git a/Bloxstrap/ProtocolHandler.cs b/Bloxstrap/ProtocolHandler.cs index eb24592..6338c77 100644 --- a/Bloxstrap/ProtocolHandler.cs +++ b/Bloxstrap/ProtocolHandler.cs @@ -73,9 +73,9 @@ namespace Bloxstrap { string handlerArgs = $"\"{handler}\" %1"; - using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}"); - using RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon"); - using RegistryKey uriCommandKey = uriKey.CreateSubKey(@"shell\open\command"); + using var uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}"); + using var uriIconKey = uriKey.CreateSubKey("DefaultIcon"); + using var uriCommandKey = uriKey.CreateSubKey(@"shell\open\command"); if (uriKey.GetValue("") is null) { diff --git a/Bloxstrap/Resources/Menu/StartMenu.png b/Bloxstrap/Resources/Menu/StartMenu.png deleted file mode 100644 index 7a54f04..0000000 Binary files a/Bloxstrap/Resources/Menu/StartMenu.png and /dev/null differ diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 7b20e22..a83be9a 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -296,6 +296,15 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to You currently do not have the WebView2 runtime installed. Some Roblox features will not work properly without it, such as the desktop app. Would you like to download it now?. + /// + public static string Bootstrapper_WebView2NotFound { + get { + return ResourceManager.GetString("Bootstrapper.WebView2NotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to Roblox requires the use of Windows Media Foundation components. You appear to be missing them, likely because you are using an N edition of Windows. Please install them first, and then launch Roblox.. /// @@ -440,6 +449,24 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Back. + /// + public static string Common_Navigation_Back { + get { + return ResourceManager.GetString("Common.Navigation.Back", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Next. + /// + public static string Common_Navigation_Next { + get { + return ResourceManager.GetString("Common.Navigation.Next", resourceCulture); + } + } + /// /// Looks up a localized string similar to New. /// @@ -657,6 +684,15 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Bloxstrap was unable to create shortcuts for the Desktop and Start menu. Try creating them later through Bloxstrap Settings.. + /// + public static string Dialog_CannotCreateShortcuts { + get { + return ResourceManager.GetString("Dialog.CannotCreateShortcuts", resourceCulture); + } + } + /// /// Looks up a localized string similar to More information:. /// @@ -1139,6 +1175,223 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Will drop you into the desktop app once everything's done. + /// + public static string Installer_Completion_Launch_Description { + get { + return ResourceManager.GetString("Installer.Completion.Launch.Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install and launch Roblox. + /// + public static string Installer_Completion_Launch_Title { + get { + return ResourceManager.GetString("Installer.Completion.Launch.Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tweak with all the features it has to offer. + /// + public static string Installer_Completion_Settings_Description { + get { + return ResourceManager.GetString("Installer.Completion.Settings.Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configure Bloxstrap's settings. + /// + public static string Installer_Completion_Settings_Title { + get { + return ResourceManager.GetString("Installer.Completion.Settings.Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bloxstrap has successfully been installed. + /// + ///Roblox has not yet been installed, that will happen when you launch it with Bloxstrap for the first time. However, before you do that, you may want to configure Bloxstrap's settings first. + /// + ///Also, to keep Bloxstrap registered as the website launch handler, avoid using the "Roblox Player" shortcut to launch Roblox. If you don't see Bloxstrap show when launching from the website, simply launch Roblox with Bloxstrap once from the desktop to fix it. + /// + ///What would y [rest of string was truncated]";. + /// + public static string Installer_Completion_Text { + get { + return ResourceManager.GetString("Installer.Completion.Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Completion. + /// + public static string Installer_Completion_Title { + get { + return ResourceManager.GetString("Installer.Completion.Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Existing data found. Your mods and settings will be restored.. + /// + public static string Installer_Install_Location_DataFound { + get { + return ResourceManager.GetString("Installer.Install.Location.DataFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Roblox will also be installed to this path. Change this if you prefer to install all your games to a separate drive. Otherwise, it's recommended that you keep this as it is.. + /// + public static string Installer_Install_Location_Text { + get { + return ResourceManager.GetString("Installer.Install.Location.Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Choose where to install to. + /// + public static string Installer_Install_Location_Title { + get { + return ResourceManager.GetString("Installer.Install.Location.Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create Desktop shortcuts. + /// + public static string Installer_Install_Shortcuts_Desktop { + get { + return ResourceManager.GetString("Installer.Install.Shortcuts.Desktop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create Start Menu shortcuts. + /// + public static string Installer_Install_Shortcuts_StartMenu { + get { + return ResourceManager.GetString("Installer.Install.Shortcuts.StartMenu", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shortcuts. + /// + public static string Installer_Install_Shortcuts_Title { + get { + return ResourceManager.GetString("Installer.Install.Shortcuts.Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install. + /// + public static string Installer_Install_Title { + get { + return ResourceManager.GetString("Installer.Install.Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bloxstrap Installer. + /// + public static string Installer_Title { + get { + return ResourceManager.GetString("Installer.Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Thank you for downloading Bloxstrap. + /// + ///You should have gotten it from either {0} or {1}. Those are the only official websites to get it from. + /// + ///This installation process will be quick and simple, and you will be able to configure any of Bloxstrap's settings after installation.. + /// + public static string Installer_Welcome_MainText { + get { + return ResourceManager.GetString("Installer.Welcome.MainText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please click 'Next' to continue.. + /// + public static string Installer_Welcome_NextToContinue { + get { + return ResourceManager.GetString("Installer.Welcome.NextToContinue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome. + /// + public static string Installer_Welcome_Title { + get { + return ResourceManager.GetString("Installer.Welcome.Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are trying to install version {0} of Bloxstrap, but the latest version available is {1}. Would you like to download it?. + /// + public static string Installer_Welcome_UpdateNotice { + get { + return ResourceManager.GetString("Installer.Welcome.UpdateNotice", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configure settings. + /// + public static string LaunchMenu_ConfigureSettings { + get { + return ResourceManager.GetString("LaunchMenu.ConfigureSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Launch Roblox. + /// + public static string LaunchMenu_LaunchRoblox { + get { + return ResourceManager.GetString("LaunchMenu.LaunchRoblox", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to What do you want to do?. + /// + public static string LaunchMenu_Title { + get { + return ResourceManager.GetString("LaunchMenu.Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to See the Wiki for help. + /// + public static string LaunchMenu_Wiki_Description { + get { + return ResourceManager.GetString("LaunchMenu.Wiki.Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Having an issue?. + /// + public static string LaunchMenu_Wiki_Title { + get { + return ResourceManager.GetString("LaunchMenu.Wiki.Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to No log file will be written for this launch because Bloxstrap is unable to write to the folder at '{0}'. /// @@ -2103,87 +2356,6 @@ namespace Bloxstrap.Resources { } } - /// - /// Looks up a localized string similar to Install. - /// - public static string Menu_Install { - get { - return ResourceManager.GetString("Menu.Install", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Configure how Bloxstrap/Roblox is installed.. - /// - public static string Menu_Installation_Description { - get { - return ResourceManager.GetString("Menu.Installation.Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Choose where Bloxstrap should be installed to.. - /// - public static string Menu_Installation_InstallLocation_Description { - get { - return ResourceManager.GetString("Menu.Installation.InstallLocation.Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Install Location. - /// - public static string Menu_Installation_InstallLocation_Title { - get { - return ResourceManager.GetString("Menu.Installation.InstallLocation.Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Where Bloxstrap is currently installed to.. - /// - public static string Menu_Installation_OpenInstallFolder_Description { - get { - return ResourceManager.GetString("Menu.Installation.OpenInstallFolder.Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open Installation Folder. - /// - public static string Menu_Installation_OpenInstallFolder_Title { - get { - return ResourceManager.GetString("Menu.Installation.OpenInstallFolder.Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Installation. - /// - public static string Menu_Installation_Title { - get { - return ResourceManager.GetString("Menu.Installation.Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Here's a guide on how to uninstall Bloxstrap.. - /// - public static string Menu_Installation_UninstallGuide_Description { - get { - return ResourceManager.GetString("Menu.Installation.UninstallGuide.Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Looking to uninstall?. - /// - public static string Menu_Installation_UninstallGuide_Title { - get { - return ResourceManager.GetString("Menu.Installation.UninstallGuide.Title", resourceCulture); - } - } - /// /// Looks up a localized string similar to Bloxstrap cannot be installed here. Please choose a different location, or resort to using the default location by clicking the reset button.. /// @@ -2523,15 +2695,6 @@ namespace Bloxstrap.Resources { } } - /// - /// Looks up a localized string similar to Bloxstrap must first be installed.. - /// - public static string Menu_Mods_OpenModsFolder_MustBeInstalled { - get { - return ResourceManager.GetString("Menu.Mods.OpenModsFolder.MustBeInstalled", resourceCulture); - } - } - /// /// Looks up a localized string similar to Open Mods Folder. /// @@ -2649,42 +2812,6 @@ namespace Bloxstrap.Resources { } } - /// - /// Looks up a localized string similar to There's just a few things you first should know about.. - /// - public static string Menu_PreInstall_Description { - get { - return ResourceManager.GetString("Menu.PreInstall.Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to After installation has finished, the Bloxstrap Menu will be registered as an application in the Start menu. If you ever need to access it again to re-adjust your settings, or access resources such as Fast Flag management, you can find it there.. - /// - public static string Menu_PreInstall_Info_1 { - get { - return ResourceManager.GetString("Menu.PreInstall.Info.1", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If you ever need help or guidance with anything, be sure to check the [Wiki]({0}). If you still need something, open an [issue]({1}) on GitHub, or join our [Discord server]({2}).. - /// - public static string Menu_PreInstall_Info_2 { - get { - return ResourceManager.GetString("Menu.PreInstall.Info.2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Before you install.... - /// - public static string Menu_PreInstall_Title { - get { - return ResourceManager.GetString("Menu.PreInstall.Title", resourceCulture); - } - } - /// /// Looks up a localized string similar to Save. /// @@ -2713,12 +2840,63 @@ namespace Bloxstrap.Resources { } /// - /// Looks up a localized string similar to Bloxstrap Menu. + /// Looks up a localized string similar to Bloxstrap Settings. /// public static string Menu_Title { get { return ResourceManager.GetString("Menu.Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to They'll be kept where Bloxstrap was installed, and will automatically be restored on a reinstall.. + /// + public static string Uninstaller_KeepData_Description { + get { + return ResourceManager.GetString("Uninstaller.KeepData.Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keep my settings and mods. + /// + public static string Uninstaller_KeepData_Label { + get { + return ResourceManager.GetString("Uninstaller.KeepData.Label", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uninstalling will remove Bloxstrap from your system, and automatically reconfigure the default Roblox launcher if it's still installed. + /// + ///If you're uninstalling or reinstalling because you are having issues with Roblox, read [this help page]({0}) first. + /// + ///The uninstall process may not be able to fully clean up itself, so you may need to manually clean up leftover files where Bloxstrap was installed. + /// + ///Bloxstrap was installed at "{1}".. + /// + public static string Uninstaller_Text { + get { + return ResourceManager.GetString("Uninstaller.Text", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uninstall Bloxstrap. + /// + public static string Uninstaller_Title { + get { + return ResourceManager.GetString("Uninstaller.Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uninstall. + /// + public static string Uninstaller_Uninstall { + get { + return ResourceManager.GetString("Uninstaller.Uninstall", resourceCulture); + } + } } } diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index fa1bdb3..5704db3 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -707,33 +707,6 @@ Do NOT use this to import large "flag lists" made by other people that promise t Icon files - - Install - - - Configure how Bloxstrap/Roblox is installed. - - - Choose where Bloxstrap should be installed to. - - - Install Location - - - Where Bloxstrap is currently installed to. - - - Open Installation Folder - - - Installation - - - Here's a guide on how to uninstall Bloxstrap. - - - Looking to uninstall? - Bloxstrap cannot be installed here. Please choose a different location, or resort to using the default location by clicking the reset button. @@ -852,9 +825,6 @@ Selecting 'No' will ignore this warning and continue installation. Manage custom Roblox mods here. - - Bloxstrap must first be installed. - Open Mods Folder @@ -894,18 +864,6 @@ Selecting 'No' will ignore this warning and continue installation. Click for more information on this option. - - There's just a few things you first should know about. - - - After installation has finished, the Bloxstrap Menu will be registered as an application in the Start menu. If you ever need to access it again to re-adjust your settings, or access resources such as Fast Flag management, you can find it there. - - - If you ever need help or guidance with anything, be sure to check the [Wiki]({0}). If you still need something, open an [issue]({1}) on GitHub, or join our [Discord server]({2}). - - - Before you install... - Save @@ -916,7 +874,7 @@ Selecting 'No' will ignore this warning and continue installation. Settings saved! - Bloxstrap Menu + Bloxstrap Settings Choose preferred language @@ -1027,4 +985,116 @@ Selecting 'No' will ignore this warning and continue installation. Roblox is still launching. A log file will only be available once Roblox launches. + + Bloxstrap Installer + + + Welcome + + + Install + + + Completion + + + Back + + + Next + + + Thank you for downloading Bloxstrap. + +You should have gotten it from either {0} or {1}. Those are the only official websites to get it from. + +This installation process will be quick and simple, and you will be able to configure any of Bloxstrap's settings after installation. + + + Please click 'Next' to continue. + + + You are trying to install version {0} of Bloxstrap, but the latest version available is {1}. Would you like to download it? + + + Choose where to install to + + + Roblox will also be installed to this path. Change this if you prefer to install all your games to a separate drive. Otherwise, it's recommended that you keep this as it is. + + + Existing data found. Your mods and settings will be restored. + + + Shortcuts + + + Create Desktop shortcuts + + + Create Start Menu shortcuts + + + Bloxstrap has successfully been installed. + +Roblox has not yet been installed, that will happen when you launch it with Bloxstrap for the first time. However, before you do that, you may want to configure Bloxstrap's settings first. + +Also, to keep Bloxstrap registered as the website launch handler, avoid using the "Roblox Player" shortcut to launch Roblox. If you don't see Bloxstrap show when launching from the website, simply launch Roblox with Bloxstrap once from the desktop to fix it. + +What would you like to do? + + + Configure Bloxstrap's settings + + + Tweak with all the features it has to offer + + + Install and launch Roblox + + + Will drop you into the desktop app once everything's done + + + Uninstall Bloxstrap + + + Uninstalling will remove Bloxstrap from your system, and automatically reconfigure the default Roblox launcher if it's still installed. + +If you're uninstalling or reinstalling because you are having issues with Roblox, read [this help page]({0}) first. + +The uninstall process may not be able to fully clean up itself, so you may need to manually clean up leftover files where Bloxstrap was installed. + +Bloxstrap was installed at "{1}". + + + Keep my settings and mods + + + They'll be kept where Bloxstrap was installed, and will automatically be restored on a reinstall. + + + Uninstall + + + What do you want to do? + + + Launch Roblox + + + Configure settings + + + Having an issue? + + + See the Wiki for help + + + You currently do not have the WebView2 runtime installed. Some Roblox features will not work properly without it, such as the desktop app. Would you like to download it now? + + + Bloxstrap was unable to create shortcuts for the Desktop and Start menu. Try creating them later through Bloxstrap Settings. + \ No newline at end of file diff --git a/Bloxstrap/RobloxDeployment.cs b/Bloxstrap/RobloxDeployment.cs index 7f00336..6c97d4e 100644 --- a/Bloxstrap/RobloxDeployment.cs +++ b/Bloxstrap/RobloxDeployment.cs @@ -1,5 +1,6 @@ namespace Bloxstrap { + // TODO: this is a mess and desperately needs refactoring public static class RobloxDeployment { public const string DefaultChannel = "LIVE"; @@ -52,7 +53,7 @@ // this function serves double duty as the setup mirror enumerator, and as our connectivity check // since we're basically asking four different urls for the exact same thing, if all four fail, then it has to be a user-side problem - // this should be checked for in the installer, in the menu, and in the bootstrapper, as each of those have a dedicated spot they show in + // this should be checked for in the installer and in the bootstrapper // returns null for success @@ -168,7 +169,7 @@ { var defaultClientVersion = await GetInfo(DefaultChannel); - if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == -1) + if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == VersionComparison.LessThan) clientVersion.IsBehindDefaultChannel = true; } diff --git a/Bloxstrap/UI/Elements/Dialogs/LanguageSelectorDialog.xaml b/Bloxstrap/UI/Elements/Dialogs/LanguageSelectorDialog.xaml index 5794e85..ab9d48e 100644 --- a/Bloxstrap/UI/Elements/Dialogs/LanguageSelectorDialog.xaml +++ b/Bloxstrap/UI/Elements/Dialogs/LanguageSelectorDialog.xaml @@ -8,7 +8,7 @@ xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base" xmlns:resources="clr-namespace:Bloxstrap.Resources" mc:Ignorable="d" - Title="Bloxstrap" + Title="{x:Static resources:Strings.Installer_Title}" MinWidth="0" MinHeight="0" Width="380" @@ -24,10 +24,10 @@ - + - + diff --git a/Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml b/Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml new file mode 100644 index 0000000..446b34c --- /dev/null +++ b/Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml.cs b/Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml.cs new file mode 100644 index 0000000..f51898f --- /dev/null +++ b/Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using Bloxstrap.UI.ViewModels.Dialogs; +using Bloxstrap.UI.ViewModels.Installer; +using Wpf.Ui.Mvvm.Interfaces; + +namespace Bloxstrap.UI.Elements.Dialogs +{ + /// + /// Interaction logic for LaunchMenuDialog.xaml + /// + public partial class LaunchMenuDialog + { + public NextAction CloseAction = NextAction.Terminate; + + public LaunchMenuDialog() + { + var viewModel = new LaunchMenuViewModel(); + viewModel.CloseWindowRequest += (_, closeAction) => + { + CloseAction = closeAction; + Close(); + }; + + DataContext = viewModel; + + InitializeComponent(); + ApplyTheme(); + } + } +} diff --git a/Bloxstrap/UI/Elements/Dialogs/UninstallerDialog.xaml b/Bloxstrap/UI/Elements/Dialogs/UninstallerDialog.xaml new file mode 100644 index 0000000..4bc1546 --- /dev/null +++ b/Bloxstrap/UI/Elements/Dialogs/UninstallerDialog.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + +