From 02b4fe923efb210178afa5e435934db9878a3959 Mon Sep 17 00:00:00 2001 From: Ryan Luu Date: Sun, 20 Oct 2024 13:38:12 -0700 Subject: [PATCH 01/33] Add Bloxstrap version to bug report Issue Form template (#3357) * Add input for Bloxstrap version to bug report issue form * Update bug_report.yaml --------- Co-authored-by: pizzaboxer --- .github/ISSUE_TEMPLATE/bug_report.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index e06c990..00cd191 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -30,6 +30,14 @@ body: - label: I am using the latest version of Bloxstrap. required: true - label: I did not answer truthfully to all the above checkboxes. + - type: input + id: version + attributes: + label: Bloxstrap Version + description: "What version of Bloxstrap are you using? Find it in the 'About' section of the Settings" + placeholder: "v1.0.0" + validations: + required: true - type: textarea id: what-happened attributes: From 60beb1100f592dafcba01fa1c4b0651b6e7afa44 Mon Sep 17 00:00:00 2001 From: BizzarBlitz <121737432+BizzarBlitz@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:39:04 -0400 Subject: [PATCH 02/33] Update Strings.resx (#3350) Fast Flags tab -> Engine Settings tag --- Bloxstrap/Resources/Strings.resx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 6b7fe31..3c777f9 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -757,7 +757,7 @@ Selecting 'No' will ignore this warning and continue installation. Choose font... - Font size can be adjusted in the Fast Flags tab. + Font size can be adjusted in the Engine Settings tab. The file you have chosen does not appear to be a valid font file. @@ -1239,4 +1239,4 @@ Would you like to enable test mode? Version {0} - \ No newline at end of file + From 4998fcd72ddd8385559bca9b465ba638a49e96e8 Mon Sep 17 00:00:00 2001 From: Matt <97983689+bluepilledgreat@users.noreply.github.com> Date: Thu, 24 Oct 2024 20:23:58 +0100 Subject: [PATCH 03/33] fix bloxstrap never closing if another menu is open (#3453) --- Bloxstrap/LaunchHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs index 35739fd..cfd1a29 100644 --- a/Bloxstrap/LaunchHandler.cs +++ b/Bloxstrap/LaunchHandler.cs @@ -157,6 +157,8 @@ namespace Bloxstrap if (process is not null) PInvoke.SetForegroundWindow((HWND)process.MainWindowHandle); + + App.Terminate(); } } From 2f3f52b1cc47271651a25dcd33cc4d1199d502b1 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 24 Oct 2024 20:49:06 +0100 Subject: [PATCH 04/33] Re-add missing flag things i still have NO idea how this happened, please don't ask me :( --- Bloxstrap/FastFlagManager.cs | 4 +++- Bloxstrap/Installer.cs | 2 -- Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs index 284df3d..c5ac0a2 100644 --- a/Bloxstrap/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -243,9 +243,11 @@ namespace Bloxstrap // clone the dictionary OriginalProp = new(Prop); - // TODO - remove when activity tracking has been revamped if (GetPreset("Network.Log") != "7") SetPreset("Network.Log", "7"); + + if (GetPreset("Rendering.ManualFullscreen") != "False") + SetPreset("Rendering.ManualFullscreen", "False"); } } } diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs index e9a4045..4bdf2fd 100644 --- a/Bloxstrap/Installer.cs +++ b/Bloxstrap/Installer.cs @@ -359,8 +359,6 @@ namespace Bloxstrap || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp")) || Paths.Process.StartsWith(Paths.TempUpdates); - isAutoUpgrade = true; - var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion; var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion; diff --git a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml index dda7e8f..239644e 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml @@ -22,6 +22,12 @@ + + + + From f1c320dc8f9d7488f6d06b60cb7cd70969bf8cef Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 24 Oct 2024 23:09:25 +0100 Subject: [PATCH 05/33] Define tab indexes for launch dialog --- Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml b/Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml index 99e944c..17d0fff 100644 --- a/Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml +++ b/Bloxstrap/UI/Elements/Dialogs/LaunchMenuDialog.xaml @@ -58,13 +58,13 @@ - + - + @@ -72,7 +72,7 @@ - + From a34c73a1b084d1338d672e51c54945963124ae2d Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 24 Oct 2024 23:19:01 +0100 Subject: [PATCH 06/33] Infer Roblox launch URIs in launch flag parser Should fix problems with Firefox's launch handling Addresses #3186 --- Bloxstrap/LaunchSettings.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Bloxstrap/LaunchSettings.cs b/Bloxstrap/LaunchSettings.cs index c700d9d..62c87f5 100644 --- a/Bloxstrap/LaunchSettings.cs +++ b/Bloxstrap/LaunchSettings.cs @@ -68,6 +68,19 @@ namespace Bloxstrap _flagMap.Add(identifier, flag); } + // infer roblox launch uris + if (Args.Length >= 1) + { + string arg = Args[0]; + + if (arg.StartsWith("roblox:", StringComparison.OrdinalIgnoreCase) + || arg.StartsWith("roblox-player:", StringComparison.OrdinalIgnoreCase)) + { + RobloxLaunchMode = LaunchMode.Player; + RobloxLaunchArgs = arg; + } + } + // parse for (int i = 0; i < Args.Length; i++) { From d9a84f4cf97488054f1f3d52ba8866c33fb8d579 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 24 Oct 2024 23:20:04 +0100 Subject: [PATCH 07/33] Bump version --- Bloxstrap/Bloxstrap.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 16b56e6..9f1b69a 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -7,8 +7,8 @@ true True Bloxstrap.ico - 2.8.0 - 2.8.0 + 2.8.1 + 2.8.1 app.manifest true false From 6dc8e286ceaa1091c01c534b3aa48b946fbb3f5b Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 24 Oct 2024 23:34:51 +0100 Subject: [PATCH 08/33] Improve Roblox launch detection --- Bloxstrap/AppData/IAppData.cs | 2 - Bloxstrap/AppData/RobloxPlayerData.cs | 2 - Bloxstrap/AppData/RobloxStudioData.cs | 2 - Bloxstrap/Bootstrapper.cs | 122 ++++++++++++-------------- 4 files changed, 55 insertions(+), 73 deletions(-) diff --git a/Bloxstrap/AppData/IAppData.cs b/Bloxstrap/AppData/IAppData.cs index b8a45c9..7af04f7 100644 --- a/Bloxstrap/AppData/IAppData.cs +++ b/Bloxstrap/AppData/IAppData.cs @@ -10,8 +10,6 @@ string ExecutableName { get; } - string StartEvent { get; } - string Directory { get; } string LockFilePath { get; } diff --git a/Bloxstrap/AppData/RobloxPlayerData.cs b/Bloxstrap/AppData/RobloxPlayerData.cs index 923c6a1..1477b8c 100644 --- a/Bloxstrap/AppData/RobloxPlayerData.cs +++ b/Bloxstrap/AppData/RobloxPlayerData.cs @@ -16,8 +16,6 @@ namespace Bloxstrap.AppData public override string ExecutableName => "RobloxPlayerBeta.exe"; - public string StartEvent => "www.roblox.com/robloxStartedEvent"; - public override string Directory => Path.Combine(Paths.Roblox, "Player"); public AppState State => App.State.Prop.Player; diff --git a/Bloxstrap/AppData/RobloxStudioData.cs b/Bloxstrap/AppData/RobloxStudioData.cs index 73c630b..4bc2369 100644 --- a/Bloxstrap/AppData/RobloxStudioData.cs +++ b/Bloxstrap/AppData/RobloxStudioData.cs @@ -10,8 +10,6 @@ public override string ExecutableName => "RobloxStudioBeta.exe"; - public string StartEvent => "www.roblox.com/robloxStudioStartedEvent"; - public override string Directory => Path.Combine(Paths.Roblox, "Studio"); public AppState State => App.State.Prop.Studio; diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 7a42594..d1a4a5f 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -319,20 +319,15 @@ namespace Bloxstrap SetStatus(Strings.Bootstrapper_Status_Starting); - if (_launchMode == LaunchMode.Player) + if (_launchMode == LaunchMode.Player && App.Settings.Prop.ForceRobloxLanguage) { - if (App.Settings.Prop.ForceRobloxLanguage) - { - var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant); + var match = Regex.Match(_launchCommandLine, "gameLocale:([a-z_]+)", RegexOptions.CultureInvariant); - if (match.Groups.Count == 2) - _launchCommandLine = _launchCommandLine.Replace("robloxLocale:en_us", $"robloxLocale:{match.Groups[1].Value}", StringComparison.InvariantCultureIgnoreCase); - } - - if (!String.IsNullOrEmpty(_launchCommandLine)) - _launchCommandLine += " "; - - _launchCommandLine += "-isInstallerLaunch"; + if (match.Groups.Count == 2) + _launchCommandLine = _launchCommandLine.Replace( + "robloxLocale:en_us", + $"robloxLocale:{match.Groups[1].Value}", + StringComparison.OrdinalIgnoreCase); } var startInfo = new ProcessStartInfo() @@ -350,66 +345,56 @@ namespace Bloxstrap string? logFileName = null; - using (var startEvent = new EventWaitHandle(false, EventResetMode.ManualReset, AppData.StartEvent)) + string rbxLogDir = Path.Combine(Paths.LocalAppData, "Roblox\\logs"); + + if (!Directory.Exists(rbxLogDir)) + Directory.CreateDirectory(rbxLogDir); + + var logWatcher = new FileSystemWatcher() { - startEvent.Reset(); + Path = rbxLogDir, + Filter = "*.log", + EnableRaisingEvents = true + }; - string rbxLogDir = Path.Combine(Paths.LocalAppData, "Roblox\\logs"); + var logCreatedEvent = new AutoResetEvent(false); - if (!Directory.Exists(rbxLogDir)) - Directory.CreateDirectory(rbxLogDir); + logWatcher.Created += (_, e) => + { + logWatcher.EnableRaisingEvents = false; + logFileName = e.FullPath; + logCreatedEvent.Set(); + }; - var logWatcher = new FileSystemWatcher() - { - Path = rbxLogDir, - Filter = "*.log", - EnableRaisingEvents = true - }; - - var logCreatedEvent = new AutoResetEvent(false); - - logWatcher.Created += (_, e) => - { - logWatcher.EnableRaisingEvents = false; - logFileName = e.FullPath; - logCreatedEvent.Set(); - }; - - // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now - try - { - using var process = Process.Start(startInfo)!; - _appPid = process.Id; - } - catch (Exception) - { - // attempt a reinstall on next launch - File.Delete(AppData.ExecutablePath); - throw; - } - - App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {_appPid}), waiting for start event"); - - if (startEvent.WaitOne(TimeSpan.FromSeconds(5))) - App.Logger.WriteLine(LOG_IDENT, "Start event signalled"); - else - App.Logger.WriteLine(LOG_IDENT, "Start event not signalled, implying successful launch"); - - logCreatedEvent.WaitOne(TimeSpan.FromSeconds(5)); - - if (String.IsNullOrEmpty(logFileName)) - { - App.Logger.WriteLine(LOG_IDENT, "Unable to identify log file"); - Frontend.ShowPlayerErrorDialog(); - return; - } - else - { - App.Logger.WriteLine(LOG_IDENT, $"Got log file as {logFileName}"); - } - - _mutex?.ReleaseAsync(); + // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now + try + { + using var process = Process.Start(startInfo)!; + _appPid = process.Id; } + catch (Exception) + { + // attempt a reinstall on next launch + File.Delete(AppData.ExecutablePath); + throw; + } + + App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {_appPid}), waiting for log file"); + + logCreatedEvent.WaitOne(TimeSpan.FromSeconds(15)); + + if (String.IsNullOrEmpty(logFileName)) + { + App.Logger.WriteLine(LOG_IDENT, "Unable to identify log file"); + Frontend.ShowPlayerErrorDialog(); + return; + } + else + { + App.Logger.WriteLine(LOG_IDENT, $"Got log file as {logFileName}"); + } + + _mutex?.ReleaseAsync(); if (IsStudioLaunch) return; @@ -466,6 +451,9 @@ namespace Bloxstrap if (ipl.IsAcquired) Process.Start(Paths.Process, args); } + + // average grace time between log being created and the window being shown + Thread.Sleep(2000); } public void Cancel() From f822c36b9254ff54ddd95ec100a3b1a78154be54 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 24 Oct 2024 23:39:52 +0100 Subject: [PATCH 09/33] Remove stat points for package download states --- Bloxstrap/Bootstrapper.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index d1a4a5f..ab8bf25 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1046,9 +1046,6 @@ namespace Bloxstrap const int maxTries = 5; - bool statIsRetrying = false; - bool statIsHttp = false; - App.Logger.WriteLine(LOG_IDENT, "Downloading..."); var buffer = new byte[4096]; @@ -1101,8 +1098,6 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, $"An exception occurred after downloading {totalBytesRead} bytes. ({i}/{maxTries})"); App.Logger.WriteException(LOG_IDENT, ex); - statIsRetrying = true; - if (ex.GetType() == typeof(ChecksumFailedException)) { App.SendStat("packageDownloadState", "httpFail"); @@ -1132,13 +1127,9 @@ namespace Bloxstrap { App.Logger.WriteLine(LOG_IDENT, "Retrying download over HTTP..."); packageUrl = packageUrl.Replace("https://", "http://"); - statIsHttp = true; } } } - - if (statIsRetrying) - App.SendStat("packageDownloadState", statIsHttp ? "httpSuccess" : "retrySuccess"); } private void ExtractPackage(Package package, List? files = null) From 7e51852c0b235a1e41a5dc310941926b2c41b357 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 24 Oct 2024 23:44:15 +0100 Subject: [PATCH 10/33] Fix emoji mod preset erroring on file existence Addresses #3352 --- Bloxstrap/Models/SettingTasks/EmojiModPresetTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bloxstrap/Models/SettingTasks/EmojiModPresetTask.cs b/Bloxstrap/Models/SettingTasks/EmojiModPresetTask.cs index 89c8d92..0f2c1d3 100644 --- a/Bloxstrap/Models/SettingTasks/EmojiModPresetTask.cs +++ b/Bloxstrap/Models/SettingTasks/EmojiModPresetTask.cs @@ -43,7 +43,7 @@ namespace Bloxstrap.Models.SettingTasks Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!); - await using var fileStream = new FileStream(_filePath, FileMode.CreateNew); + await using var fileStream = new FileStream(_filePath, FileMode.Create); await response.Content.CopyToAsync(fileStream); OriginalState = NewState; From 21396fb39ef6ff1521c23a5b9ba358041fa8f828 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Thu, 24 Oct 2024 23:49:23 +0100 Subject: [PATCH 11/33] Temporarily disable escape menu version preset Will be readded a later time when it is convenient to do so --- Bloxstrap/FastFlagManager.cs | 134 +++++++++--------- Bloxstrap/Installer.cs | 15 ++ .../Settings/Pages/FastFlagsPage.xaml | 4 +- 3 files changed, 84 insertions(+), 69 deletions(-) diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs index c5ac0a2..11a3af6 100644 --- a/Bloxstrap/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -39,17 +39,17 @@ namespace Bloxstrap { "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" }, - { "UI.Menu.Style.V2Rollout", "FIntNewInGameMenuPercentRollout3" }, - { "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" }, - { "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" }, - { "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" }, - { "UI.Menu.Style.ReportButtonCutOff", "FFlagFixReportButtonCutOff" }, + //{ "UI.Menu.Style.V2Rollout", "FIntNewInGameMenuPercentRollout3" }, + //{ "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" }, + //{ "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" }, + //{ "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" }, + //{ "UI.Menu.Style.ReportButtonCutOff", "FFlagFixReportButtonCutOff" }, - { "UI.Menu.Style.ABTest.1", "FFlagEnableMenuControlsABTest" }, - { "UI.Menu.Style.ABTest.2", "FFlagEnableV3MenuABTest3" }, - { "UI.Menu.Style.ABTest.3", "FFlagEnableInGameMenuChromeABTest3" }, - { "UI.Menu.Style.ABTest.4", "FFlagEnableInGameMenuChromeABTest4" } + //{ "UI.Menu.Style.ABTest.1", "FFlagEnableMenuControlsABTest" }, + //{ "UI.Menu.Style.ABTest.2", "FFlagEnableV3MenuABTest3" }, + //{ "UI.Menu.Style.ABTest.3", "FFlagEnableInGameMenuChromeABTest3" }, + //{ "UI.Menu.Style.ABTest.4", "FFlagEnableInGameMenuChromeABTest4" } }; public static IReadOnlyDictionary RenderingModes => new Dictionary @@ -86,68 +86,68 @@ namespace Bloxstrap // this is one hell of a dictionary definition lmao // since these all set the same flags, wouldn't making this use bitwise operators be better? - public static IReadOnlyDictionary> IGMenuVersions => new Dictionary> - { - { - InGameMenuVersion.Default, - new Dictionary - { - { "V2Rollout", null }, - { "EnableV4", null }, - { "EnableV4Chrome", null }, - { "ABTest", null }, - { "ReportButtonCutOff", null } - } - }, + //public static IReadOnlyDictionary> IGMenuVersions => new Dictionary> + //{ + // { + // InGameMenuVersion.Default, + // new Dictionary + // { + // { "V2Rollout", null }, + // { "EnableV4", null }, + // { "EnableV4Chrome", null }, + // { "ABTest", null }, + // { "ReportButtonCutOff", null } + // } + // }, - { - InGameMenuVersion.V1, - new Dictionary - { - { "V2Rollout", "0" }, - { "EnableV4", "False" }, - { "EnableV4Chrome", "False" }, - { "ABTest", "False" }, - { "ReportButtonCutOff", "False" } - } - }, + // { + // InGameMenuVersion.V1, + // new Dictionary + // { + // { "V2Rollout", "0" }, + // { "EnableV4", "False" }, + // { "EnableV4Chrome", "False" }, + // { "ABTest", "False" }, + // { "ReportButtonCutOff", "False" } + // } + // }, - { - InGameMenuVersion.V2, - new Dictionary - { - { "V2Rollout", "100" }, - { "EnableV4", "False" }, - { "EnableV4Chrome", "False" }, - { "ABTest", "False" }, - { "ReportButtonCutOff", null } - } - }, + // { + // InGameMenuVersion.V2, + // new Dictionary + // { + // { "V2Rollout", "100" }, + // { "EnableV4", "False" }, + // { "EnableV4Chrome", "False" }, + // { "ABTest", "False" }, + // { "ReportButtonCutOff", null } + // } + // }, - { - InGameMenuVersion.V4, - new Dictionary - { - { "V2Rollout", "0" }, - { "EnableV4", "True" }, - { "EnableV4Chrome", "False" }, - { "ABTest", "False" }, - { "ReportButtonCutOff", null } - } - }, + // { + // InGameMenuVersion.V4, + // new Dictionary + // { + // { "V2Rollout", "0" }, + // { "EnableV4", "True" }, + // { "EnableV4Chrome", "False" }, + // { "ABTest", "False" }, + // { "ReportButtonCutOff", null } + // } + // }, - { - InGameMenuVersion.V4Chrome, - new Dictionary - { - { "V2Rollout", "0" }, - { "EnableV4", "True" }, - { "EnableV4Chrome", "True" }, - { "ABTest", "False" }, - { "ReportButtonCutOff", null } - } - } - }; + // { + // InGameMenuVersion.V4Chrome, + // new Dictionary + // { + // { "V2Rollout", "0" }, + // { "EnableV4", "True" }, + // { "EnableV4Chrome", "True" }, + // { "ABTest", "False" }, + // { "ReportButtonCutOff", null } + // } + // } + //}; // all fflags are stored as strings // to delete a flag, set the value as null diff --git a/Bloxstrap/Installer.cs b/Bloxstrap/Installer.cs index 4bdf2fd..6742f42 100644 --- a/Bloxstrap/Installer.cs +++ b/Bloxstrap/Installer.cs @@ -590,6 +590,21 @@ namespace Bloxstrap } } + if (Utilities.CompareVersions(existingVer, "2.8.1") == VersionComparison.LessThan) + { + // wipe all escape menu flag presets + App.FastFlags.SetValue("FIntNewInGameMenuPercentRollout3", null); + App.FastFlags.SetValue("FFlagEnableInGameMenuControls", null); + App.FastFlags.SetValue("FFlagEnableInGameMenuModernization", null); + App.FastFlags.SetValue("FFlagEnableInGameMenuChrome", null); + App.FastFlags.SetValue("FFlagFixReportButtonCutOff", null); + App.FastFlags.SetValue("FFlagEnableMenuControlsABTest", null); + App.FastFlags.SetValue("FFlagEnableV3MenuABTest3", null); + App.FastFlags.SetValue("FFlagEnableInGameMenuChromeABTest3", null); + App.FastFlags.SetValue("FFlagEnableInGameMenuChromeABTest4", null); + } + + App.Settings.Save(); App.FastFlags.Save(); } diff --git a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml index 239644e..65f145f 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagsPage.xaml @@ -131,7 +131,7 @@ - @@ -141,7 +141,7 @@ - + --> Date: Fri, 25 Oct 2024 00:04:28 +0100 Subject: [PATCH 12/33] Followup to 21396fb3 my bad --- Bloxstrap/FastFlagManager.cs | 2 +- .../ViewModels/Settings/FastFlagsViewModel.cs | 56 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Bloxstrap/FastFlagManager.cs b/Bloxstrap/FastFlagManager.cs index 11a3af6..689299e 100644 --- a/Bloxstrap/FastFlagManager.cs +++ b/Bloxstrap/FastFlagManager.cs @@ -246,7 +246,7 @@ namespace Bloxstrap if (GetPreset("Network.Log") != "7") SetPreset("Network.Log", "7"); - if (GetPreset("Rendering.ManualFullscreen") != "False") + if (GetPreset("Rendering.ManualFullscreen") != "False") SetPreset("Rendering.ManualFullscreen", "False"); } } diff --git a/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs index bb3ea2d..cc1df4f 100644 --- a/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs @@ -53,39 +53,39 @@ namespace Bloxstrap.UI.ViewModels.Settings set => App.FastFlags.SetPreset("Rendering.DisableScaling", value ? "True" : null); } - public IReadOnlyDictionary> IGMenuVersions => FastFlagManager.IGMenuVersions; + //public IReadOnlyDictionary> IGMenuVersions => FastFlagManager.IGMenuVersions; - public InGameMenuVersion SelectedIGMenuVersion - { - get - { - // yeah this kinda sucks - foreach (var version in IGMenuVersions) - { - bool flagsMatch = true; + //public InGameMenuVersion SelectedIGMenuVersion + //{ + // get + // { + // // yeah this kinda sucks + // foreach (var version in IGMenuVersions) + // { + // bool flagsMatch = true; - foreach (var flag in version.Value) - { - foreach (var presetFlag in FastFlagManager.PresetFlags.Where(x => x.Key.StartsWith($"UI.Menu.Style.{flag.Key}"))) - { - if (App.FastFlags.GetValue(presetFlag.Value) != flag.Value) - flagsMatch = false; - } - } + // foreach (var flag in version.Value) + // { + // foreach (var presetFlag in FastFlagManager.PresetFlags.Where(x => x.Key.StartsWith($"UI.Menu.Style.{flag.Key}"))) + // { + // if (App.FastFlags.GetValue(presetFlag.Value) != flag.Value) + // flagsMatch = false; + // } + // } - if (flagsMatch) - return version.Key; - } + // if (flagsMatch) + // return version.Key; + // } - return IGMenuVersions.First().Key; - } + // return IGMenuVersions.First().Key; + // } - set - { - foreach (var flag in IGMenuVersions[value]) - App.FastFlags.SetPreset($"UI.Menu.Style.{flag.Key}", flag.Value); - } - } + // set + // { + // foreach (var flag in IGMenuVersions[value]) + // App.FastFlags.SetPreset($"UI.Menu.Style.{flag.Key}", flag.Value); + // } + //} public IReadOnlyDictionary LightingModes => FastFlagManager.LightingModes; From 4bb623c64e451bcca993a9d933027538d5ede9ac Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Fri, 25 Oct 2024 20:43:00 +0100 Subject: [PATCH 13/33] Add debounce for installer navigation buttons --- Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs b/Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs index 6e1926b..8198509 100644 --- a/Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs +++ b/Bloxstrap/UI/Elements/Installer/MainWindow.xaml.cs @@ -31,9 +31,7 @@ namespace Bloxstrap.UI.Elements.Installer /// - MainWindow has a single-set Func property named NextPageCallback which is reset on every page load /// - This callback is called when the next page button is pressed /// - Page CodeBehind gets MainWindow and sets the callback to its own local function on page load - /// - CodeBehind's local function then directly calls the ViewModel to do whatever it needs to do - /// - /// TODO: theme selection + /// - CodeBehind's local function then directly calls its ViewModel to do whatever it needs to do public partial class MainWindow : WpfUiWindow, INavigationWindow { @@ -43,6 +41,8 @@ namespace Bloxstrap.UI.Elements.Installer private List _pages = new() { typeof(WelcomePage), typeof(InstallPage), typeof(CompletionPage) }; + private DateTimeOffset _lastNavigation = DateTimeOffset.Now; + public Func? NextPageCallback; public NextAction CloseAction = NextAction.Terminate; @@ -55,10 +55,16 @@ namespace Bloxstrap.UI.Elements.Installer _viewModel.PageRequest += (_, type) => { + // debounce + if (DateTimeOffset.Now.Subtract(_lastNavigation).TotalMilliseconds < 500) + return; + if (type == "next") NextPage(); else if (type == "back") BackPage(); + + _lastNavigation = DateTimeOffset.Now; }; DataContext = _viewModel; From a121e14f94ac1ce8b6da331a995a6642585f0c34 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Fri, 25 Oct 2024 21:06:16 +0100 Subject: [PATCH 14/33] Make player launch error dialog less annoying additionally added clarification about the "don't exit to desktop app" option breaking the ability to accept player invites --- Bloxstrap/App.xaml.cs | 1 - Bloxstrap/Resources/Strings.Designer.cs | 8 +++++--- Bloxstrap/Resources/Strings.resx | 8 +++++--- Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml | 3 ++- Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs | 1 + .../UI/Elements/Settings/Pages/IntegrationsPage.xaml | 3 ++- Bloxstrap/UI/Frontend.cs | 8 ++++++-- 7 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index f5bcefb..7542ed4 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -218,7 +218,6 @@ namespace Bloxstrap else { // check if user profile folder has been renamed - // honestly, i'll be expecting bugs from this var match = Regex.Match(value, @"^[a-zA-Z]:\\Users\\([^\\]+)", RegexOptions.IgnoreCase); if (match.Success) diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index ad28195..115ee7e 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -1031,7 +1031,9 @@ namespace Bloxstrap.Resources { } /// - /// Looks up a localized string similar to Please read the following help information, which will open in your web browser when you close this dialog.. + /// Looks up a localized string similar to For information about why this could be happening and how this can be resolved, please read [this help article]({0}). + /// + ///Check if Roblox works with [the original launcher]({1}). If it doesn't, then this isn't a Bloxstrap issue. If it does, then refer to the help article.. /// public static string Dialog_PlayerError_HelpInformation { get { @@ -2745,7 +2747,7 @@ namespace Bloxstrap.Resources { } /// - /// Looks up a localized string similar to Roblox will fully close when you leave a game instead of dropping you back into the app.. + /// Looks up a localized string similar to Roblox will fully close when you leave a game instead of going back to the app. [Will break some things!]({0}). /// public static string Menu_Integrations_DesktopApp_Description { get { @@ -2898,7 +2900,7 @@ namespace Bloxstrap.Resources { } /// - /// Looks up a localized string similar to Font size can be adjusted in the Fast Flags tab.. + /// Looks up a localized string similar to Font size can be adjusted in the Engine Settings tab.. /// public static string Menu_Mods_Misc_CustomFont_Description { get { diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 3c777f9..1771f15 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -717,7 +717,7 @@ Selecting 'No' will ignore this warning and continue installation. Configure additional functionality to go alongside Roblox. - Roblox will fully close when you leave a game instead of dropping you back into the app. + Roblox will fully close when you leave a game instead of going back to the app. [Will break some things!]({0}) Don't exit to desktop app @@ -1104,7 +1104,9 @@ Are you sure you want to continue? Roblox has crashed. - Please read the following help information, which will open in your web browser when you close this dialog. + For information about why this could be happening and how this can be resolved, please read [this help article]({0}). + +Check if Roblox works with [the original launcher]({1}). If it doesn't, then this isn't a Bloxstrap issue. If it does, then refer to the help article. Could not load data because of a network error. @@ -1239,4 +1241,4 @@ Would you like to enable test mode? Version {0} - + \ No newline at end of file diff --git a/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml b/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml index cf6ec08..3d3ec62 100644 --- a/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml +++ b/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml @@ -6,6 +6,7 @@ xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Dialogs" xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base" + xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls" mc:Ignorable="d" Title="Bloxstrap" d:DesignWidth="480" @@ -33,7 +34,7 @@ - + diff --git a/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs b/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs index 81defd2..60b7d46 100644 --- a/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs +++ b/Bloxstrap/UI/Elements/Dialogs/FluentMessageBox.xaml.cs @@ -60,6 +60,7 @@ namespace Bloxstrap.UI.Elements.Dialogs Title = App.ProjectName; MessageTextBlock.Text = message; + MessageTextBlock.MarkdownText = message; ButtonOne.Visibility = Visibility.Collapsed; ButtonTwo.Visibility = Visibility.Collapsed; ButtonThree.Visibility = Visibility.Collapsed; diff --git a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml index 84fafee..0e391bb 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/IntegrationsPage.xaml @@ -35,7 +35,8 @@ diff --git a/Bloxstrap/UI/Frontend.cs b/Bloxstrap/UI/Frontend.cs index dc40990..cb069fc 100644 --- a/Bloxstrap/UI/Frontend.cs +++ b/Bloxstrap/UI/Frontend.cs @@ -27,9 +27,13 @@ namespace Bloxstrap.UI if (crash) topLine = Strings.Dialog_PlayerError_Crash; - ShowMessageBox($"{topLine}\n\n{Strings.Dialog_PlayerError_HelpInformation}", MessageBoxImage.Error); + string info = String.Format( + Strings.Dialog_PlayerError_HelpInformation, + $"https://github.com/{App.ProjectRepository}/wiki/Roblox-crashes-or-does-not-launch", + $"https://github.com/{App.ProjectRepository}/wiki/Switching-between-Roblox-and-Bloxstrap" + ); - Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Roblox-crashes-or-does-not-launch"); + ShowMessageBox($"{topLine}\n\n{info}", MessageBoxImage.Error); } public static void ShowExceptionDialog(Exception exception) From 5bdac105c20e28c48ccadfa323b3367dff69d94e Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Fri, 25 Oct 2024 21:19:40 +0100 Subject: [PATCH 15/33] Add option to use classic Bloxstrap logo for those afraid of change --- Bloxstrap/Enums/BootstrapperIcon.cs | 4 +++- Bloxstrap/Extensions/BootstrapperIconEx.cs | 2 ++ Bloxstrap/Properties/Resources.Designer.cs | 12 ++++++++++++ Bloxstrap/Properties/Resources.resx | 3 +++ Bloxstrap/Resources/IconBloxstrapClassic.ico | Bin 0 -> 102134 bytes 5 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Bloxstrap/Resources/IconBloxstrapClassic.ico diff --git a/Bloxstrap/Enums/BootstrapperIcon.cs b/Bloxstrap/Enums/BootstrapperIcon.cs index 8706e82..4125cc6 100644 --- a/Bloxstrap/Enums/BootstrapperIcon.cs +++ b/Bloxstrap/Enums/BootstrapperIcon.cs @@ -17,6 +17,8 @@ [EnumName(StaticName = "2022")] Icon2022, [EnumName(FromTranslation = "Common.Custom")] - IconCustom + IconCustom, + [EnumName(FromTranslation = "Enums.BootstrapperStyle.ClassicFluentDialog")] + IconBloxstrapClassic } } diff --git a/Bloxstrap/Extensions/BootstrapperIconEx.cs b/Bloxstrap/Extensions/BootstrapperIconEx.cs index b2df13c..943cad6 100644 --- a/Bloxstrap/Extensions/BootstrapperIconEx.cs +++ b/Bloxstrap/Extensions/BootstrapperIconEx.cs @@ -14,6 +14,7 @@ namespace Bloxstrap.Extensions BootstrapperIcon.IconEarly2015, BootstrapperIcon.Icon2011, BootstrapperIcon.Icon2008, + BootstrapperIcon.IconBloxstrapClassic, BootstrapperIcon.IconCustom }; @@ -61,6 +62,7 @@ namespace Bloxstrap.Extensions BootstrapperIcon.Icon2017 => Properties.Resources.Icon2017, BootstrapperIcon.Icon2019 => Properties.Resources.Icon2019, BootstrapperIcon.Icon2022 => Properties.Resources.Icon2022, + BootstrapperIcon.IconBloxstrapClassic => Properties.Resources.IconBloxstrapClassic, _ => Properties.Resources.IconBloxstrap }; } diff --git a/Bloxstrap/Properties/Resources.Designer.cs b/Bloxstrap/Properties/Resources.Designer.cs index a65c8d7..64c37de 100644 --- a/Bloxstrap/Properties/Resources.Designer.cs +++ b/Bloxstrap/Properties/Resources.Designer.cs @@ -195,5 +195,17 @@ namespace Bloxstrap.Properties { return ((System.Drawing.Icon)(obj)); } } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon IconBloxstrapClassic + { + get + { + object obj = ResourceManager.GetObject("IconBloxstrapClassic", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } } } diff --git a/Bloxstrap/Properties/Resources.resx b/Bloxstrap/Properties/Resources.resx index 7c24a62..dc3013d 100644 --- a/Bloxstrap/Properties/Resources.resx +++ b/Bloxstrap/Properties/Resources.resx @@ -154,4 +154,7 @@ ..\Resources\IconLate2015.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\IconBloxstrapClassic.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + \ No newline at end of file diff --git a/Bloxstrap/Resources/IconBloxstrapClassic.ico b/Bloxstrap/Resources/IconBloxstrapClassic.ico new file mode 100644 index 0000000000000000000000000000000000000000..56ab2c5f36e05157adb80475a267bbb1a203c4df GIT binary patch literal 102134 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 literal 0 HcmV?d00001 From c7ab37edf1494820998564a5cc18421cd28cc575 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Fri, 25 Oct 2024 21:47:59 +0100 Subject: [PATCH 16/33] Add diagnostic log for version comparison --- Bloxstrap/Utilities.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/Utilities.cs b/Bloxstrap/Utilities.cs index 6d16a27..b1cd93d 100644 --- a/Bloxstrap/Utilities.cs +++ b/Bloxstrap/Utilities.cs @@ -42,10 +42,24 @@ namespace Bloxstrap /// public static VersionComparison CompareVersions(string versionStr1, string versionStr2) { - var version1 = new Version(versionStr1.Replace("v", "")); - var version2 = new Version(versionStr2.Replace("v", "")); + try + { + var version1 = new Version(versionStr1.Replace("v", "")); + var version2 = new Version(versionStr2.Replace("v", "")); - return (VersionComparison)version1.CompareTo(version2); + return (VersionComparison)version1.CompareTo(version2); + } + catch (Exception) + { + // temporary diagnostic log for the issue described here: + // https://github.com/bloxstraplabs/bloxstrap/issues/3193 + // the problem is that this happens only on upgrade, so my only hope of catching this is bug reports following the next release + + App.Logger.WriteLine("Utilities::CompareVersions", "An exception occurred when comparing versions"); + App.Logger.WriteLine("Utilities::CompareVersions", $"versionStr1={versionStr1} versionStr2={versionStr2}"); + + throw; + } } public static string GetRobloxVersion(bool studio) From 6a93624040fb98fee0decb11d1168c5731b6807a Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Fri, 25 Oct 2024 23:44:12 +0100 Subject: [PATCH 17/33] Improve Roblox upgrade reliability Addresses #3408 --- Bloxstrap/AppData/IAppData.cs | 2 ++ Bloxstrap/AppData/RobloxPlayerData.cs | 2 ++ Bloxstrap/AppData/RobloxStudioData.cs | 4 ++- Bloxstrap/Bootstrapper.cs | 33 ++++++++++++++++++------ Bloxstrap/Logger.cs | 4 ++- Bloxstrap/Resources/Strings.Designer.cs | 11 ++++++++ Bloxstrap/Resources/Strings.resx | 6 +++++ Bloxstrap/RobloxInterfaces/Deployment.cs | 5 ++++ 8 files changed, 57 insertions(+), 10 deletions(-) diff --git a/Bloxstrap/AppData/IAppData.cs b/Bloxstrap/AppData/IAppData.cs index 7af04f7..63a6b48 100644 --- a/Bloxstrap/AppData/IAppData.cs +++ b/Bloxstrap/AppData/IAppData.cs @@ -12,6 +12,8 @@ string Directory { get; } + string OldDirectory { get; } + string LockFilePath { get; } string ExecutablePath { get; } diff --git a/Bloxstrap/AppData/RobloxPlayerData.cs b/Bloxstrap/AppData/RobloxPlayerData.cs index 1477b8c..6ec09a1 100644 --- a/Bloxstrap/AppData/RobloxPlayerData.cs +++ b/Bloxstrap/AppData/RobloxPlayerData.cs @@ -18,6 +18,8 @@ namespace Bloxstrap.AppData public override string Directory => Path.Combine(Paths.Roblox, "Player"); + public string OldDirectory => Path.Combine(Paths.Roblox, "Player.old"); + public AppState State => App.State.Prop.Player; public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() diff --git a/Bloxstrap/AppData/RobloxStudioData.cs b/Bloxstrap/AppData/RobloxStudioData.cs index 4bc2369..886e630 100644 --- a/Bloxstrap/AppData/RobloxStudioData.cs +++ b/Bloxstrap/AppData/RobloxStudioData.cs @@ -11,7 +11,9 @@ public override string ExecutableName => "RobloxStudioBeta.exe"; public override string Directory => Path.Combine(Paths.Roblox, "Studio"); - + + public string OldDirectory => Path.Combine(Paths.Roblox, "Studio.old"); + public AppState State => App.State.Prop.Studio; public override IReadOnlyDictionary PackageDirectoryMap { get; set; } = new Dictionary() diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index ab8bf25..381965a 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -452,8 +452,8 @@ namespace Bloxstrap Process.Start(Paths.Process, args); } - // average grace time between log being created and the window being shown - Thread.Sleep(2000); + // allow for window to show, since the log is created pretty far beforehand + Thread.Sleep(1000); } public void Cancel() @@ -631,19 +631,36 @@ namespace Bloxstrap { try { - // gross hack to see if roblox is still running - // i don't want to rely on mutexes because they can change, and will false flag for - // running installations that are not by bloxstrap - File.Delete(AppData.ExecutablePath); + // test to see if any files are in use + // if you have a better way to check for this, please let me know! + Directory.Move(AppData.Directory, AppData.OldDirectory); } catch (Exception ex) { - App.Logger.WriteLine(LOG_IDENT, "Could not delete executable/folder, Roblox may still be running. Aborting update."); + App.Logger.WriteLine(LOG_IDENT, "Could not clear old files, aborting update."); App.Logger.WriteException(LOG_IDENT, ex); + + // 0x80070020 is the HRESULT that indicates that a process is still running + // (either RobloxPlayerBeta or RobloxCrashHandler), so we'll silently ignore it + if ((uint)ex.HResult != 0x80070020) + { + // ensure no files are marked as read-only for good measure + foreach (var file in Directory.GetFiles(AppData.Directory, "*", SearchOption.AllDirectories)) + Filesystem.AssertReadOnly(file); + + Frontend.ShowMessageBox( + Strings.Bootstrapper_FilesInUse, + _mustUpgrade ? MessageBoxImage.Error : MessageBoxImage.Warning + ); + + if (_mustUpgrade) + App.Terminate(ErrorCode.ERROR_CANCELLED); + } + return; } - Directory.Delete(AppData.Directory, true); + Directory.Delete(AppData.OldDirectory, true); } _isInstalling = true; diff --git a/Bloxstrap/Logger.cs b/Bloxstrap/Logger.cs index 4d4ee69..ff6db1a 100644 --- a/Bloxstrap/Logger.cs +++ b/Bloxstrap/Logger.cs @@ -115,7 +115,9 @@ { Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - WriteLine($"[{identifier}] {ex}"); + string hresult = "0x" + ex.HResult.ToString("X8"); + + WriteLine($"[{identifier}] ({hresult}) {ex}"); Thread.CurrentThread.CurrentUICulture = Locale.CurrentCulture; } diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 115ee7e..41b4c8f 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -168,6 +168,17 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Bloxstrap tried to upgrade Roblox but can't because Roblox's files are still in use. + /// + ///Please close any applications that may be using Roblox's files, and relaunch.. + /// + public static string Bootstrapper_FilesInUse { + get { + return ResourceManager.GetString("Bootstrapper.FilesInUse", resourceCulture); + } + } + /// /// Looks up a localized string similar to You must first install Bloxstrap before uninstalling.. /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 1771f15..2b9ca6e 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -1241,4 +1241,10 @@ Would you like to enable test mode? Version {0} + + Bloxstrap tried to upgrade Roblox but can't because Roblox's files are still in use. + +Please close any applications that may be using Roblox's files, and relaunch. + This is *not* for when Roblox is still running when trying to upgrade. This applies to files being open (i.e. image assets) + \ No newline at end of file diff --git a/Bloxstrap/RobloxInterfaces/Deployment.cs b/Bloxstrap/RobloxInterfaces/Deployment.cs index 698be79..f6ee997 100644 --- a/Bloxstrap/RobloxInterfaces/Deployment.cs +++ b/Bloxstrap/RobloxInterfaces/Deployment.cs @@ -157,6 +157,11 @@ { clientVersion = await Http.GetJson("https://clientsettingscdn.roblox.com" + path); } + catch (HttpRequestException) + { + // throw up the exception handler chain, as we shouldn't be the one handling it + throw; + } catch (Exception ex) { App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings..."); From ce6ab31c32ad2b7bcce06892e985a482e54df1fe Mon Sep 17 00:00:00 2001 From: Matt <97983689+bluepilledgreat@users.noreply.github.com> Date: Sat, 26 Oct 2024 20:38:11 +0100 Subject: [PATCH 18/33] add length check to github issue url (#3485) --- .../Elements/Dialogs/ExceptionDialog.xaml.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml.cs b/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml.cs index 117fb62..9301fdf 100644 --- a/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml.cs @@ -16,6 +16,8 @@ namespace Bloxstrap.UI.Elements.Dialogs /// public partial class ExceptionDialog { + const int MAX_GITHUB_URL_LENGTH = 8192; + public ExceptionDialog(Exception exception) { InitializeComponent(); @@ -27,12 +29,19 @@ namespace Bloxstrap.UI.Elements.Dialogs string repoUrl = $"https://github.com/{App.ProjectRepository}"; string wikiUrl = $"{repoUrl}/wiki"; - string issueUrl = String.Format( - "{0}/issues/new?template=bug_report.yaml&title={1}&log={2}", - repoUrl, - HttpUtility.UrlEncode($"[BUG] {exception.GetType()}: {exception.Message}"), - HttpUtility.UrlEncode(String.Join('\n', App.Logger.History)) - ); + string title = HttpUtility.UrlEncode($"[BUG] {exception.GetType()}: {exception.Message}"); + string log = HttpUtility.UrlEncode(String.Join('\n', App.Logger.History)); + + string issueUrl = $"{repoUrl}/issues/new?template=bug_report.yaml&title={title}&log={log}"; + + if (issueUrl.Length > MAX_GITHUB_URL_LENGTH) + { + // url is way too long for github. remove the log parameter. + issueUrl = $"{repoUrl}/issues/new?template=bug_report.yaml&title={title}"; + + if (issueUrl.Length > MAX_GITHUB_URL_LENGTH) + issueUrl = $"{repoUrl}/issues/new?template=bug_report.yaml"; // bruh + } string helpMessage = String.Format(Strings.Dialog_Exception_Info_2, wikiUrl, issueUrl); From b3a1b1c55e18e7123c2980a23d58da4dccc0b9a7 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 26 Oct 2024 23:03:47 +0100 Subject: [PATCH 19/33] Dynamically resize supporter grid columns + fix bootstrapper bug from yesterday --- Bloxstrap/Bootstrapper.cs | 3 +++ .../Elements/About/Pages/SupportersPage.xaml | 5 ++-- .../About/Pages/SupportersPage.xaml.cs | 11 +++++++-- .../ViewModels/About/SupportersViewModel.cs | 24 ++++++++++++++++++- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 381965a..a0d9963 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -629,6 +629,9 @@ namespace Bloxstrap if (Directory.Exists(AppData.Directory)) { + if (Directory.Exists(AppData.OldDirectory)) + Directory.Delete(AppData.OldDirectory, true); + try { // test to see if any files are in use diff --git a/Bloxstrap/UI/Elements/About/Pages/SupportersPage.xaml b/Bloxstrap/UI/Elements/About/Pages/SupportersPage.xaml index 925a520..9620493 100644 --- a/Bloxstrap/UI/Elements/About/Pages/SupportersPage.xaml +++ b/Bloxstrap/UI/Elements/About/Pages/SupportersPage.xaml @@ -10,6 +10,7 @@ xmlns:resources="clr-namespace:Bloxstrap.Resources" mc:Ignorable="d" d:DesignHeight="1500" d:DesignWidth="800" + SizeChanged="UiPage_SizeChanged" Title="AboutPage" Scrollable="True"> @@ -93,7 +94,7 @@ - + @@ -122,7 +123,7 @@ - + diff --git a/Bloxstrap/UI/Elements/About/Pages/SupportersPage.xaml.cs b/Bloxstrap/UI/Elements/About/Pages/SupportersPage.xaml.cs index c824558..546d2e4 100644 --- a/Bloxstrap/UI/Elements/About/Pages/SupportersPage.xaml.cs +++ b/Bloxstrap/UI/Elements/About/Pages/SupportersPage.xaml.cs @@ -1,4 +1,6 @@ -using Bloxstrap.UI.ViewModels.About; +using System.Windows; + +using Bloxstrap.UI.ViewModels.About; namespace Bloxstrap.UI.Elements.About.Pages { @@ -7,10 +9,15 @@ namespace Bloxstrap.UI.Elements.About.Pages /// public partial class SupportersPage { + private readonly SupportersViewModel _viewModel = new(); + public SupportersPage() { - DataContext = new SupportersViewModel(); + DataContext = _viewModel; InitializeComponent(); } + + private void UiPage_SizeChanged(object sender, SizeChangedEventArgs e) + => _viewModel.WindowResizeEvent?.Invoke(sender, e); } } diff --git a/Bloxstrap/UI/ViewModels/About/SupportersViewModel.cs b/Bloxstrap/UI/ViewModels/About/SupportersViewModel.cs index cc5afbc..8fca94b 100644 --- a/Bloxstrap/UI/ViewModels/About/SupportersViewModel.cs +++ b/Bloxstrap/UI/ViewModels/About/SupportersViewModel.cs @@ -1,4 +1,6 @@ -namespace Bloxstrap.UI.ViewModels.About +using System.Windows; + +namespace Bloxstrap.UI.ViewModels.About { public class SupportersViewModel : NotifyPropertyChangedViewModel { @@ -8,12 +10,32 @@ public string LoadError { get; set; } = ""; + public int Columns { get; set; } = 3; + + public SizeChangedEventHandler? WindowResizeEvent; + public SupportersViewModel() { + WindowResizeEvent += OnWindowResize; + // this will cause momentary freezes only when ran under the debugger LoadSupporterData(); } + private void OnWindowResize(object sender, SizeChangedEventArgs e) + { + if (!e.WidthChanged) + return; + + int newCols = (int)Math.Floor(e.NewSize.Width / 200); + + if (Columns == newCols) + return; + + Columns = newCols; + OnPropertyChanged(nameof(Columns)); + } + public async void LoadSupporterData() { const string LOG_IDENT = "AboutViewModel::LoadSupporterData"; From aab9e153d16f96ddc27a830a7ff1c892701152ad Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sat, 26 Oct 2024 23:51:40 +0100 Subject: [PATCH 20/33] Add support for running RobloxPlayerBeta as Admin for some reason, i'm unable to delete the appcompatflags entry programatically in the uninstall process? weird --- Bloxstrap/Bootstrapper.cs | 32 +++++++++++++++++++++++++++- Bloxstrap/Utility/WindowsRegistry.cs | 2 ++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index a0d9963..6b73caa 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -11,6 +11,8 @@ #warning "Automatic updater debugging is enabled" #endif +using System.ComponentModel; +using System.Data; using System.Windows; using System.Windows.Forms; using System.Windows.Shell; @@ -337,7 +339,12 @@ namespace Bloxstrap WorkingDirectory = AppData.Directory }; - if (_launchMode == LaunchMode.StudioAuth) + if (_launchMode == LaunchMode.Player && ShouldRunAsAdmin()) + { + startInfo.Verb = "runas"; + startInfo.UseShellExecute = true; + } + else if (_launchMode == LaunchMode.StudioAuth) { Process.Start(startInfo); return; @@ -372,6 +379,11 @@ namespace Bloxstrap using var process = Process.Start(startInfo)!; _appPid = process.Id; } + catch (Win32Exception ex) when (ex.NativeErrorCode == 1223) + { + // 1223 = ERROR_CANCELLED, gets thrown if a UAC prompt is cancelled + return; + } catch (Exception) { // attempt a reinstall on next launch @@ -456,6 +468,24 @@ namespace Bloxstrap Thread.Sleep(1000); } + private bool ShouldRunAsAdmin() + { + foreach (var root in WindowsRegistry.Roots) + { + using var key = root.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers"); + + if (key is null) + continue; + + string? flags = (string?)key.GetValue(AppData.ExecutablePath); + + if (flags is not null && flags.Contains("RUNASADMIN", StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + public void Cancel() { const string LOG_IDENT = "Bootstrapper::Cancel"; diff --git a/Bloxstrap/Utility/WindowsRegistry.cs b/Bloxstrap/Utility/WindowsRegistry.cs index 23ca712..c13cd65 100644 --- a/Bloxstrap/Utility/WindowsRegistry.cs +++ b/Bloxstrap/Utility/WindowsRegistry.cs @@ -5,6 +5,8 @@ namespace Bloxstrap.Utility static class WindowsRegistry { private const string RobloxPlaceKey = "Roblox.Place"; + + public static readonly List Roots = new() { Registry.CurrentUser, Registry.LocalMachine }; public static void RegisterProtocol(string key, string name, string handler, string handlerParam = "%1") { From 72f1d70342f68765b114d523af4a6db7e4e719fd Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 27 Oct 2024 16:52:54 +0000 Subject: [PATCH 21/33] Fix log file access through activity watcher icon wasn't showing due to the changes made to how the activity watcher works --- Bloxstrap/Integrations/ActivityWatcher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/Integrations/ActivityWatcher.cs b/Bloxstrap/Integrations/ActivityWatcher.cs index 2e0483a..8132250 100644 --- a/Bloxstrap/Integrations/ActivityWatcher.cs +++ b/Bloxstrap/Integrations/ActivityWatcher.cs @@ -102,8 +102,6 @@ await Task.Delay(1000); } - OnLogOpen?.Invoke(this, EventArgs.Empty); - LogLocation = logFileInfo.FullName; } else @@ -111,6 +109,8 @@ logFileInfo = new FileInfo(LogLocation); } + OnLogOpen?.Invoke(this, EventArgs.Empty); + var logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}"); From 705cf078503dfa279282023797c5c73f49faa0a8 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 27 Oct 2024 16:56:50 +0000 Subject: [PATCH 22/33] Update description for supporters page --- Bloxstrap/Resources/Strings.Designer.cs | 3 ++- Bloxstrap/Resources/Strings.resx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 41b4c8f..e175e6c 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -70,7 +70,8 @@ namespace Bloxstrap.Resources { } /// - /// Looks up a localized string similar to These are the people currently supporting Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!. + /// Looks up a localized string similar to These are the people who've supported Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here! + ///Every person here is ranked by their overall pledge.. /// public static string About_Supporters_Description { get { diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 2b9ca6e..777fd95 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -1118,7 +1118,8 @@ Check if Roblox works with [the original launcher]({1}). If it doesn't, then thi Supporters - These are the people currently supporting Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here! + These are the people who've supported Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here! +Every person here is ranked by their overall pledge. Your Settings could not be loaded. They have been reset to the default configuration. From 637af71299a0bc112bdd65c63d2d3acbae84df99 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 27 Oct 2024 16:58:27 +0000 Subject: [PATCH 23/33] Trim name input in flag editor --- Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorPage.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorPage.xaml.cs b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorPage.xaml.cs index 4577618..b49438d 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorPage.xaml.cs @@ -104,7 +104,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages return; if (dialog.Tabs.SelectedIndex == 0) - AddSingle(dialog.FlagNameTextBox.Text, dialog.FlagValueTextBox.Text); + AddSingle(dialog.FlagNameTextBox.Text.Trim(), dialog.FlagValueTextBox.Text); else if (dialog.Tabs.SelectedIndex == 1) ImportJSON(dialog.JsonTextBox.Text); } From 59bcf6fd27b9a525f7c013bace61791db70fa783 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 27 Oct 2024 17:15:41 +0000 Subject: [PATCH 24/33] Fix edge case regarding package restoration Exception is thrown if the Downloads folder does not exist when trying to restore the original version of a modded file Addresses #3308 --- Bloxstrap/Bootstrapper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 6b73caa..232081f 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1053,6 +1053,8 @@ namespace Bloxstrap if (_cancelTokenSource.IsCancellationRequested) return; + Directory.CreateDirectory(Paths.Downloads); + string packageUrl = Deployment.GetLocation($"/{_latestVersionGuid}-{package.Name}"); string robloxPackageLocation = Path.Combine(Paths.LocalAppData, "Roblox", "Downloads", package.Signature); From cac081eeb811b64884f27f54cd720664de8daf87 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Sun, 27 Oct 2024 20:05:23 +0000 Subject: [PATCH 25/33] Add analytic logging of exceptions --- Bloxstrap/App.xaml.cs | 20 +++++++++++++++++++ Bloxstrap/Logger.cs | 2 ++ .../Elements/Dialogs/ExceptionDialog.xaml.cs | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/App.xaml.cs b/Bloxstrap/App.xaml.cs index 7542ed4..5dced57 100644 --- a/Bloxstrap/App.xaml.cs +++ b/Bloxstrap/App.xaml.cs @@ -107,6 +107,8 @@ namespace Bloxstrap _showingExceptionDialog = true; + SendLog(); + if (Bootstrapper?.Dialog != null) { if (Bootstrapper.Dialog.TaskbarProgressValue == 0) @@ -159,6 +161,24 @@ namespace Bloxstrap } } + public static async void SendLog() + { + if (!Settings.Prop.EnableAnalytics || !IsProductionBuild) + return; + + try + { + await HttpClient.PostAsync( + $"https://bloxstraplabs.com/metrics/post-exception", + new StringContent(Logger.AsDocument) + ); + } + catch (Exception ex) + { + Logger.WriteException("App::SendLog", ex); + } + } + protected override void OnStartup(StartupEventArgs e) { const string LOG_IDENT = "App::OnStartup"; diff --git a/Bloxstrap/Logger.cs b/Bloxstrap/Logger.cs index ff6db1a..884e077 100644 --- a/Bloxstrap/Logger.cs +++ b/Bloxstrap/Logger.cs @@ -12,6 +12,8 @@ public bool NoWriteMode = false; public string? FileLocation; + public string AsDocument => String.Join('\n', History); + public void Initialize(bool useTempDir = false) { const string LOG_IDENT = "Logger::Initialize"; diff --git a/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml.cs b/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml.cs index 9301fdf..d1d9411 100644 --- a/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml.cs +++ b/Bloxstrap/UI/Elements/Dialogs/ExceptionDialog.xaml.cs @@ -30,7 +30,7 @@ namespace Bloxstrap.UI.Elements.Dialogs string wikiUrl = $"{repoUrl}/wiki"; string title = HttpUtility.UrlEncode($"[BUG] {exception.GetType()}: {exception.Message}"); - string log = HttpUtility.UrlEncode(String.Join('\n', App.Logger.History)); + string log = HttpUtility.UrlEncode(App.Logger.AsDocument); string issueUrl = $"{repoUrl}/issues/new?template=bug_report.yaml&title={title}&log={log}"; @@ -58,7 +58,7 @@ namespace Bloxstrap.UI.Elements.Dialogs if (App.Logger.Initialized && !String.IsNullOrEmpty(App.Logger.FileLocation)) Utilities.ShellExecute(App.Logger.FileLocation); else - Clipboard.SetDataObject(String.Join("\r\n", App.Logger.History)); + Clipboard.SetDataObject(App.Logger.AsDocument); }; CloseButton.Click += delegate From fa7ebf4f9df2bd5fcf65c0857e4046d44b3a9334 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Mon, 28 Oct 2024 00:35:37 +0000 Subject: [PATCH 26/33] Add easy exporting of diagnostic data --- Bloxstrap/Resources/Strings.Designer.cs | 54 +++++++++++++ Bloxstrap/Resources/Strings.resx | 23 ++++++ .../Settings/Pages/BloxstrapPage.xaml | 41 +++++++++- .../ViewModels/Settings/BloxstrapViewModel.cs | 75 ++++++++++++++++++- .../ViewModels/Settings/FastFlagsViewModel.cs | 3 +- 5 files changed, 192 insertions(+), 4 deletions(-) diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index e175e6c..0201300 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -432,6 +432,15 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Export. + /// + public static string Common_Export { + get { + return ResourceManager.GetString("Common.Export", resourceCulture); + } + } + /// /// Looks up a localized string similar to Help. /// @@ -1386,6 +1395,15 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Zip archive. + /// + public static string FileTypes_ZipArchive { + get { + return ResourceManager.GetString("FileTypes.ZipArchive", resourceCulture); + } + } + /// /// Looks up a localized string similar to Bloxstrap has been upgraded to v{0}. /// @@ -2103,6 +2121,42 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to Gather information that can be uploaded online to troubleshoot a problem you're having.. + /// + public static string Menu_Bloxstrap_ExportData_Description { + get { + return ResourceManager.GetString("Menu.Bloxstrap.ExportData.Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bloxstrap configuration. + /// + public static string Menu_Bloxstrap_ExportData_ExportConfig { + get { + return ResourceManager.GetString("Menu.Bloxstrap.ExportData.ExportConfig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All Bloxstrap logs. + /// + public static string Menu_Bloxstrap_ExportData_ExportLogs { + get { + return ResourceManager.GetString("Menu.Bloxstrap.ExportData.ExportLogs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export diagnostic data. + /// + public static string Menu_Bloxstrap_ExportData_Title { + get { + return ResourceManager.GetString("Menu.Bloxstrap.ExportData.Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to Add new. /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 777fd95..aea14a5 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -906,6 +906,7 @@ Selecting 'No' will ignore this warning and continue installation. JSON files + Shown in the open file dialog, where the file type selection dropdown is, e.g. "JSON files (*.json)" The entry for '{0}' is not valid as the value must be a boolean (either 'True' or 'False') @@ -1248,4 +1249,26 @@ Would you like to enable test mode? Please close any applications that may be using Roblox's files, and relaunch. This is *not* for when Roblox is still running when trying to upgrade. This applies to files being open (i.e. image assets) + + Zip archive + Shown in the save file dialog, where the file type selection dropdown is, e.g. "Zip archive (*.zip)" + + + Export + Currently used under the "Bloxstrap" settings tab for the button to export diagnostic data + + + Export diagnostic data + + + Gather information that can be uploaded online to troubleshoot a problem you're having. + + + Bloxstrap configuration + Label that appears next to a checkbox + + + All Bloxstrap logs + Label that appears next to a checkbox + \ No newline at end of file diff --git a/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml index d96f081..d4f731a 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/BloxstrapPage.xaml @@ -8,7 +8,7 @@ xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls" xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.Settings" xmlns:resources="clr-namespace:Bloxstrap.Resources" - d:DataContext="{d:DesignInstance Type=models:BehaviourViewModel}" + d:DataContext="{d:DesignInstance Type=models:BloxstrapViewModel}" mc:Ignorable="d" d:DesignHeight="600" d:DesignWidth="800" Title="BehaviourPage" @@ -28,5 +28,44 @@ Description="{Binding Source={x:Static resources:Strings.Menu_Bloxstrap_Analytics_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://github.com/bloxstraplabs/bloxstrap/wiki/Privacy-Policy#analytical-functionality'}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/ViewModels/Settings/BloxstrapViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/BloxstrapViewModel.cs index bd913d1..016f69f 100644 --- a/Bloxstrap/UI/ViewModels/Settings/BloxstrapViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/BloxstrapViewModel.cs @@ -1,4 +1,9 @@ -namespace Bloxstrap.UI.ViewModels.Settings +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using ICSharpCode.SharpZipLib.Zip; +using Microsoft.Win32; + +namespace Bloxstrap.UI.ViewModels.Settings { public class BloxstrapViewModel : NotifyPropertyChangedViewModel { @@ -13,5 +18,73 @@ get => App.Settings.Prop.EnableAnalytics; set => App.Settings.Prop.EnableAnalytics = value; } + + public bool ShouldExportConfig { get; set; } = true; + + public bool ShouldExportLogs { get; set; } = true; + + public ICommand ExportDataCommand => new RelayCommand(ExportData); + + private void ExportData() + { + string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'"); + + var dialog = new SaveFileDialog + { + FileName = $"Bloxstrap-export-{timestamp}.zip", + Filter = $"{Strings.FileTypes_ZipArchive}|*.zip" + }; + + if (dialog.ShowDialog() != true) + return; + + using var memStream = new MemoryStream(); + using var zipStream = new ZipOutputStream(memStream); + + if (ShouldExportConfig) + { + var files = new List() + { + App.Settings.FileLocation, + App.State.FileLocation, + App.FastFlags.FileLocation + }; + + AddFilesToZipStream(zipStream, files, "Config/"); + } + + if (ShouldExportLogs && Directory.Exists(Paths.Logs)) + { + var files = Directory.GetFiles(Paths.Logs) + .Where(x => !x.Equals(App.Logger.FileLocation, StringComparison.OrdinalIgnoreCase)); + + AddFilesToZipStream(zipStream, files, "Logs/"); + } + + zipStream.CloseEntry(); + memStream.Position = 0; + + using var outputStream = File.OpenWrite(dialog.FileName); + memStream.CopyTo(outputStream); + + Process.Start("explorer.exe", $"/select,\"{dialog.FileName}\""); + } + + private void AddFilesToZipStream(ZipOutputStream zipStream, IEnumerable files, string directory) + { + foreach (string file in files) + { + if (!File.Exists(file)) + continue; + + var entry = new ZipEntry(directory + Path.GetFileName(file)); + entry.DateTime = DateTime.Now; + + zipStream.PutNextEntry(entry); + + using var fileStream = File.OpenRead(file); + fileStream.CopyTo(zipStream); + } + } } } diff --git a/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs index cc1df4f..7f63032 100644 --- a/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/FastFlagsViewModel.cs @@ -1,5 +1,4 @@ -using System.Windows; -using System.Windows.Input; +using System.Windows.Input; using CommunityToolkit.Mvvm.Input; From ba561a2421a9d07c8154cf9ae004eb0555bf37a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:18:35 +0000 Subject: [PATCH 27/33] Bump wpfui from `c4c58c5` to `9080158` (#3470) Bumps [wpfui](https://github.com/bloxstraplabs/wpfui) from `c4c58c5` to `9080158`. - [Commits](https://github.com/bloxstraplabs/wpfui/compare/c4c58c589970a66b27a9de41ab1b6b6539918b52...9080158ba8d496501146d1167aae910898eff9af) --- updated-dependencies: - dependency-name: wpfui dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- wpfui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wpfui b/wpfui index c4c58c5..9080158 160000 --- a/wpfui +++ b/wpfui @@ -1 +1 @@ -Subproject commit c4c58c589970a66b27a9de41ab1b6b6539918b52 +Subproject commit 9080158ba8d496501146d1167aae910898eff9af From bd506ae54567f62f94a99ba7a95295ce24e72af0 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 29 Oct 2024 21:54:35 +0000 Subject: [PATCH 28/33] Improve handling of bootstrapper connection errors --- Bloxstrap/Bootstrapper.cs | 37 +++++++++++-------- .../Exceptions/InvalidChannelException.cs | 10 +++++ Bloxstrap/Resources/Strings.Designer.cs | 27 +++++--------- Bloxstrap/Resources/Strings.resx | 7 +--- Bloxstrap/RobloxInterfaces/Deployment.cs | 21 ++++++++--- 5 files changed, 57 insertions(+), 45 deletions(-) create mode 100644 Bloxstrap/Exceptions/InvalidChannelException.cs diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 232081f..409c8d0 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -125,15 +125,14 @@ namespace Bloxstrap App.Logger.WriteLine(LOG_IDENT, "Connectivity check failed"); App.Logger.WriteException(LOG_IDENT, exception); - string message = Strings.Dialog_Connectivity_Preventing; + string message = Strings.Dialog_Connectivity_BadConnection; - if (exception.GetType() == typeof(AggregateException)) + if (exception is AggregateException) exception = exception.InnerException!; - if (exception.GetType() == typeof(HttpRequestException)) + // https://gist.github.com/pizzaboxer/4b58303589ee5b14cc64397460a8f386 + if (exception is HttpRequestException && exception.InnerException is null) message = String.Format(Strings.Dialog_Connectivity_RobloxDown, "[status.roblox.com](https://status.roblox.com)"); - else if (exception.GetType() == typeof(TaskCanceledException)) - message = Strings.Dialog_Connectivity_TimedOut; if (_mustUpgrade) message += $"\n\n{Strings.Dialog_Connectivity_RobloxUpgradeNeeded}\n\n{Strings.Dialog_Connectivity_TryAgainLater}"; @@ -252,6 +251,10 @@ namespace Bloxstrap Dialog?.CloseBootstrapper(); } + /// + /// Will throw whatever HttpClient can throw + /// + /// private async Task GetLatestVersionInfo() { const string LOG_IDENT = "Bootstrapper::GetLatestVersionInfo"; @@ -262,7 +265,11 @@ namespace Bloxstrap using var key = Registry.CurrentUser.CreateSubKey($"SOFTWARE\\ROBLOX Corporation\\Environments\\{AppData.RegistryName}\\Channel"); - var match = Regex.Match(App.LaunchSettings.RobloxLaunchArgs, "channel:([a-zA-Z0-9-_]+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + var match = Regex.Match( + App.LaunchSettings.RobloxLaunchArgs, + "channel:([a-zA-Z0-9-_]+)", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant + ); if (match.Groups.Count == 2) { @@ -273,9 +280,12 @@ namespace Bloxstrap Deployment.Channel = value.ToLowerInvariant(); } - App.Logger.WriteLine(LOG_IDENT, "Got channel as " + (String.IsNullOrEmpty(Deployment.Channel) ? Deployment.DefaultChannel : Deployment.Channel)); + if (String.IsNullOrEmpty(Deployment.Channel)) + Deployment.Channel = Deployment.DefaultChannel; - if (Deployment.Channel != "production") + App.Logger.WriteLine(LOG_IDENT, $"Got channel as {Deployment.DefaultChannel}"); + + if (!Deployment.IsDefaultChannel) App.SendStat("robloxChannel", Deployment.Channel); ClientVersion clientVersion; @@ -284,14 +294,9 @@ namespace Bloxstrap { clientVersion = await Deployment.GetInfo(); } - catch (HttpRequestException ex) + catch (InvalidChannelException ex) { - if (ex.StatusCode is not HttpStatusCode.Unauthorized - and not HttpStatusCode.Forbidden - and not HttpStatusCode.NotFound) - throw; - - App.Logger.WriteLine(LOG_IDENT, $"Changing channel from {Deployment.Channel} to {Deployment.DefaultChannel} because HTTP {(int)ex.StatusCode}"); + App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because {ex.StatusCode}"); Deployment.Channel = Deployment.DefaultChannel; clientVersion = await Deployment.GetInfo(); @@ -299,7 +304,7 @@ namespace Bloxstrap if (clientVersion.IsBehindDefaultChannel) { - App.Logger.WriteLine(LOG_IDENT, $"Changing channel from {Deployment.Channel} to {Deployment.DefaultChannel} because channel is behind production"); + App.Logger.WriteLine(LOG_IDENT, $"Resetting channel from {Deployment.Channel} because it's behind production"); Deployment.Channel = Deployment.DefaultChannel; clientVersion = await Deployment.GetInfo(); diff --git a/Bloxstrap/Exceptions/InvalidChannelException.cs b/Bloxstrap/Exceptions/InvalidChannelException.cs new file mode 100644 index 0000000..eff6d79 --- /dev/null +++ b/Bloxstrap/Exceptions/InvalidChannelException.cs @@ -0,0 +1,10 @@ +namespace Bloxstrap.Exceptions +{ + public class InvalidChannelException : Exception + { + public HttpStatusCode? StatusCode; + + public InvalidChannelException(HttpStatusCode? statusCode) : base() + => StatusCode = statusCode; + } +} diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 0201300..f50f201 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -838,6 +838,15 @@ namespace Bloxstrap.Resources { } } + /// + /// Looks up a localized string similar to A connection could not be made, which likely indicates a poor internet connection or a firewall block. If your connection is fine, please ensure that your antivirus isn't blocking Bloxstrap.. + /// + public static string Dialog_Connectivity_BadConnection { + get { + return ResourceManager.GetString("Dialog.Connectivity.BadConnection", resourceCulture); + } + } + /// /// Looks up a localized string similar to More information:. /// @@ -847,15 +856,6 @@ namespace Bloxstrap.Resources { } } - /// - /// Looks up a localized string similar to Something is likely preventing Bloxstrap from connecting to the internet.. - /// - public static string Dialog_Connectivity_Preventing { - get { - return ResourceManager.GetString("Dialog.Connectivity.Preventing", resourceCulture); - } - } - /// /// Looks up a localized string similar to Roblox may be down right now. See {0} for more information.. /// @@ -883,15 +883,6 @@ namespace Bloxstrap.Resources { } } - /// - /// Looks up a localized string similar to The connection timed out, which could indicate a poor internet connection or a firewall block.. - /// - public static string Dialog_Connectivity_TimedOut { - get { - return ResourceManager.GetString("Dialog.Connectivity.TimedOut", resourceCulture); - } - } - /// /// Looks up a localized string similar to Connectivity error. /// diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index aea14a5..96f6d0b 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -123,14 +123,11 @@ Roblox is currently running, and launching another instance will close it. Are you sure you want to continue launching? - - Something is likely preventing Bloxstrap from connecting to the internet. - Roblox may be down right now. See {0} for more information. - - The connection timed out, which could indicate a poor internet connection or a firewall block. + + A connection could not be made, which likely indicates a poor internet connection or a firewall block. If your connection is fine, please ensure that your antivirus isn't blocking Bloxstrap. You must first install Bloxstrap before uninstalling. diff --git a/Bloxstrap/RobloxInterfaces/Deployment.cs b/Bloxstrap/RobloxInterfaces/Deployment.cs index f6ee997..da88c14 100644 --- a/Bloxstrap/RobloxInterfaces/Deployment.cs +++ b/Bloxstrap/RobloxInterfaces/Deployment.cs @@ -10,10 +10,17 @@ public static string BinaryType = "WindowsPlayer"; - public static bool IsDefaultChannel => String.Compare(Channel, DefaultChannel, StringComparison.OrdinalIgnoreCase) == 0; + public static bool IsDefaultChannel => Channel.Equals(DefaultChannel, StringComparison.OrdinalIgnoreCase); public static string BaseUrl { get; private set; } = null!; - + + public static readonly List BadChannelCodes = new() + { + HttpStatusCode.Unauthorized, + HttpStatusCode.Forbidden, + HttpStatusCode.NotFound + }; + private static readonly Dictionary ClientVersionCache = new(); // a list of roblox deployment locations that we check for, in case one of them don't work @@ -97,7 +104,9 @@ { if (exceptions.Any()) return exceptions[0]; - return new TaskCanceledException("All tasks have been cancelled"); // we can't add TaskCanceledExceptions to the list + + // task cancellation exceptions don't get added to the list + return new TaskCanceledException("All connection attempts timed out."); } App.Logger.WriteLine(LOG_IDENT, $"Got {BaseUrl} as the optimal base URL"); @@ -157,10 +166,10 @@ { clientVersion = await Http.GetJson("https://clientsettingscdn.roblox.com" + path); } - catch (HttpRequestException) + catch (HttpRequestException httpEx) + when (!isDefaultChannel && BadChannelCodes.Contains(httpEx.StatusCode)) { - // throw up the exception handler chain, as we shouldn't be the one handling it - throw; + throw new InvalidChannelException(httpEx.StatusCode); } catch (Exception ex) { From 2c70430dfa312762cb6aa10fd74486cd00773419 Mon Sep 17 00:00:00 2001 From: Matt <97983689+bluepilledgreat@users.noreply.github.com> Date: Tue, 29 Oct 2024 21:55:34 +0000 Subject: [PATCH 29/33] improve the flag editor warning viewmodel (#3532) * improve the flag editor warning viewmodel - no longer creates a new viewmodel every page reload - fixes an oversight * stop countdown on unload * move the viewmodel to a variable makes everything look cleaner * remove initialload check --- .../Pages/FastFlagEditorWarningPage.xaml | 1 + .../Pages/FastFlagEditorWarningPage.xaml.cs | 19 +++++----- .../FastFlagEditorWarningViewModel.cs | 37 ++++++++++++++++--- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorWarningPage.xaml b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorWarningPage.xaml index 9a2b938..55b0d71 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorWarningPage.xaml +++ b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorWarningPage.xaml @@ -10,6 +10,7 @@ d:DesignHeight="450" d:DesignWidth="800" Scrollable="True" Loaded="Page_Loaded" + Unloaded="Page_Unloaded" Title="FastFlagEditorWarningPage"> diff --git a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorWarningPage.xaml.cs b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorWarningPage.xaml.cs index 89d1e4c..9b3b08a 100644 --- a/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorWarningPage.xaml.cs +++ b/Bloxstrap/UI/Elements/Settings/Pages/FastFlagEditorWarningPage.xaml.cs @@ -8,25 +8,24 @@ namespace Bloxstrap.UI.Elements.Settings.Pages /// public partial class FastFlagEditorWarningPage { - private bool _initialLoad = false; + private FastFlagEditorWarningViewModel _viewModel; public FastFlagEditorWarningPage() { - DataContext = new FastFlagEditorWarningViewModel(this); + _viewModel = new FastFlagEditorWarningViewModel(this); + DataContext = _viewModel; + InitializeComponent(); } private void Page_Loaded(object sender, RoutedEventArgs e) { - // refresh datacontext on page load to reset timer + _viewModel.StartCountdown(); + } - if (!_initialLoad) - { - _initialLoad = true; - return; - } - - DataContext = new FastFlagEditorWarningViewModel(this); + private void Page_Unloaded(object sender, RoutedEventArgs e) + { + _viewModel.StopCountdown(); } } } diff --git a/Bloxstrap/UI/ViewModels/Settings/FastFlagEditorWarningViewModel.cs b/Bloxstrap/UI/ViewModels/Settings/FastFlagEditorWarningViewModel.cs index c335d35..957f62b 100644 --- a/Bloxstrap/UI/ViewModels/Settings/FastFlagEditorWarningViewModel.cs +++ b/Bloxstrap/UI/ViewModels/Settings/FastFlagEditorWarningViewModel.cs @@ -13,6 +13,8 @@ namespace Bloxstrap.UI.ViewModels.Settings { private Page _page; + private CancellationTokenSource? _cancellationTokenSource; + public string ContinueButtonText { get; set; } = ""; public bool CanContinue { get; set; } = false; @@ -24,17 +26,40 @@ namespace Bloxstrap.UI.ViewModels.Settings public FastFlagEditorWarningViewModel(Page page) { _page = page; - DoCountdown(); } - private async void DoCountdown() + public void StopCountdown() { + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource = null; + } + + public void StartCountdown() + { + StopCountdown(); + + _cancellationTokenSource = new CancellationTokenSource(); + DoCountdown(_cancellationTokenSource.Token); + } + + private async void DoCountdown(CancellationToken token) + { + CanContinue = false; + OnPropertyChanged(nameof(CanContinue)); + for (int i = 10; i > 0; i--) { ContinueButtonText = $"({i}) {Strings.Menu_FastFlagEditor_Warning_Continue}"; OnPropertyChanged(nameof(ContinueButtonText)); - await Task.Delay(1000); + try + { + await Task.Delay(1000, token); + } + catch (TaskCanceledException) + { + return; + } } ContinueButtonText = Strings.Menu_FastFlagEditor_Warning_Continue; @@ -42,9 +67,6 @@ namespace Bloxstrap.UI.ViewModels.Settings CanContinue = true; OnPropertyChanged(nameof(CanContinue)); - - App.State.Prop.ShowFFlagEditorWarning = false; - App.State.Save(); } private void Continue() @@ -52,6 +74,9 @@ namespace Bloxstrap.UI.ViewModels.Settings if (!CanContinue) return; + App.State.Prop.ShowFFlagEditorWarning = false; + App.State.Save(); // should we be force saving here? + if (Window.GetWindow(_page) is INavigationWindow window) window.Navigate(typeof(FastFlagEditorPage)); } From 749a8d6f3a2d33b6e1ae7598f9eef99b3f372863 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 29 Oct 2024 22:07:23 +0000 Subject: [PATCH 30/33] Add package entry for platform-dictionaries on my life this is the *LAST* time we are doing this - i didn't get time to implement a remote config package map system --- Bloxstrap/AppData/CommonAppData.cs | 1 + Bloxstrap/Bootstrapper.cs | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Bloxstrap/AppData/CommonAppData.cs b/Bloxstrap/AppData/CommonAppData.cs index 4a74118..54aaaa0 100644 --- a/Bloxstrap/AppData/CommonAppData.cs +++ b/Bloxstrap/AppData/CommonAppData.cs @@ -32,6 +32,7 @@ namespace Bloxstrap.AppData { "content-textures3.zip", @"PlatformContent\pc\textures\" }, { "content-terrain.zip", @"PlatformContent\pc\terrain\" }, { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" }, + { "content-platform-dictionaries.zip", @"PlatformContent\pc\shared_compression_dictionaries\" }, { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" }, { "extracontent-translations.zip", @"ExtraContent\translations\" }, diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 409c8d0..207c4a4 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -1193,7 +1193,15 @@ namespace Bloxstrap { const string LOG_IDENT = "Bootstrapper::ExtractPackage"; - string packageFolder = Path.Combine(AppData.Directory, AppData.PackageDirectoryMap[package.Name]); + string? packageDir = AppData.PackageDirectoryMap.GetValueOrDefault(package.Name); + + if (packageDir is null) + { + App.Logger.WriteLine(LOG_IDENT, $"WARNING: {package.Name} was not found in the package map!"); + return; + } + + string packageFolder = Path.Combine(AppData.Directory, packageDir); string? fileFilter = null; // for sharpziplib, each file in the filter needs to be a regex From 49b1eb056935a6f99090a353558ee7d3291f87da Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 29 Oct 2024 22:08:36 +0000 Subject: [PATCH 31/33] Update ci-release.yml --- .github/workflows/ci-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 39087c1..4038dcb 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -79,7 +79,7 @@ jobs: api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' organization-id: '107b3de5-057b-42fc-a985-3546e4261775' project-slug: 'bloxstrap' - signing-policy-slug: 'test-signing' + signing-policy-slug: 'release-signing' artifact-configuration-slug: 'github-ci' github-artifact-id: '${{ needs.build.outputs.artifact-id }}' wait-for-completion: true From 2be2f4d0d275e60eabf2f63ecbbe4e38b2aa0773 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 29 Oct 2024 22:32:30 +0000 Subject: [PATCH 32/33] Enable code signing on production releases --- .github/workflows/ci-release.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 4038dcb..7d2c607 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -39,17 +39,17 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') steps: - # - name: Sign and download artifact - # uses: signpath/github-action-submit-signing-request@v1 - # with: - # api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' - # organization-id: '107b3de5-057b-42fc-a985-3546e4261775' - # project-slug: 'bloxstrap' - # signing-policy-slug: 'release-signing' - # artifact-configuration-slug: 'github-ci' - # github-artifact-id: '${{ needs.build.outputs.artifact-id }}' - # wait-for-completion: true - # output-artifact-directory: 'release' + - name: Sign and download artifact + uses: signpath/github-action-submit-signing-request@v1 + with: + api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' + organization-id: '107b3de5-057b-42fc-a985-3546e4261775' + project-slug: 'bloxstrap' + signing-policy-slug: 'release-signing' + artifact-configuration-slug: 'github-ci' + github-artifact-id: '${{ needs.build.outputs.artifact-id }}' + wait-for-completion: true + output-artifact-directory: 'release' - name: Download x64 release artifact uses: actions/download-artifact@v4 From 7bc95e7dd4e97afa5de40d88f94af25695fd3b64 Mon Sep 17 00:00:00 2001 From: pizzaboxer Date: Tue, 29 Oct 2024 23:32:28 +0000 Subject: [PATCH 33/33] Enable code signing *for real this time* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 😭 --- .github/workflows/ci-release.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 7d2c607..caf8255 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -51,12 +51,6 @@ jobs: wait-for-completion: true output-artifact-directory: 'release' - - name: Download x64 release artifact - uses: actions/download-artifact@v4 - with: - name: Bloxstrap (Release) (${{ github.sha }}) - path: release - - name: Rename binaries run: mv release/Bloxstrap.exe Bloxstrap-${{ github.ref_name }}.exe